@swarmclawai/swarmclaw 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -0,0 +1,137 @@
1
+ import path from 'path'
2
+ import { z } from 'zod'
3
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
4
+ import type { Plugin, PluginHooks } from '@/types'
5
+ import { getPluginManager } from '../plugins'
6
+ import { runStructuredExtraction } from '../structured-extract'
7
+ import type { ToolBuildContext } from './context'
8
+ import { safePath } from './context'
9
+ import { normalizeToolInputArgs } from './normalize-tool-args'
10
+
11
+ function resolveSessionForExtraction(bctx: ToolBuildContext) {
12
+ const session = bctx.resolveCurrentSession?.()
13
+ if (!session) throw new Error('extract requires an active session context.')
14
+ return session
15
+ }
16
+
17
+ async function executeExtractAction(args: Record<string, unknown>, bctx: ToolBuildContext) {
18
+ const normalized = normalizeToolInputArgs(args)
19
+ const action = String(normalized.action || 'extract_structured').trim().toLowerCase()
20
+
21
+ try {
22
+ if (action === 'status') {
23
+ const session = resolveSessionForExtraction(bctx)
24
+ return JSON.stringify({
25
+ provider: session.provider || null,
26
+ model: session.model || null,
27
+ source: 'session',
28
+ supports: ['extract_structured', 'summarize'],
29
+ })
30
+ }
31
+
32
+ const session = resolveSessionForExtraction(bctx)
33
+ const filePath = typeof normalized.filePath === 'string' && normalized.filePath.trim()
34
+ ? (path.isAbsolute(normalized.filePath) ? path.resolve(normalized.filePath) : safePath(bctx.cwd, normalized.filePath))
35
+ : typeof normalized.path === 'string' && normalized.path.trim()
36
+ ? (path.isAbsolute(normalized.path) ? path.resolve(normalized.path) : safePath(bctx.cwd, normalized.path))
37
+ : null
38
+ const schema = action === 'summarize' ? undefined : normalized.schema
39
+ const instruction = typeof normalized.instruction === 'string'
40
+ ? normalized.instruction
41
+ : action === 'summarize'
42
+ ? (typeof normalized.prompt === 'string' ? normalized.prompt : 'Summarize the input and extract the main entities and key points.')
43
+ : typeof normalized.prompt === 'string'
44
+ ? normalized.prompt
45
+ : 'Extract the requested structured data.'
46
+ const result = await runStructuredExtraction({
47
+ session,
48
+ text: typeof normalized.text === 'string' ? normalized.text : typeof normalized.content === 'string' ? normalized.content : null,
49
+ filePath,
50
+ instruction,
51
+ schema,
52
+ preferOcr: normalized.preferOcr === true,
53
+ maxChars: typeof normalized.maxChars === 'number' ? Math.max(5_000, normalized.maxChars) : undefined,
54
+ })
55
+
56
+ return JSON.stringify({
57
+ object: result.object,
58
+ validationErrors: result.validationErrors,
59
+ provider: result.provider,
60
+ model: result.model,
61
+ source: {
62
+ kind: result.source.kind,
63
+ filePath: result.source.filePath || null,
64
+ method: result.source.artifact?.method || null,
65
+ fileName: result.source.artifact?.fileName || null,
66
+ },
67
+ raw: normalized.includeRaw === true ? result.raw : undefined,
68
+ })
69
+ } catch (err: unknown) {
70
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
71
+ }
72
+ }
73
+
74
+ const ExtractPlugin: Plugin = {
75
+ name: 'Extract',
76
+ enabledByDefault: false,
77
+ description: 'Run schema-driven structured extraction over text or local files using the current session model.',
78
+ hooks: {
79
+ getCapabilityDescription: () =>
80
+ 'I can turn unstructured text or documents into validated JSON with `extract`, using the current session provider/model and a caller-supplied schema.',
81
+ } as PluginHooks,
82
+ tools: [
83
+ {
84
+ name: 'extract',
85
+ description: 'Structured extraction tool. Actions: extract_structured, summarize, status.',
86
+ parameters: {
87
+ type: 'object',
88
+ properties: {
89
+ action: { type: 'string', enum: ['extract_structured', 'summarize', 'status'] },
90
+ text: { type: 'string' },
91
+ content: { type: 'string' },
92
+ filePath: { type: 'string' },
93
+ schema: {},
94
+ instruction: { type: 'string' },
95
+ prompt: { type: 'string' },
96
+ maxChars: { type: 'number' },
97
+ preferOcr: { type: 'boolean' },
98
+ includeRaw: { type: 'boolean' },
99
+ },
100
+ required: ['action'],
101
+ },
102
+ execute: async (args, context) => {
103
+ const syntheticBuildContext = {
104
+ cwd: context.session.cwd || process.cwd(),
105
+ ctx: { sessionId: context.session.id, agentId: context.session.agentId || null },
106
+ hasPlugin: () => true,
107
+ hasTool: () => true,
108
+ cleanupFns: [],
109
+ commandTimeoutMs: 0,
110
+ claudeTimeoutMs: 0,
111
+ cliProcessTimeoutMs: 0,
112
+ persistDelegateResumeId: () => undefined,
113
+ readStoredDelegateResumeId: () => null,
114
+ resolveCurrentSession: () => context.session,
115
+ activePlugins: context.session.plugins || [],
116
+ } as ToolBuildContext
117
+ return executeExtractAction(args, syntheticBuildContext)
118
+ },
119
+ },
120
+ ],
121
+ }
122
+
123
+ getPluginManager().registerBuiltin('extract', ExtractPlugin)
124
+
125
+ export function buildExtractTools(bctx: ToolBuildContext): StructuredToolInterface[] {
126
+ if (!bctx.hasPlugin('extract')) return []
127
+ return [
128
+ tool(
129
+ async (args) => executeExtractAction(args, bctx),
130
+ {
131
+ name: 'extract',
132
+ description: ExtractPlugin.tools![0].description,
133
+ schema: z.object({}).passthrough(),
134
+ },
135
+ ),
136
+ ]
137
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'fs'
4
+ import os from 'os'
5
+ import path from 'path'
6
+ import { executeFileAction, normalizeFileArgs } from './file'
7
+
8
+ describe('normalizeFileArgs', () => {
9
+ it('infers write from top-level filename and text', () => {
10
+ const out = normalizeFileArgs({ filename: 'note.txt', text: 'hello' })
11
+ assert.equal(out.action, 'write')
12
+ assert.equal(out.filePath, 'note.txt')
13
+ assert.equal(out.content, 'hello')
14
+ })
15
+
16
+ it('accepts top-level name and body aliases', () => {
17
+ const out = normalizeFileArgs({ name: 'note.txt', body: 'hello' })
18
+ assert.equal(out.action, 'write')
19
+ assert.equal(out.filePath, 'note.txt')
20
+ assert.equal(out.content, 'hello')
21
+ })
22
+
23
+ it('infers read from filename when action is omitted', () => {
24
+ const out = normalizeFileArgs({ filename: 'note.txt' })
25
+ assert.equal(out.action, 'read')
26
+ assert.equal(out.filePath, 'note.txt')
27
+ })
28
+
29
+ it('infers read from lowercase filepath alias when action is omitted', () => {
30
+ const out = normalizeFileArgs({ filepath: 'note.txt' })
31
+ assert.equal(out.action, 'read')
32
+ assert.equal(out.filePath, 'note.txt')
33
+ })
34
+
35
+ it('infers list from directory aliases', () => {
36
+ const out = normalizeFileArgs({ directory: 'docs' })
37
+ assert.equal(out.action, 'list')
38
+ assert.equal(out.dirPath, 'docs')
39
+ })
40
+
41
+ it('infers write from bulk file entries with text content', () => {
42
+ const out = normalizeFileArgs({
43
+ files: [
44
+ { filename: 'a.txt', text: 'alpha' },
45
+ ],
46
+ })
47
+ assert.equal(out.action, 'write')
48
+ assert.deepEqual(out.files, [{ filename: 'a.txt', text: 'alpha' }])
49
+ })
50
+
51
+ it('normalizes legacy write wrapper payloads', () => {
52
+ const out = normalizeFileArgs({
53
+ input: JSON.stringify({
54
+ write: {
55
+ filename: 'legacy.txt',
56
+ content: 'legacy body',
57
+ },
58
+ }),
59
+ })
60
+
61
+ assert.equal(out.action, 'write')
62
+ assert.equal(out.filePath, 'legacy.txt')
63
+ assert.equal(out.content, 'legacy body')
64
+ })
65
+
66
+ it('preserves nested write.files arrays from natural model payloads', () => {
67
+ const out = normalizeFileArgs({
68
+ input: JSON.stringify({
69
+ write: {
70
+ files: [
71
+ { name: 'report.md', content: '# report' },
72
+ ],
73
+ },
74
+ }),
75
+ })
76
+
77
+ assert.equal(out.action, 'write')
78
+ assert.deepEqual(out.files, [{ name: 'report.md', content: '# report' }])
79
+ })
80
+
81
+ it('treats trailing-slash write targets as directory creation', async () => {
82
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'file-write-dir-'))
83
+ const out = await executeFileAction({
84
+ action: 'write',
85
+ path: 'weather_update/',
86
+ content: 'placeholder',
87
+ }, { cwd })
88
+
89
+ assert.equal(out, 'Created directory weather_update/')
90
+ assert.equal(fs.statSync(path.join(cwd, 'weather_update')).isDirectory(), true)
91
+ fs.rmSync(cwd, { recursive: true, force: true })
92
+ })
93
+ })
@@ -1,6 +1,9 @@
1
1
  import { describe, it } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import { normalizeSendFilePaths } from './file'
3
+ import fs from 'fs'
4
+ import os from 'os'
5
+ import path from 'path'
6
+ import { findRecentSendFileFallbackPaths, normalizeSendFilePaths, resolveSendFileSourcePath } from './file'
4
7
 
5
8
  describe('normalizeSendFilePaths', () => {
6
9
  it('reads top-level filePath', () => {
@@ -8,6 +11,11 @@ describe('normalizeSendFilePaths', () => {
8
11
  assert.deepEqual(out, ['foo.png'])
9
12
  })
10
13
 
14
+ it('reads top-level lowercase filepath alias', () => {
15
+ const out = normalizeSendFilePaths({ filepath: 'foo.png' })
16
+ assert.deepEqual(out, ['foo.png'])
17
+ })
18
+
11
19
  it('reads nested input.files string array payload', () => {
12
20
  const out = normalizeSendFilePaths({
13
21
  input: {
@@ -26,6 +34,16 @@ describe('normalizeSendFilePaths', () => {
26
34
  assert.deepEqual(out, ['a.png'])
27
35
  })
28
36
 
37
+ it('accepts filePaths arrays from natural model tool calls', () => {
38
+ const out = normalizeSendFilePaths({
39
+ filePaths: ['a.png', 'b.png'],
40
+ input: JSON.stringify({
41
+ filePaths: ['b.png', 'c.png'],
42
+ }),
43
+ })
44
+ assert.deepEqual(out, ['a.png', 'b.png', 'c.png'])
45
+ })
46
+
29
47
  it('reads files object entries with path/filePath and dedupes', () => {
30
48
  const out = normalizeSendFilePaths({
31
49
  files: [
@@ -36,4 +54,69 @@ describe('normalizeSendFilePaths', () => {
36
54
  })
37
55
  assert.deepEqual(out, ['a.png', 'b.png'])
38
56
  })
57
+
58
+ it('accepts filename/name aliases commonly produced by model tool calls', () => {
59
+ const out = normalizeSendFilePaths({
60
+ filename: 'brief.md',
61
+ input: {
62
+ files: [{ name: 'fallback.md' }],
63
+ },
64
+ })
65
+ assert.deepEqual(out, ['brief.md', 'fallback.md'])
66
+ })
67
+
68
+ it('accepts fileId aliases commonly produced by model tool calls', () => {
69
+ const out = normalizeSendFilePaths({
70
+ input: {
71
+ fileId: 'brief.md',
72
+ },
73
+ })
74
+ assert.deepEqual(out, ['brief.md'])
75
+ })
76
+
77
+ it('extracts upload URLs from screenshot markdown tool output', () => {
78
+ const out = normalizeSendFilePaths({
79
+ filePath: '- [Screenshot of viewport](../../../.swarmclaw/browser-profiles/session/mcp-output/page.png)\n![Screenshot](/api/uploads/screenshot-123.png)',
80
+ })
81
+ assert.deepEqual(out, ['/api/uploads/screenshot-123.png', '../../../.swarmclaw/browser-profiles/session/mcp-output/page.png'])
82
+ })
83
+
84
+ it('falls back to a single recent file in the workspace when the payload is empty', () => {
85
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'send-file-'))
86
+ const recent = path.join(dir, 'brief.md')
87
+ const stale = path.join(dir, 'notes.txt')
88
+ fs.writeFileSync(recent, '# brief')
89
+ fs.writeFileSync(stale, 'old')
90
+ const oldTime = new Date(Date.now() - 20 * 60 * 1000)
91
+ fs.utimesSync(stale, oldTime, oldTime)
92
+
93
+ const out = findRecentSendFileFallbackPaths(dir)
94
+ assert.deepEqual(out, ['brief.md'])
95
+ })
96
+
97
+ it('resolves sandbox upload URLs when sending files', async () => {
98
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'send-file-upload-'))
99
+ const resolved = resolveSendFileSourcePath(cwd, 'sandbox:/api/uploads/artifact.md')
100
+ assert.equal(path.basename(resolved), 'artifact.md')
101
+ assert.match(resolved, /uploads[\/\\]artifact\.md$/)
102
+ fs.rmSync(cwd, { recursive: true, force: true })
103
+ })
104
+
105
+ it('resolves /workspace aliases against the current session workspace first', () => {
106
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'send-file-workspace-alias-'))
107
+ const artifact = path.join(cwd, 'spec.md')
108
+ fs.writeFileSync(artifact, '# spec')
109
+
110
+ const resolved = resolveSendFileSourcePath(cwd, '/workspace/spec.md')
111
+
112
+ assert.equal(resolved, artifact)
113
+ fs.rmSync(cwd, { recursive: true, force: true })
114
+ })
115
+
116
+ it('resolves browser profile screenshot paths back into the agent home directory', () => {
117
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'send-file-browser-profile-'))
118
+ const resolved = resolveSendFileSourcePath(cwd, '../../../.swarmclaw/browser-profiles/example/mcp-output/page.png')
119
+ assert.match(resolved, new RegExp(`\\.swarmclaw[\\\\/]browser-profiles[\\\\/]example[\\\\/]mcp-output[\\\\/]page\\.png$`))
120
+ fs.rmSync(cwd, { recursive: true, force: true })
121
+ })
39
122
  })