@swarmclawai/swarmclaw 0.6.7 → 0.7.0

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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -4,453 +4,285 @@ import fs from 'fs'
4
4
  import path from 'path'
5
5
  import { UPLOAD_DIR } from '../storage'
6
6
  import type { ToolBuildContext } from './context'
7
- import { safePath, truncate, listDirRecursive, MAX_OUTPUT, MAX_FILE } from './context'
8
-
9
- const SEND_FILE_DEDUPE_TTL_MS = 30_000
10
- const recentSendFileResults = new Map<string, { at: number; output: string; uploadPath: string }>()
11
-
12
- function pruneRecentSendFileCache(now: number): void {
13
- for (const [key, entry] of recentSendFileResults.entries()) {
14
- if (now - entry.at > SEND_FILE_DEDUPE_TTL_MS || !fs.existsSync(entry.uploadPath)) {
15
- recentSendFileResults.delete(key)
16
- }
7
+ import { safePath, truncate, listDirRecursive, MAX_FILE } from './context'
8
+ import type { Plugin, PluginHooks } from '@/types'
9
+ import { getPluginManager } from '../plugins'
10
+ import { normalizeToolInputArgs } from './normalize-tool-args'
11
+
12
+ function resolveFileToolPath(cwd: string, target: string): string {
13
+ try {
14
+ return safePath(cwd, target)
15
+ } catch (err: unknown) {
16
+ if (!path.isAbsolute(target)) throw err
17
+ return safePath(process.cwd(), target)
17
18
  }
18
19
  }
19
20
 
20
- export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
21
- const tools: StructuredToolInterface[] = []
22
-
23
- const filesEnabled = bctx.hasTool('files')
24
- const canReadFiles = filesEnabled || bctx.hasTool('read_file')
25
- const canWriteFiles = filesEnabled || bctx.hasTool('write_file')
26
- const canListFiles = filesEnabled || bctx.hasTool('list_files')
27
- const canSendFiles = filesEnabled || bctx.hasTool('send_file')
28
- const canCopyFiles = filesEnabled || bctx.hasTool('copy_file')
29
- const canMoveFiles = filesEnabled || bctx.hasTool('move_file')
30
- const canDeleteFiles = bctx.hasTool('delete_file')
31
-
32
- if (canReadFiles) {
33
- tools.push(
34
- tool(
35
- async ({ filePath }) => {
36
- try {
37
- const resolved = safePath(bctx.cwd, filePath)
38
- const content = fs.readFileSync(resolved, 'utf-8')
39
- return truncate(content, MAX_FILE)
40
- } catch (err: unknown) {
41
- return `Error reading file: ${err instanceof Error ? err.message : String(err)}`
42
- }
43
- },
44
- {
45
- name: 'read_file',
46
- description: 'Read a file from the session working directory.',
47
- schema: z.object({
48
- filePath: z.string().describe('Relative path to the file'),
49
- }),
50
- },
51
- ),
52
- )
21
+ /**
22
+ * Unified File Execution Logic
23
+ */
24
+ async function executeFileAction(args: Record<string, unknown>, bctx: { cwd: string }) {
25
+ const normalized = normalizeToolInputArgs(args)
26
+ // Normalize filePath/content for single-file mode
27
+ const files = normalized.files as Array<Record<string, unknown>> | undefined
28
+ let action = normalized.action as string | undefined
29
+ 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
53
34
  }
54
35
 
55
- if (canWriteFiles) {
56
- tools.push(
57
- tool(
58
- async ({ filePath, content, encoding }) => {
59
- try {
60
- const resolved = safePath(bctx.cwd, filePath)
61
- fs.mkdirSync(path.dirname(resolved), { recursive: true })
62
- if (encoding === 'base64') {
63
- const buf = Buffer.from(content, 'base64')
64
- fs.writeFileSync(resolved, buf)
65
- return `File written: ${filePath} (${buf.length} bytes, binary)`
66
- }
67
- fs.writeFileSync(resolved, content, 'utf-8')
68
- return `File written: ${filePath} (${content.length} bytes)`
69
- } catch (err: unknown) {
70
- return `Error writing file: ${err instanceof Error ? err.message : String(err)}`
36
+ const filePath = (normalized.filePath || normalized.path) as string | undefined
37
+ 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
41
+ const overwrite = !!normalized.overwrite
42
+ const recursive = !!normalized.recursive
43
+ const force = !!normalized.force
44
+
45
+ try {
46
+ switch (action) {
47
+ case 'read': {
48
+ const target = filePath || (files?.[0]?.path as string | undefined)
49
+ if (!target) return 'Error: no filePath or path provided.'
50
+ const resolved = resolveFileToolPath(bctx.cwd, target)
51
+ return truncate(fs.readFileSync(resolved, 'utf-8'), MAX_FILE)
52
+ }
53
+
54
+ case 'write': {
55
+ // Handle bulk files if provided
56
+ const filesToWrite: Array<Record<string, unknown>> = Array.isArray(files) ? files : [{ path: filePath, content }]
57
+ const results: string[] = []
58
+
59
+ for (const file of filesToWrite) {
60
+ const targetPath = (file.path || file.filePath) as string | undefined
61
+ if (!targetPath) continue
62
+ const fileContent = (file.content ?? '') as string
63
+
64
+ const resolved = resolveFileToolPath(bctx.cwd, targetPath)
65
+ fs.mkdirSync(path.dirname(resolved), { recursive: true })
66
+
67
+ if (encoding === 'base64' && typeof fileContent === 'string') {
68
+ const buf = Buffer.from(fileContent, 'base64')
69
+ fs.writeFileSync(resolved, buf)
70
+ results.push(`Written ${targetPath} (${buf.length} bytes, binary)`)
71
+ } else {
72
+ fs.writeFileSync(resolved, fileContent, 'utf-8')
73
+ results.push(`Written ${targetPath} (${fileContent.length} bytes)`)
71
74
  }
72
- },
73
- {
74
- name: 'write_file',
75
- description: 'Write content to a file in the session working directory. Creates directories if needed. For PDFs and styled reports, use the create_document tool instead. For other binary files (Excel, images, zip, etc.), set encoding to "base64" and pass base64-encoded content.',
76
- schema: z.object({
77
- filePath: z.string().describe('Relative path to the file'),
78
- content: z.string().describe('The content to write. For binary files, this must be a base64-encoded string.'),
79
- encoding: z.enum(['utf-8', 'base64']).optional().describe('Encoding of the content. Use "base64" for binary files like PDF, Excel, images, zip archives. Defaults to "utf-8" for plain text.'),
80
- }),
81
- },
82
- ),
83
- )
75
+ }
76
+ return results.join('\n') || 'Error: no files to write.'
77
+ }
78
+
79
+ case 'list': {
80
+ const resolved = resolveFileToolPath(bctx.cwd, dirPath || filePath || '.')
81
+ const tree = listDirRecursive(resolved, 0, 3)
82
+ return tree.length ? tree.join('\n') : '(empty directory)'
83
+ }
84
+
85
+ case 'copy': {
86
+ if (!sourcePath) return 'Error: sourcePath is required for copy action.'
87
+ if (!destinationPath) return 'Error: destinationPath is required for copy action.'
88
+ const src = resolveFileToolPath(bctx.cwd, sourcePath)
89
+ const dest = resolveFileToolPath(bctx.cwd, destinationPath)
90
+ if (fs.existsSync(dest) && !overwrite) return `Error: destination exists`
91
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
92
+ fs.copyFileSync(src, dest)
93
+ return `Copied ${sourcePath} to ${destinationPath}`
94
+ }
95
+
96
+ case 'move': {
97
+ if (!sourcePath) return 'Error: sourcePath is required for move action.'
98
+ if (!destinationPath) return 'Error: destinationPath is required for move action.'
99
+ const src = resolveFileToolPath(bctx.cwd, sourcePath)
100
+ const dest = resolveFileToolPath(bctx.cwd, destinationPath)
101
+ if (fs.existsSync(dest) && !overwrite) return `Error: destination exists`
102
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
103
+ if (fs.existsSync(dest) && overwrite) fs.unlinkSync(dest)
104
+ fs.renameSync(src, dest)
105
+ return `Moved ${sourcePath} to ${destinationPath}`
106
+ }
107
+
108
+ case 'delete': {
109
+ const target = filePath || (files?.[0]?.path as string | undefined)
110
+ if (!target) return 'Error: no filePath or path provided.'
111
+ const resolved = resolveFileToolPath(bctx.cwd, target)
112
+ if (resolved === path.resolve(bctx.cwd) || resolved === path.resolve(process.cwd())) return 'Error: cannot delete root'
113
+ fs.rmSync(resolved, { recursive: !!recursive, force: !!force })
114
+ return `Deleted ${target}`
115
+ }
116
+
117
+ default:
118
+ return `Error: Unknown action "${action}"`
119
+ }
120
+ } catch (err: unknown) {
121
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
84
122
  }
123
+ }
85
124
 
86
- if (canListFiles) {
87
- tools.push(
88
- tool(
89
- async ({ dirPath }) => {
90
- try {
91
- const resolved = safePath(bctx.cwd, dirPath || '.')
92
- const tree = listDirRecursive(resolved, 0, 3)
93
- return tree.length ? tree.join('\n') : '(empty directory)'
94
- } catch (err: unknown) {
95
- return `Error listing files: ${err instanceof Error ? err.message : String(err)}`
96
- }
97
- },
98
- {
99
- name: 'list_files',
100
- description: 'List files in the session working directory recursively (max depth 3).',
101
- schema: z.object({
102
- dirPath: z.string().optional().describe('Relative path to list (defaults to working directory)'),
103
- }),
104
- },
105
- ),
106
- )
125
+ function collectSendFilePaths(payload: unknown, into: string[]): void {
126
+ if (!payload) return
127
+ if (typeof payload === 'string') {
128
+ const trimmed = payload.trim()
129
+ if (trimmed) into.push(trimmed)
130
+ return
107
131
  }
108
-
109
- if (canCopyFiles) {
110
- tools.push(
111
- tool(
112
- async ({ sourcePath, destinationPath, overwrite }) => {
113
- try {
114
- const source = safePath(bctx.cwd, sourcePath)
115
- const destination = safePath(bctx.cwd, destinationPath)
116
- if (!fs.existsSync(source)) return `Error: source file not found: ${sourcePath}`
117
- const sourceStat = fs.statSync(source)
118
- if (sourceStat.isDirectory()) return `Error: source must be a file (directories are not supported by copy_file).`
119
- if (fs.existsSync(destination) && !overwrite) return `Error: destination already exists: ${destinationPath} (set overwrite=true to replace).`
120
- fs.mkdirSync(path.dirname(destination), { recursive: true })
121
- fs.copyFileSync(source, destination)
122
- return `File copied: ${sourcePath} -> ${destinationPath}`
123
- } catch (err: unknown) {
124
- return `Error copying file: ${err instanceof Error ? err.message : String(err)}`
125
- }
126
- },
127
- {
128
- name: 'copy_file',
129
- description: 'Copy a file to a new location in the working directory.',
130
- schema: z.object({
131
- sourcePath: z.string().describe('Source file path (relative to working directory)'),
132
- destinationPath: z.string().describe('Destination file path (relative to working directory)'),
133
- overwrite: z.boolean().optional().describe('Overwrite destination if it exists (default false)'),
134
- }),
135
- },
136
- ),
137
- )
132
+ if (Array.isArray(payload)) {
133
+ for (const item of payload) collectSendFilePaths(item, into)
134
+ return
138
135
  }
136
+ if (typeof payload !== 'object') return
137
+ const record = payload as Record<string, unknown>
138
+ if (typeof record.filePath === 'string') into.push(record.filePath)
139
+ if (typeof record.path === 'string') into.push(record.path)
140
+ if (record.files !== undefined) collectSendFilePaths(record.files, into)
141
+ }
139
142
 
140
- if (canMoveFiles) {
141
- tools.push(
142
- tool(
143
- async ({ sourcePath, destinationPath, overwrite }) => {
144
- try {
145
- const source = safePath(bctx.cwd, sourcePath)
146
- const destination = safePath(bctx.cwd, destinationPath)
147
- if (!fs.existsSync(source)) return `Error: source file not found: ${sourcePath}`
148
- const sourceStat = fs.statSync(source)
149
- if (sourceStat.isDirectory()) return `Error: source must be a file (directories are not supported by move_file).`
150
- if (fs.existsSync(destination) && !overwrite) return `Error: destination already exists: ${destinationPath} (set overwrite=true to replace).`
151
- fs.mkdirSync(path.dirname(destination), { recursive: true })
152
- if (fs.existsSync(destination) && overwrite) fs.unlinkSync(destination)
153
- fs.renameSync(source, destination)
154
- return `File moved: ${sourcePath} -> ${destinationPath}`
155
- } catch (err: unknown) {
156
- return `Error moving file: ${err instanceof Error ? err.message : String(err)}`
157
- }
158
- },
159
- {
160
- name: 'move_file',
161
- description: 'Move (rename) a file to a new location in the working directory.',
162
- schema: z.object({
163
- sourcePath: z.string().describe('Source file path (relative to working directory)'),
164
- destinationPath: z.string().describe('Destination file path (relative to working directory)'),
165
- overwrite: z.boolean().optional().describe('Overwrite destination if it exists (default false)'),
166
- }),
167
- },
168
- ),
169
- )
143
+ export function normalizeSendFilePaths(args: Record<string, unknown>): string[] {
144
+ const candidates: string[] = []
145
+ collectSendFilePaths(args.filePath, candidates)
146
+ collectSendFilePaths(args.path, candidates)
147
+ collectSendFilePaths(args.files, candidates)
148
+
149
+ const nestedInput = args.input
150
+ if (typeof nestedInput === 'string') {
151
+ try {
152
+ const parsed = JSON.parse(nestedInput)
153
+ collectSendFilePaths(parsed, candidates)
154
+ } catch {
155
+ // ignore non-JSON input strings
156
+ }
157
+ } else if (nestedInput && typeof nestedInput === 'object') {
158
+ collectSendFilePaths(nestedInput, candidates)
170
159
  }
171
160
 
172
- if (canDeleteFiles) {
173
- tools.push(
174
- tool(
175
- async ({ filePath, recursive, force }) => {
176
- try {
177
- const resolved = safePath(bctx.cwd, filePath)
178
- const root = path.resolve(bctx.cwd)
179
- if (resolved === root) return 'Error: refusing to delete the session working directory root.'
180
- if (!fs.existsSync(resolved)) {
181
- return force ? `Path already absent: ${filePath}` : `Error: path not found: ${filePath}`
182
- }
183
- const stat = fs.statSync(resolved)
184
- if (stat.isDirectory() && !recursive) {
185
- return 'Error: target is a directory. Set recursive=true to delete directories.'
186
- }
187
- fs.rmSync(resolved, { recursive: !!recursive, force: !!force })
188
- return `Deleted: ${filePath}`
189
- } catch (err: unknown) {
190
- return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`
191
- }
192
- },
193
- {
194
- name: 'delete_file',
195
- description: 'Delete a file or directory from the working directory. Disabled by default and must be explicitly enabled.',
196
- schema: z.object({
197
- filePath: z.string().describe('Path to delete (relative to working directory)'),
198
- recursive: z.boolean().optional().describe('Required for deleting directories'),
199
- force: z.boolean().optional().describe('Ignore missing paths and force deletion where possible'),
200
- }),
201
- },
202
- ),
203
- )
161
+ const deduped = new Set<string>()
162
+ for (const candidate of candidates) {
163
+ const trimmed = candidate.trim()
164
+ if (trimmed) deduped.add(trimmed)
204
165
  }
166
+ return [...deduped]
167
+ }
205
168
 
206
- if (canSendFiles) {
207
- tools.push(
208
- tool(
209
- async ({ filePath: rawPath }) => {
210
- try {
211
- const now = Date.now()
212
- pruneRecentSendFileCache(now)
213
- // Resolve relative to cwd, but also allow absolute paths
214
- const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(bctx.cwd, rawPath)
215
- if (!fs.existsSync(resolved)) return `Error: file not found: ${rawPath}`
216
- const stat = fs.statSync(resolved)
217
- if (stat.isDirectory()) return `Error: cannot send a directory. Send individual files instead.`
218
- if (stat.size > 100 * 1024 * 1024) return `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 100MB.`
219
-
220
- const sessionId = bctx.ctx?.sessionId || 'no-session'
221
- const dedupeKey = `${sessionId}|${resolved}`
222
- const cached = recentSendFileResults.get(dedupeKey)
223
- if (cached && now - cached.at <= SEND_FILE_DEDUPE_TTL_MS && fs.existsSync(cached.uploadPath)) {
224
- return cached.output
225
- }
226
-
227
- const ext = path.extname(resolved).slice(1).toLowerCase()
228
- const basename = path.basename(resolved)
229
- const filename = `${Date.now()}-${basename}`
230
- const dest = path.join(UPLOAD_DIR, filename)
231
- fs.copyFileSync(resolved, dest)
169
+ async function executeSendFile(args: Record<string, unknown>, bctx: { cwd: string }) {
170
+ try {
171
+ const paths = normalizeSendFilePaths(args)
172
+ if (paths.length === 0) {
173
+ return 'Error: filePath/path is required (or provide files[] / input.files[]).'
174
+ }
232
175
 
233
- const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico']
234
- const VIDEO_EXTS = ['mp4', 'webm', 'mov', 'avi', 'mkv']
235
- const AUDIO_EXTS = ['mp3', 'ogg', 'wav', 'aac', 'm4a', 'opus']
176
+ const links: string[] = []
177
+ const errors: string[] = []
178
+ for (const rawPath of paths) {
179
+ const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(bctx.cwd, rawPath)
180
+ if (!fs.existsSync(resolved)) {
181
+ errors.push(`file not found: ${rawPath}`)
182
+ continue
183
+ }
184
+ const basename = path.basename(resolved)
185
+ const filename = `${Date.now()}-${basename}`
186
+ const dest = path.join(UPLOAD_DIR, filename)
187
+ fs.copyFileSync(resolved, dest)
188
+ links.push(`[Download ${basename}](/api/uploads/${filename})`)
189
+ }
236
190
 
237
- const output = (IMAGE_EXTS.includes(ext) || VIDEO_EXTS.includes(ext) || AUDIO_EXTS.includes(ext))
238
- ? `![${basename}](/api/uploads/${filename})`
239
- : `[Download ${basename}](/api/uploads/${filename})`
240
- recentSendFileResults.set(dedupeKey, { at: now, output, uploadPath: dest })
241
- return output
242
- } catch (err: unknown) {
243
- return `Error sending file: ${err instanceof Error ? err.message : String(err)}`
244
- }
245
- },
246
- {
247
- name: 'send_file',
248
- description: 'Send a file to the user so they can view or download it in the chat. Works with images, videos, PDFs, documents, and any other file type. The file will appear inline for images/videos, or as a download link for other types.',
249
- schema: z.object({
250
- filePath: z.string().describe('Path to the file (relative to working directory, or absolute)'),
251
- }),
252
- },
253
- ),
254
- )
191
+ if (links.length === 0) return `Error: ${errors[0] || 'file not found'}`
192
+ if (errors.length === 0) return links.join('\n')
193
+ return `${links.join('\n')}\n\nSkipped: ${errors.join('; ')}`
194
+ } catch (err: unknown) {
195
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
255
196
  }
197
+ }
256
198
 
257
- if (canSendFiles || canWriteFiles) {
258
- // create_document: markdown pdf / html / png / jpg
259
- tools.push(
260
- tool(
261
- async ({ content, title, filename, format }) => {
262
- try {
263
- const fmt = format || 'pdf'
264
- const { marked } = await import('marked')
265
- const html = await marked.parse(content)
266
- const safeTitle = (title || 'Document').replace(/</g, '&lt;')
267
- const fullHtml = `<!DOCTYPE html>
268
- <html><head><meta charset="utf-8"><title>${safeTitle}</title>
269
- <style>
270
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:800px;margin:40px auto;padding:0 20px;color:#1a1a1a;line-height:1.6}
271
- h1{font-size:28px;border-bottom:2px solid #e5e7eb;padding-bottom:8px}
272
- h2{font-size:22px;margin-top:32px}
273
- h3{font-size:18px;margin-top:24px}
274
- pre{background:#f3f4f6;padding:16px;border-radius:8px;overflow-x:auto;font-size:13px}
275
- code{background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:13px}
276
- pre code{background:none;padding:0}
277
- table{border-collapse:collapse;width:100%}
278
- th,td{border:1px solid #d1d5db;padding:8px 12px;text-align:left}
279
- th{background:#f9fafb;font-weight:600}
280
- blockquote{border-left:4px solid #d1d5db;margin:16px 0;padding:8px 16px;color:#4b5563}
281
- img{max-width:100%}
282
- </style></head><body>${html}</body></html>`
283
-
284
- const defaultBase = (title || 'document').replace(/[^a-zA-Z0-9_-]/g, '_')
285
-
286
- if (fmt === 'html') {
287
- const outName = filename || `${defaultBase}.html`
288
- const resolved = safePath(bctx.cwd, outName)
289
- fs.mkdirSync(path.dirname(resolved), { recursive: true })
290
- fs.writeFileSync(resolved, fullHtml, 'utf-8')
291
- return `HTML document created: ${outName} (${fullHtml.length} bytes)`
292
- }
293
-
294
- const { chromium } = await import('playwright')
295
- const browser = await chromium.launch({ headless: true })
296
- try {
297
- const page = await browser.newPage()
298
- await page.setContent(fullHtml, { waitUntil: 'networkidle' })
299
-
300
- if (fmt === 'pdf') {
301
- const outName = filename || `${defaultBase}.pdf`
302
- const resolved = safePath(bctx.cwd, outName)
303
- fs.mkdirSync(path.dirname(resolved), { recursive: true })
304
- await page.pdf({ path: resolved, format: 'A4', margin: { top: '40px', bottom: '40px', left: '40px', right: '40px' }, printBackground: true })
305
- return `PDF created: ${outName}`
306
- }
307
-
308
- // png or jpg screenshot
309
- const ext = fmt === 'jpg' ? 'jpeg' : 'png'
310
- const outName = filename || `${defaultBase}.${fmt}`
311
- const resolved = safePath(bctx.cwd, outName)
312
- fs.mkdirSync(path.dirname(resolved), { recursive: true })
313
- await page.screenshot({ path: resolved, type: ext, fullPage: true })
314
- const size = fs.statSync(resolved).size
315
- return `Image created: ${outName} (${(size / 1024).toFixed(1)} KB)`
316
- } finally {
317
- await browser.close()
318
- }
319
- } catch (err: unknown) {
320
- return `Error creating document: ${err instanceof Error ? err.message : String(err)}`
321
- }
199
+ /**
200
+ * Register as a Built-in Plugin
201
+ */
202
+ const FilePlugin: Plugin = {
203
+ name: 'Core Files',
204
+ description: 'Complete file management: read, write, list, move, copy, delete, and send.',
205
+ hooks: {} as PluginHooks,
206
+ tools: [
207
+ {
208
+ name: 'files',
209
+ description: 'Unified file management tool. Actions: read, write, list, copy, move, delete. Supports bulk writes via "files" array.',
210
+ parameters: {
211
+ type: 'object',
212
+ properties: {
213
+ action: { type: 'string', enum: ['read', 'write', 'list', 'copy', 'move', 'delete'] },
214
+ filePath: { type: 'string' },
215
+ path: { type: 'string', description: 'Alias for filePath' },
216
+ content: { type: 'string' },
217
+ files: {
218
+ type: 'array',
219
+ items: {
220
+ type: 'object',
221
+ properties: { path: { type: 'string' }, content: { type: 'string' } }
222
+ }
223
+ },
224
+ encoding: { type: 'string', enum: ['utf-8', 'base64'] },
225
+ dirPath: { type: 'string' },
226
+ sourcePath: { type: 'string' },
227
+ destinationPath: { type: 'string' },
228
+ overwrite: { type: 'boolean' },
229
+ recursive: { type: 'boolean' },
230
+ force: { type: 'boolean' },
322
231
  },
323
- {
324
- name: 'create_document',
325
- description: 'Create a document from markdown content. Renders markdown with professional styling and outputs as PDF, HTML, or image. Use this instead of write_file for PDFs, reports, styled pages, or document screenshots. After creating, use send_file to deliver it to the user.',
326
- schema: z.object({
327
- content: z.string().describe('Markdown content for the document'),
328
- title: z.string().optional().describe('Document title (shown in header and used for default filename)'),
329
- filename: z.string().optional().describe('Output filename (defaults to title-based name with appropriate extension)'),
330
- format: z.enum(['pdf', 'html', 'png', 'jpg']).optional().describe('Output format. "pdf" (default) for print-ready documents, "html" for web pages, "png"/"jpg" for images.'),
331
- }),
232
+ required: ['action']
233
+ },
234
+ execute: async (args, context) => executeFileAction(args, { cwd: context.session.cwd || process.cwd() })
235
+ },
236
+ {
237
+ name: 'send_file',
238
+ description: 'Send a file to the user in chat.',
239
+ parameters: {
240
+ type: 'object',
241
+ properties: {
242
+ filePath: { type: 'string' },
243
+ path: { type: 'string', description: 'Alias for filePath' },
244
+ files: {
245
+ type: 'array',
246
+ items: {
247
+ anyOf: [
248
+ { type: 'string' },
249
+ { type: 'object', properties: { filePath: { type: 'string' }, path: { type: 'string' } } },
250
+ ],
251
+ },
252
+ },
253
+ input: { type: 'object', additionalProperties: true },
332
254
  },
333
- ),
334
- )
335
-
336
- // create_spreadsheet: JSON data → xlsx or csv
337
- tools.push(
338
- tool(
339
- async ({ data, headers, sheetName, filename, format }) => {
340
- try {
341
- const fmt = format || 'xlsx'
342
- let rows: Record<string, unknown>[]
343
- try {
344
- rows = JSON.parse(data)
345
- if (!Array.isArray(rows)) return 'Error: data must be a JSON array of objects'
346
- } catch {
347
- return 'Error: data is not valid JSON. Pass a JSON array of objects, e.g. [{"name":"Alice","age":30}]'
348
- }
349
-
350
- if (!rows.length) return 'Error: data array is empty'
351
-
352
- // Resolve column headers: explicit headers, or keys from first row
353
- const cols = headers?.length
354
- ? headers
355
- : Object.keys(rows[0] && typeof rows[0] === 'object' ? rows[0] : {})
356
- if (!cols.length) return 'Error: could not determine column headers. Pass headers or use objects with keys.'
357
-
358
- const defaultBase = (sheetName || 'spreadsheet').replace(/[^a-zA-Z0-9_-]/g, '_')
359
-
360
- if (fmt === 'csv') {
361
- const escapeCsv = (val: unknown): string => {
362
- const s = val == null ? '' : String(val)
363
- return s.includes(',') || s.includes('"') || s.includes('\n')
364
- ? `"${s.replace(/"/g, '""')}"`
365
- : s
366
- }
367
- const lines = [cols.map(escapeCsv).join(',')]
368
- for (const row of rows) {
369
- const r = Array.isArray(row) ? row : cols.map((c) => (row as Record<string, unknown>)[c])
370
- lines.push(r.map(escapeCsv).join(','))
371
- }
372
- const outName = filename || `${defaultBase}.csv`
373
- const resolved = safePath(bctx.cwd, outName)
374
- fs.mkdirSync(path.dirname(resolved), { recursive: true })
375
- fs.writeFileSync(resolved, lines.join('\n'), 'utf-8')
376
- return `CSV created: ${outName} (${rows.length} rows, ${cols.length} columns)`
377
- }
378
-
379
- // xlsx via exceljs
380
- const ExcelJS = await import('exceljs')
381
- const workbook = new ExcelJS.default.Workbook()
382
- const sheet = workbook.addWorksheet(sheetName || 'Sheet1')
383
-
384
- sheet.columns = cols.map((c) => ({ header: c, key: c, width: Math.max(12, c.length + 4) }))
385
- // Style header row
386
- sheet.getRow(1).font = { bold: true }
387
- sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } }
388
-
389
- for (const row of rows) {
390
- if (Array.isArray(row)) {
391
- const obj: Record<string, unknown> = {}
392
- cols.forEach((c, i) => { obj[c] = row[i] })
393
- sheet.addRow(obj)
394
- } else {
395
- sheet.addRow(row)
396
- }
397
- }
255
+ required: []
256
+ },
257
+ execute: async (args, context) => executeSendFile(args, { cwd: context.session.cwd || process.cwd() })
258
+ }
259
+ ]
260
+ }
398
261
 
399
- const outName = filename || `${defaultBase}.xlsx`
400
- const resolved = safePath(bctx.cwd, outName)
401
- fs.mkdirSync(path.dirname(resolved), { recursive: true })
402
- await workbook.xlsx.writeFile(resolved)
403
- const size = fs.statSync(resolved).size
404
- return `Excel spreadsheet created: ${outName} (${rows.length} rows, ${cols.length} columns, ${(size / 1024).toFixed(1)} KB)`
405
- } catch (err: unknown) {
406
- return `Error creating spreadsheet: ${err instanceof Error ? err.message : String(err)}`
407
- }
408
- },
409
- {
410
- name: 'create_spreadsheet',
411
- description: 'Create an Excel (.xlsx) or CSV file from structured data. Pass data as a JSON array of objects. Use this for tables, reports, data exports, and any tabular data the user requests. After creating, use send_file to deliver it to the user.',
412
- schema: z.object({
413
- data: z.string().describe('JSON array of objects, e.g. [{"name":"Alice","score":95},{"name":"Bob","score":87}]'),
414
- headers: z.array(z.string()).optional().describe('Column headers in display order. If omitted, keys from the first object are used.'),
415
- sheetName: z.string().optional().describe('Worksheet name (default "Sheet1")'),
416
- filename: z.string().optional().describe('Output filename (defaults to sheetName-based name with extension)'),
417
- format: z.enum(['xlsx', 'csv']).optional().describe('Output format: "xlsx" (default) for Excel, "csv" for plain CSV.'),
418
- }),
419
- },
420
- ),
421
- )
422
- }
262
+ getPluginManager().registerBuiltin('files', FilePlugin)
423
263
 
424
- if (bctx.hasTool('edit_file')) {
425
- tools.push(
426
- tool(
427
- async ({ filePath, oldText, newText }) => {
428
- try {
429
- const resolved = safePath(bctx.cwd, filePath)
430
- if (!fs.existsSync(resolved)) return `Error: File not found: ${filePath}`
431
- const content = fs.readFileSync(resolved, 'utf-8')
432
- const count = content.split(oldText).length - 1
433
- if (count === 0) return `Error: oldText not found in ${filePath}`
434
- if (count > 1) return `Error: oldText found ${count} times in ${filePath}. Make it more specific.`
435
- const updated = content.replace(oldText, newText)
436
- fs.writeFileSync(resolved, updated, 'utf-8')
437
- return `Successfully edited ${filePath}`
438
- } catch (err: unknown) {
439
- return `Error editing file: ${err instanceof Error ? err.message : String(err)}`
440
- }
441
- },
442
- {
443
- name: 'edit_file',
444
- description: 'Search and replace text in a file. The oldText must match exactly once in the file.',
445
- schema: z.object({
446
- filePath: z.string().describe('Relative path to the file'),
447
- oldText: z.string().describe('Exact text to find (must be unique in the file)'),
448
- newText: z.string().describe('Text to replace it with'),
449
- }),
450
- },
451
- ),
264
+ /**
265
+ * Legacy Bridge
266
+ */
267
+ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
268
+ if (!bctx.hasTool('files')) return []
269
+
270
+ return [
271
+ tool(
272
+ async (args) => executeFileAction(args, { cwd: bctx.cwd }),
273
+ {
274
+ name: 'files',
275
+ description: FilePlugin.tools![0].description,
276
+ schema: z.object({}).passthrough()
277
+ }
278
+ ),
279
+ tool(
280
+ async (args) => executeSendFile(args, { cwd: bctx.cwd }),
281
+ {
282
+ name: 'send_file',
283
+ description: FilePlugin.tools![1].description,
284
+ schema: z.object({}).passthrough()
285
+ }
452
286
  )
453
- }
454
-
455
- return tools
287
+ ]
456
288
  }