@swarmclawai/swarmclaw 0.2.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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,673 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import crypto from 'crypto'
6
+ import { spawnSync } from 'child_process'
7
+ import * as cheerio from 'cheerio'
8
+ import {
9
+ loadAgents, saveAgents,
10
+ loadTasks, saveTasks,
11
+ loadSchedules, saveSchedules,
12
+ loadSkills, saveSkills,
13
+ loadConnectors, saveConnectors,
14
+ loadDocuments, saveDocuments,
15
+ loadWebhooks, saveWebhooks,
16
+ loadSecrets, saveSecrets,
17
+ loadSessions, saveSessions,
18
+ encryptKey,
19
+ decryptKey,
20
+ } from '../storage'
21
+ import { resolveScheduleName } from '@/lib/schedule-name'
22
+ import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
23
+ import type { ToolBuildContext } from './context'
24
+ import { safePath, findBinaryOnPath } from './context'
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Document helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const MAX_DOCUMENT_TEXT_CHARS = 500_000
31
+
32
+ function extractDocumentText(filePath: string): { text: string; method: string } {
33
+ const ext = path.extname(filePath).toLowerCase()
34
+
35
+ const readUtf8Text = (): string => {
36
+ const raw = fs.readFileSync(filePath, 'utf-8')
37
+ const cleaned = raw.replace(/\u0000/g, '')
38
+ return cleaned
39
+ }
40
+
41
+ if (ext === '.pdf') {
42
+ const pdftotextBinary = findBinaryOnPath('pdftotext')
43
+ if (!pdftotextBinary) throw new Error('pdftotext is not installed. Install poppler to index PDF files.')
44
+ const out = spawnSync(pdftotextBinary, ['-layout', '-nopgbrk', '-q', filePath, '-'], {
45
+ encoding: 'utf-8',
46
+ maxBuffer: 25 * 1024 * 1024,
47
+ timeout: 20_000,
48
+ })
49
+ if ((out.status ?? 1) !== 0) {
50
+ throw new Error(`pdftotext failed: ${(out.stderr || out.stdout || '').trim() || 'unknown error'}`)
51
+ }
52
+ return { text: out.stdout || '', method: 'pdftotext' }
53
+ }
54
+
55
+ if (['.txt', '.md', '.markdown', '.json', '.csv', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.yaml', '.yml'].includes(ext)) {
56
+ return { text: readUtf8Text(), method: 'utf8' }
57
+ }
58
+
59
+ if (ext === '.html' || ext === '.htm') {
60
+ const html = fs.readFileSync(filePath, 'utf-8')
61
+ const $ = cheerio.load(html)
62
+ const text = $('body').text() || $.text()
63
+ return { text, method: 'html-strip' }
64
+ }
65
+
66
+ if (['.doc', '.docx', '.rtf'].includes(ext)) {
67
+ const out = spawnSync('/usr/bin/textutil', ['-convert', 'txt', '-stdout', filePath], {
68
+ encoding: 'utf-8',
69
+ maxBuffer: 25 * 1024 * 1024,
70
+ timeout: 20_000,
71
+ })
72
+ if ((out.status ?? 1) === 0 && out.stdout?.trim()) {
73
+ return { text: out.stdout, method: 'textutil' }
74
+ }
75
+ }
76
+
77
+ const fallback = readUtf8Text()
78
+ if (fallback.trim()) return { text: fallback, method: 'utf8-fallback' }
79
+ throw new Error(`Unsupported document type: ${ext || '(no extension)'}`)
80
+ }
81
+
82
+ function trimDocumentContent(text: string): string {
83
+ const normalized = text.replace(/\r\n/g, '\n').replace(/\u0000/g, '').trim()
84
+ if (normalized.length <= MAX_DOCUMENT_TEXT_CHARS) return normalized
85
+ return normalized.slice(0, MAX_DOCUMENT_TEXT_CHARS)
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // RESOURCE_DEFAULTS
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
93
+ manage_agents: (p) => ({
94
+ name: p.name || 'Unnamed Agent',
95
+ description: p.description || '',
96
+ systemPrompt: p.systemPrompt || '',
97
+ soul: p.soul || '',
98
+ provider: p.provider || 'claude-cli',
99
+ model: p.model || '',
100
+ isOrchestrator: p.isOrchestrator || false,
101
+ tools: p.tools || [],
102
+ skills: p.skills || [],
103
+ skillIds: p.skillIds || [],
104
+ subAgentIds: p.subAgentIds || [],
105
+ ...p,
106
+ }),
107
+ manage_tasks: (p) => ({
108
+ title: p.title || 'Untitled Task',
109
+ description: p.description || '',
110
+ status: p.status || 'backlog',
111
+ agentId: p.agentId || null,
112
+ sessionId: p.sessionId || null,
113
+ result: null,
114
+ error: null,
115
+ queuedAt: null,
116
+ startedAt: null,
117
+ completedAt: null,
118
+ ...p,
119
+ }),
120
+ manage_schedules: (p) => {
121
+ const now = Date.now()
122
+ const base = {
123
+ name: resolveScheduleName({ name: p.name, taskPrompt: p.taskPrompt }),
124
+ agentId: p.agentId || null,
125
+ taskPrompt: p.taskPrompt || '',
126
+ scheduleType: p.scheduleType || 'interval',
127
+ status: p.status || 'active',
128
+ ...p,
129
+ }
130
+ if (!base.nextRunAt) {
131
+ if (base.scheduleType === 'once' && base.runAt) base.nextRunAt = base.runAt
132
+ else if (base.scheduleType === 'interval' && base.intervalMs) base.nextRunAt = now + base.intervalMs
133
+ }
134
+ return base
135
+ },
136
+ manage_skills: (p) => ({
137
+ name: p.name || 'Unnamed Skill',
138
+ description: p.description || '',
139
+ content: p.content || '',
140
+ filename: p.filename || '',
141
+ ...p,
142
+ }),
143
+ manage_connectors: (p) => ({
144
+ name: p.name || 'Unnamed Connector',
145
+ platform: p.platform || 'discord',
146
+ agentId: p.agentId || null,
147
+ enabled: p.enabled ?? false,
148
+ ...p,
149
+ }),
150
+ manage_webhooks: (p) => ({
151
+ name: p.name || 'Unnamed Webhook',
152
+ source: p.source || 'custom',
153
+ events: Array.isArray(p.events) ? p.events : [],
154
+ agentId: p.agentId || null,
155
+ secret: p.secret || '',
156
+ isEnabled: p.isEnabled ?? true,
157
+ ...p,
158
+ }),
159
+ manage_secrets: (p) => ({
160
+ name: p.name || 'Unnamed Secret',
161
+ service: p.service || 'custom',
162
+ scope: p.scope || 'global',
163
+ agentIds: Array.isArray(p.agentIds) ? p.agentIds : [],
164
+ ...p,
165
+ }),
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // PLATFORM_RESOURCES
170
+ // ---------------------------------------------------------------------------
171
+
172
+ const PLATFORM_RESOURCES: Record<string, {
173
+ toolId: string
174
+ label: string
175
+ load: () => Record<string, any>
176
+ save: (d: Record<string, any>) => void
177
+ readOnly?: boolean
178
+ }> = {
179
+ manage_agents: { toolId: 'manage_agents', label: 'agents', load: loadAgents, save: saveAgents },
180
+ manage_tasks: { toolId: 'manage_tasks', label: 'tasks', load: loadTasks, save: saveTasks },
181
+ manage_schedules: { toolId: 'manage_schedules', label: 'schedules', load: loadSchedules, save: saveSchedules },
182
+ manage_skills: { toolId: 'manage_skills', label: 'skills', load: loadSkills, save: saveSkills },
183
+ manage_connectors: { toolId: 'manage_connectors', label: 'connectors', load: loadConnectors, save: saveConnectors },
184
+ manage_webhooks: { toolId: 'manage_webhooks', label: 'webhooks', load: loadWebhooks, save: saveWebhooks },
185
+ manage_sessions: { toolId: 'manage_sessions', label: 'sessions', load: loadSessions, save: saveSessions, readOnly: true },
186
+ manage_secrets: { toolId: 'manage_secrets', label: 'secrets', load: loadSecrets, save: saveSecrets },
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // buildCrudTools
191
+ // ---------------------------------------------------------------------------
192
+
193
+ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[] {
194
+ const tools: StructuredToolInterface[] = []
195
+ const { cwd, ctx, hasTool } = bctx
196
+
197
+ // Build dynamic agent summary for tools that need agent awareness
198
+ const assignScope = ctx?.platformAssignScope || 'self'
199
+ let agentSummary = ''
200
+ if (hasTool('manage_tasks') || hasTool('manage_schedules')) {
201
+ if (assignScope === 'all') {
202
+ try {
203
+ const agents = loadAgents()
204
+ const agentList = Object.values(agents)
205
+ .map((a: any) => ` - "${a.id}": ${a.name}${a.description ? ` — ${a.description}` : ''}`)
206
+ .join('\n')
207
+ if (agentList) agentSummary = `\n\nAvailable agents:\n${agentList}`
208
+ } catch { /* ignore */ }
209
+ }
210
+ }
211
+
212
+ for (const [toolKey, res] of Object.entries(PLATFORM_RESOURCES)) {
213
+ if (!hasTool(toolKey)) continue
214
+
215
+ let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
216
+ if (toolKey === 'manage_tasks') {
217
+ if (assignScope === 'self') {
218
+ description += `\n\nSet "agentId" to assign a task to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign tasks to yourself. Valid statuses: backlog, queued, running, completed, failed.`
219
+ } else {
220
+ description += `\n\nSet "agentId" to assign a task to an agent (including yourself: "${ctx?.agentId || 'unknown'}"). Valid statuses: backlog, queued, running, completed, failed.` + agentSummary
221
+ }
222
+ } else if (toolKey === 'manage_agents') {
223
+ description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field.`
224
+ } else if (toolKey === 'manage_schedules') {
225
+ if (assignScope === 'self') {
226
+ description += `\n\nSet "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Set taskPrompt for what the agent should do. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
227
+ } else {
228
+ description += `\n\nSet "agentId" to assign a schedule to an agent (including yourself: "${ctx?.agentId || 'unknown'}"). Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Set taskPrompt for what the agent should do. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
229
+ }
230
+ } else if (toolKey === 'manage_webhooks') {
231
+ description += '\n\nUse `source`, `events`, `agentId`, and `secret` when creating webhooks. Inbound calls should POST to `/api/webhooks/{id}` with header `x-webhook-secret` when a secret is configured.'
232
+ }
233
+
234
+ tools.push(
235
+ tool(
236
+ async ({ action, id, data }) => {
237
+ const canAccessSecret = (secret: any): boolean => {
238
+ if (!secret) return false
239
+ if (secret.scope !== 'agent') return true
240
+ if (!ctx?.agentId) return false
241
+ return Array.isArray(secret.agentIds) && secret.agentIds.includes(ctx.agentId)
242
+ }
243
+ try {
244
+ if (action === 'list') {
245
+ if (toolKey === 'manage_secrets') {
246
+ const values = Object.values(res.load())
247
+ .filter((s: any) => canAccessSecret(s))
248
+ .map((s: any) => ({
249
+ id: s.id,
250
+ name: s.name,
251
+ service: s.service,
252
+ scope: s.scope || 'global',
253
+ agentIds: s.agentIds || [],
254
+ createdAt: s.createdAt,
255
+ updatedAt: s.updatedAt,
256
+ }))
257
+ return JSON.stringify(values)
258
+ }
259
+ return JSON.stringify(Object.values(res.load()))
260
+ }
261
+ if (action === 'get') {
262
+ if (!id) return 'Error: "id" is required for get action.'
263
+ const all = res.load()
264
+ if (!all[id]) return `Not found: ${res.label} "${id}"`
265
+ if (toolKey === 'manage_secrets') {
266
+ if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
267
+ let value = ''
268
+ try {
269
+ value = all[id].encryptedValue ? decryptKey(all[id].encryptedValue) : ''
270
+ } catch {
271
+ value = ''
272
+ }
273
+ return JSON.stringify({
274
+ id: all[id].id,
275
+ name: all[id].name,
276
+ service: all[id].service,
277
+ scope: all[id].scope || 'global',
278
+ agentIds: all[id].agentIds || [],
279
+ value,
280
+ createdAt: all[id].createdAt,
281
+ updatedAt: all[id].updatedAt,
282
+ })
283
+ }
284
+ return JSON.stringify(all[id])
285
+ }
286
+ if (res.readOnly) return `Cannot ${action} ${res.label} via this tool (read-only).`
287
+ if (action === 'create') {
288
+ const all = res.load()
289
+ const raw = data ? JSON.parse(data) : {}
290
+ const defaults = RESOURCE_DEFAULTS[toolKey]
291
+ const parsed = defaults ? defaults(raw) : raw
292
+ if (parsed && typeof parsed === 'object' && 'id' in parsed) {
293
+ delete (parsed as Record<string, unknown>).id
294
+ }
295
+ // Enforce assignment scope for tasks and schedules
296
+ if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
297
+ if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
298
+ return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
299
+ }
300
+ }
301
+ const now = Date.now()
302
+ if (toolKey === 'manage_schedules') {
303
+ const duplicate = findDuplicateSchedule(all as Record<string, ScheduleLike>, {
304
+ agentId: parsed.agentId || null,
305
+ taskPrompt: parsed.taskPrompt || '',
306
+ scheduleType: parsed.scheduleType || 'interval',
307
+ cron: parsed.cron,
308
+ intervalMs: parsed.intervalMs,
309
+ runAt: parsed.runAt,
310
+ createdByAgentId: ctx?.agentId || null,
311
+ createdInSessionId: ctx?.sessionId || null,
312
+ }, {
313
+ creatorScope: {
314
+ agentId: ctx?.agentId || null,
315
+ sessionId: ctx?.sessionId || null,
316
+ },
317
+ })
318
+ if (duplicate) {
319
+ let changed = false
320
+ const duplicateId = typeof duplicate.id === 'string' ? duplicate.id : ''
321
+ const nextName = resolveScheduleName({
322
+ name: parsed.name ?? duplicate.name,
323
+ taskPrompt: parsed.taskPrompt ?? duplicate.taskPrompt,
324
+ })
325
+ if (nextName && nextName !== duplicate.name) {
326
+ duplicate.name = nextName
327
+ changed = true
328
+ }
329
+ const normalizedStatus = typeof parsed.status === 'string' ? parsed.status.trim().toLowerCase() : ''
330
+ if ((normalizedStatus === 'active' || normalizedStatus === 'paused') && duplicate.status !== normalizedStatus) {
331
+ duplicate.status = normalizedStatus
332
+ changed = true
333
+ }
334
+ if (changed) {
335
+ duplicate.updatedAt = now
336
+ if (duplicateId) all[duplicateId] = duplicate
337
+ res.save(all)
338
+ }
339
+ return JSON.stringify({
340
+ ...duplicate,
341
+ deduplicated: true,
342
+ })
343
+ }
344
+ }
345
+ const newId = crypto.randomBytes(4).toString('hex')
346
+ const entry = {
347
+ id: newId,
348
+ ...parsed,
349
+ createdByAgentId: ctx?.agentId || null,
350
+ createdInSessionId: ctx?.sessionId || null,
351
+ createdAt: now,
352
+ updatedAt: now,
353
+ }
354
+ let responseEntry: any = entry
355
+ if (toolKey === 'manage_secrets') {
356
+ const secretValue = typeof parsed.value === 'string' ? parsed.value : null
357
+ if (!secretValue) return 'Error: data.value is required to create a secret.'
358
+ const normalizedScope = parsed.scope === 'agent' ? 'agent' : 'global'
359
+ const normalizedAgentIds = normalizedScope === 'agent'
360
+ ? Array.from(new Set([
361
+ ...(Array.isArray(parsed.agentIds) ? parsed.agentIds.filter((x: any) => typeof x === 'string') : []),
362
+ ...(ctx?.agentId ? [ctx.agentId] : []),
363
+ ]))
364
+ : []
365
+ const stored = {
366
+ ...entry,
367
+ scope: normalizedScope,
368
+ agentIds: normalizedAgentIds,
369
+ encryptedValue: encryptKey(secretValue),
370
+ }
371
+ delete (stored as any).value
372
+ all[newId] = stored
373
+ const { encryptedValue, ...safe } = stored
374
+ responseEntry = safe
375
+ } else {
376
+ all[newId] = entry
377
+ }
378
+
379
+ if (toolKey === 'manage_tasks' && entry.status === 'completed') {
380
+ const { formatValidationFailure, validateTaskCompletion } = await import('../task-validation')
381
+ const { ensureTaskCompletionReport } = await import('../task-reports')
382
+ const report = ensureTaskCompletionReport(entry as any)
383
+ if (report?.relativePath) (entry as any).completionReportPath = report.relativePath
384
+ const validation = validateTaskCompletion(entry as any, { report })
385
+ ;(entry as any).validation = validation
386
+ if (!validation.ok) {
387
+ entry.status = 'failed'
388
+ ;(entry as any).completedAt = null
389
+ ;(entry as any).error = formatValidationFailure(validation.reasons).slice(0, 500)
390
+ }
391
+ }
392
+
393
+ res.save(all)
394
+ if (toolKey === 'manage_tasks' && entry.status === 'queued') {
395
+ const { enqueueTask } = await import('../queue')
396
+ enqueueTask(newId)
397
+ } else if (
398
+ toolKey === 'manage_tasks'
399
+ && (entry.status === 'completed' || entry.status === 'failed')
400
+ && entry.sessionId
401
+ ) {
402
+ const { disableSessionHeartbeat } = await import('../queue')
403
+ disableSessionHeartbeat(entry.sessionId)
404
+ }
405
+ return JSON.stringify(responseEntry)
406
+ }
407
+ if (action === 'update') {
408
+ if (!id) return 'Error: "id" is required for update action.'
409
+ const all = res.load()
410
+ if (!all[id]) return `Not found: ${res.label} "${id}"`
411
+ const parsed = data ? JSON.parse(data) : {}
412
+ const prevStatus = all[id]?.status
413
+ // Enforce assignment scope for tasks and schedules
414
+ if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
415
+ if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
416
+ return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
417
+ }
418
+ }
419
+ all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
420
+ if (toolKey === 'manage_secrets') {
421
+ if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
422
+ const nextScope = parsed.scope === 'agent'
423
+ ? 'agent'
424
+ : parsed.scope === 'global'
425
+ ? 'global'
426
+ : (all[id].scope === 'agent' ? 'agent' : 'global')
427
+ if (nextScope === 'agent') {
428
+ const incomingIds = Array.isArray(parsed.agentIds)
429
+ ? parsed.agentIds.filter((x: any) => typeof x === 'string')
430
+ : Array.isArray(all[id].agentIds)
431
+ ? all[id].agentIds
432
+ : []
433
+ all[id].agentIds = Array.from(new Set([
434
+ ...incomingIds,
435
+ ...(ctx?.agentId ? [ctx.agentId] : []),
436
+ ]))
437
+ } else {
438
+ all[id].agentIds = []
439
+ }
440
+ all[id].scope = nextScope
441
+ if (typeof parsed.value === 'string' && parsed.value.trim()) {
442
+ all[id].encryptedValue = encryptKey(parsed.value)
443
+ }
444
+ delete all[id].value
445
+ }
446
+
447
+ if (toolKey === 'manage_tasks' && all[id].status === 'completed') {
448
+ const { formatValidationFailure, validateTaskCompletion } = await import('../task-validation')
449
+ const { ensureTaskCompletionReport } = await import('../task-reports')
450
+ const report = ensureTaskCompletionReport(all[id] as any)
451
+ if (report?.relativePath) (all[id] as any).completionReportPath = report.relativePath
452
+ const validation = validateTaskCompletion(all[id] as any, { report })
453
+ ;(all[id] as any).validation = validation
454
+ if (!validation.ok) {
455
+ all[id].status = 'failed'
456
+ ;(all[id] as any).completedAt = null
457
+ ;(all[id] as any).error = formatValidationFailure(validation.reasons).slice(0, 500)
458
+ } else if ((all[id] as any).completedAt == null) {
459
+ ;(all[id] as any).completedAt = Date.now()
460
+ }
461
+ }
462
+
463
+ res.save(all)
464
+ if (toolKey === 'manage_tasks' && prevStatus !== 'queued' && all[id].status === 'queued') {
465
+ const { enqueueTask } = await import('../queue')
466
+ enqueueTask(id)
467
+ } else if (
468
+ toolKey === 'manage_tasks'
469
+ && prevStatus !== all[id].status
470
+ && (all[id].status === 'completed' || all[id].status === 'failed')
471
+ && all[id].sessionId
472
+ ) {
473
+ const { disableSessionHeartbeat } = await import('../queue')
474
+ disableSessionHeartbeat(all[id].sessionId)
475
+ }
476
+ if (toolKey === 'manage_secrets') {
477
+ const { encryptedValue, ...safe } = all[id]
478
+ return JSON.stringify(safe)
479
+ }
480
+ return JSON.stringify(all[id])
481
+ }
482
+ if (action === 'delete') {
483
+ if (!id) return 'Error: "id" is required for delete action.'
484
+ const all = res.load()
485
+ if (!all[id]) return `Not found: ${res.label} "${id}"`
486
+ if (toolKey === 'manage_secrets' && !canAccessSecret(all[id])) {
487
+ return 'Error: you do not have access to this secret.'
488
+ }
489
+ delete all[id]
490
+ res.save(all)
491
+ return JSON.stringify({ deleted: id })
492
+ }
493
+ return `Unknown action "${action}". Valid: list, get, create, update, delete`
494
+ } catch (err: any) {
495
+ return `Error: ${err.message}`
496
+ }
497
+ },
498
+ {
499
+ name: toolKey,
500
+ description,
501
+ schema: z.object({
502
+ action: z.enum(['list', 'get', 'create', 'update', 'delete']).describe('The CRUD action to perform'),
503
+ id: z.string().optional().describe('Resource ID (required for get, update, delete)'),
504
+ data: z.string().optional().describe('JSON string of fields for create/update'),
505
+ }),
506
+ },
507
+ ),
508
+ )
509
+ }
510
+
511
+ if (hasTool('manage_documents')) {
512
+ tools.push(
513
+ tool(
514
+ async ({ action, id, filePath, query, limit, metadata, title }) => {
515
+ try {
516
+ const documents = loadDocuments()
517
+
518
+ if (action === 'list') {
519
+ const rows = Object.values(documents)
520
+ .sort((a: any, b: any) => (b.updatedAt || 0) - (a.updatedAt || 0))
521
+ .slice(0, Math.max(1, Math.min(limit || 100, 500)))
522
+ .map((doc: any) => ({
523
+ id: doc.id,
524
+ title: doc.title,
525
+ fileName: doc.fileName,
526
+ sourcePath: doc.sourcePath,
527
+ textLength: doc.textLength,
528
+ method: doc.method,
529
+ metadata: doc.metadata || {},
530
+ createdAt: doc.createdAt,
531
+ updatedAt: doc.updatedAt,
532
+ }))
533
+ return JSON.stringify(rows)
534
+ }
535
+
536
+ if (action === 'get') {
537
+ if (!id) return 'Error: id is required for get.'
538
+ const doc = documents[id]
539
+ if (!doc) return `Not found: document "${id}"`
540
+ const maxContentChars = 60_000
541
+ return JSON.stringify({
542
+ ...doc,
543
+ content: typeof doc.content === 'string' && doc.content.length > maxContentChars
544
+ ? `${doc.content.slice(0, maxContentChars)}\n... [truncated]`
545
+ : (doc.content || ''),
546
+ })
547
+ }
548
+
549
+ if (action === 'delete') {
550
+ if (!id) return 'Error: id is required for delete.'
551
+ if (!documents[id]) return `Not found: document "${id}"`
552
+ delete documents[id]
553
+ saveDocuments(documents)
554
+ return JSON.stringify({ ok: true, id })
555
+ }
556
+
557
+ if (action === 'upload') {
558
+ if (!filePath?.trim()) return 'Error: filePath is required for upload.'
559
+ const sourcePath = path.isAbsolute(filePath) ? filePath : safePath(cwd, filePath)
560
+ if (!fs.existsSync(sourcePath)) return `Error: file not found: ${filePath}`
561
+ const stat = fs.statSync(sourcePath)
562
+ if (!stat.isFile()) return 'Error: upload expects a file path.'
563
+
564
+ const extracted = extractDocumentText(sourcePath)
565
+ const content = trimDocumentContent(extracted.text)
566
+ if (!content) return 'Error: extracted document text is empty.'
567
+
568
+ const docId = crypto.randomBytes(6).toString('hex')
569
+ const now = Date.now()
570
+ const parsedMetadata = metadata && typeof metadata === 'string'
571
+ ? (() => {
572
+ try {
573
+ const m = JSON.parse(metadata)
574
+ return (m && typeof m === 'object' && !Array.isArray(m)) ? m : {}
575
+ } catch {
576
+ return {}
577
+ }
578
+ })()
579
+ : {}
580
+
581
+ const entry = {
582
+ id: docId,
583
+ title: title?.trim() || path.basename(sourcePath),
584
+ fileName: path.basename(sourcePath),
585
+ sourcePath,
586
+ method: extracted.method,
587
+ textLength: content.length,
588
+ content,
589
+ metadata: parsedMetadata,
590
+ uploadedByAgentId: ctx?.agentId || null,
591
+ uploadedInSessionId: ctx?.sessionId || null,
592
+ createdAt: now,
593
+ updatedAt: now,
594
+ }
595
+ documents[docId] = entry
596
+ saveDocuments(documents)
597
+ return JSON.stringify({
598
+ id: entry.id,
599
+ title: entry.title,
600
+ fileName: entry.fileName,
601
+ textLength: entry.textLength,
602
+ method: entry.method,
603
+ })
604
+ }
605
+
606
+ if (action === 'search') {
607
+ const q = (query || '').trim().toLowerCase()
608
+ if (!q) return 'Error: query is required for search.'
609
+ const terms = q.split(/\s+/).filter(Boolean)
610
+ const max = Math.max(1, Math.min(limit || 5, 50))
611
+
612
+ const matches = Object.values(documents)
613
+ .map((doc: any) => {
614
+ const hay = (doc.content || '').toLowerCase()
615
+ if (!hay) return null
616
+ if (!terms.every((term) => hay.includes(term))) return null
617
+ let score = hay.includes(q) ? 10 : 0
618
+ for (const term of terms) {
619
+ let pos = hay.indexOf(term)
620
+ while (pos !== -1) {
621
+ score += 1
622
+ pos = hay.indexOf(term, pos + term.length)
623
+ }
624
+ }
625
+ const firstTerm = terms[0] || q
626
+ const at = firstTerm ? hay.indexOf(firstTerm) : -1
627
+ const start = at >= 0 ? Math.max(0, at - 120) : 0
628
+ const end = Math.min((doc.content || '').length, start + 320)
629
+ const snippet = ((doc.content || '').slice(start, end) || '').replace(/\s+/g, ' ').trim()
630
+ return {
631
+ id: doc.id,
632
+ title: doc.title,
633
+ score,
634
+ snippet,
635
+ textLength: doc.textLength,
636
+ updatedAt: doc.updatedAt,
637
+ }
638
+ })
639
+ .filter(Boolean)
640
+ .sort((a: any, b: any) => b.score - a.score)
641
+ .slice(0, max)
642
+
643
+ return JSON.stringify({
644
+ query,
645
+ total: matches.length,
646
+ matches,
647
+ })
648
+ }
649
+
650
+ return 'Unknown action. Use list, upload, search, get, or delete.'
651
+ } catch (err: any) {
652
+ return `Error: ${err.message || String(err)}`
653
+ }
654
+ },
655
+ {
656
+ name: 'manage_documents',
657
+ description: 'Upload and index documents, then search/get/delete them for long-term retrieval. Supports PDFs (via pdftotext) and common text/doc formats.',
658
+ schema: z.object({
659
+ action: z.enum(['list', 'upload', 'search', 'get', 'delete']).describe('Document action'),
660
+ id: z.string().optional().describe('Document id (for get/delete)'),
661
+ filePath: z.string().optional().describe('Path to document file for upload (relative to working directory or absolute)'),
662
+ title: z.string().optional().describe('Optional title override for upload'),
663
+ query: z.string().optional().describe('Search query text (for search)'),
664
+ limit: z.number().optional().describe('Max results (default 5 for search, 100 for list)'),
665
+ metadata: z.string().optional().describe('Optional JSON string metadata for upload'),
666
+ }),
667
+ },
668
+ ),
669
+ )
670
+ }
671
+
672
+ return tools
673
+ }