@swarmclawai/swarmclaw 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -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 undefined
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,30 @@ 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)
283
+ collectSendFilePaths(args.file, candidates)
147
284
  collectSendFilePaths(args.files, candidates)
148
285
 
149
286
  const nestedInput = args.input
@@ -166,17 +303,94 @@ export function normalizeSendFilePaths(args: Record<string, unknown>): string[]
166
303
  return [...deduped]
167
304
  }
168
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
+
169
379
  async function executeSendFile(args: Record<string, unknown>, bctx: { cwd: string }) {
170
380
  try {
171
- const paths = normalizeSendFilePaths(args)
381
+ const explicitPaths = normalizeSendFilePaths(args)
382
+ const paths = explicitPaths.length > 0 ? explicitPaths : findRecentSendFileFallbackPaths(bctx.cwd)
172
383
  if (paths.length === 0) {
173
384
  return 'Error: filePath/path is required (or provide files[] / input.files[]).'
174
385
  }
386
+ if (explicitPaths.length === 0 && paths.length !== 1) {
387
+ return 'Error: filePath/path is required (or provide files[] / input.files[]).'
388
+ }
175
389
 
176
390
  const links: string[] = []
177
391
  const errors: string[] = []
178
392
  for (const rawPath of paths) {
179
- const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(bctx.cwd, rawPath)
393
+ const resolved = resolveSendFileSourcePath(bctx.cwd, rawPath)
180
394
  if (!fs.existsSync(resolved)) {
181
395
  errors.push(`file not found: ${rawPath}`)
182
396
  continue
@@ -202,11 +416,13 @@ async function executeSendFile(args: Record<string, unknown>, bctx: { cwd: strin
202
416
  const FilePlugin: Plugin = {
203
417
  name: 'Core Files',
204
418
  description: 'Complete file management: read, write, list, move, copy, delete, and send.',
205
- hooks: {} as PluginHooks,
419
+ hooks: {
420
+ getCapabilityDescription: () => 'I can read, write, copy, move, and send files (`read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `send_file`). When writing, I should always provide a target path (`filePath`, `path`, `filename`, or `name`) and the content (`content`, `text`, or `body`). When `send_file` returns a download link, I should copy that link exactly instead of rewriting it. Deleting files is destructive, so that may need explicit permission.',
421
+ } as PluginHooks,
206
422
  tools: [
207
423
  {
208
424
  name: 'files',
209
- description: 'Unified file management tool. Actions: read, write, list, copy, move, delete. Supports bulk writes via "files" array.',
425
+ 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.',
210
426
  parameters: {
211
427
  type: 'object',
212
428
  properties: {
@@ -235,7 +451,7 @@ const FilePlugin: Plugin = {
235
451
  },
236
452
  {
237
453
  name: 'send_file',
238
- description: 'Send a file to the user in chat.',
454
+ description: 'Send a file to the user in chat. Use the returned /api/uploads/... links exactly as provided.',
239
455
  parameters: {
240
456
  type: 'object',
241
457
  properties: {
@@ -265,7 +481,7 @@ getPluginManager().registerBuiltin('files', FilePlugin)
265
481
  * Legacy Bridge
266
482
  */
267
483
  export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
268
- if (!bctx.hasTool('files')) return []
484
+ if (!bctx.hasPlugin('files')) return []
269
485
 
270
486
  return [
271
487
  tool(
@@ -97,7 +97,7 @@ getPluginManager().registerBuiltin('git', GitPlugin)
97
97
  * Legacy Bridge
98
98
  */
99
99
  export function buildGitTools(bctx: ToolBuildContext): StructuredToolInterface[] {
100
- if (!bctx.hasTool('git')) return []
100
+ if (!bctx.hasPlugin('git')) return []
101
101
  return [
102
102
  tool(
103
103
  async (args) => executeGitAction(args, { cwd: bctx.cwd }),
@@ -97,7 +97,7 @@ getPluginManager().registerBuiltin('http', HttpPlugin)
97
97
  * Legacy Bridge
98
98
  */
99
99
  export function buildHttpTools(bctx: ToolBuildContext): StructuredToolInterface[] {
100
- if (!bctx.hasTool('http_request')) return []
100
+ if (!bctx.hasPlugin('http_request')) return []
101
101
 
102
102
  return [
103
103
  tool(
@@ -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
+ }