@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
@@ -5,7 +5,7 @@ import { getPluginManager } from '../plugins'
5
5
  import type { Plugin, PluginHooks, ClawHubSkill } from '@/types'
6
6
  import { searchClawHub } from '../clawhub-client'
7
7
  import { normalizeToolInputArgs } from './normalize-tool-args'
8
- import { toolIdMatches } from '../tool-aliases'
8
+ import { pluginIdMatches } from '../tool-aliases'
9
9
  import { loadSessions } from '../storage'
10
10
 
11
11
  /**
@@ -88,7 +88,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
88
88
  if (bctx?.ctx?.sessionId) {
89
89
  const allSessions = loadSessions()
90
90
  const currentSession = allSessions[bctx.ctx.sessionId]
91
- if (currentSession && toolIdMatches(currentSession.tools, pluginId)) {
91
+ if (currentSession && pluginIdMatches(currentSession.tools, pluginId)) {
92
92
  return JSON.stringify({
93
93
  alreadyGranted: true,
94
94
  pluginId,
@@ -96,8 +96,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
96
96
  })
97
97
  }
98
98
  }
99
- const { requestApproval } = await import('../approvals')
100
- requestApproval({
99
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
100
+ const approval = await requestApprovalMaybeAutoApprove({
101
101
  category: 'tool_access',
102
102
  title: `Enable Plugin: ${pluginId}`,
103
103
  description: reason || `Agent is requesting access to the "${pluginId}" plugin.`,
@@ -105,6 +105,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
105
105
  agentId: bctx?.ctx?.agentId,
106
106
  sessionId: bctx?.ctx?.sessionId,
107
107
  })
108
+ if (approval.status === 'approved') {
109
+ return JSON.stringify({
110
+ alreadyGranted: true,
111
+ pluginId,
112
+ toolId: pluginId,
113
+ autoApproved: true,
114
+ message: `Access to "${pluginId}" was auto-approved and granted. Proceed to use it directly.`,
115
+ })
116
+ }
108
117
  return JSON.stringify({
109
118
  type: 'plugin_request',
110
119
  pluginId,
@@ -118,8 +127,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
118
127
  return JSON.stringify({ error: 'url is required for install_request.' })
119
128
  }
120
129
  if (approved !== true) {
121
- const { requestApproval } = await import('../approvals')
122
- requestApproval({
130
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
131
+ const approval = await requestApprovalMaybeAutoApprove({
123
132
  category: 'plugin_install',
124
133
  title: `Install Plugin${pluginId ? `: ${pluginId}` : ' from URL'}`,
125
134
  description: reason || `Agent wants to install a plugin${url ? ` from ${url}` : ''}.`,
@@ -127,6 +136,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
127
136
  agentId: bctx?.ctx?.agentId,
128
137
  sessionId: bctx?.ctx?.sessionId,
129
138
  })
139
+ if (approval.status === 'approved') {
140
+ return JSON.stringify({
141
+ type: 'plugin_install_request',
142
+ url,
143
+ pluginId,
144
+ autoApproved: true,
145
+ message: `Plugin install from ${url} was auto-approved and has been applied.`,
146
+ })
147
+ }
130
148
  return JSON.stringify({
131
149
  type: 'plugin_install_request',
132
150
  url,
@@ -0,0 +1,283 @@
1
+ import path from 'path'
2
+ import { z } from 'zod'
3
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
4
+ import { genId } from '@/lib/id'
5
+ import type { DocumentEntry, Plugin, PluginHooks } from '@/types'
6
+ import { getPluginManager } from '../plugins'
7
+ import { loadDocuments, saveDocuments } from '../storage'
8
+ import { extractDocumentArtifact } from '../document-utils'
9
+ import type { ToolBuildContext } from './context'
10
+ import { findBinaryOnPath, safePath } from './context'
11
+ import { normalizeToolInputArgs } from './normalize-tool-args'
12
+
13
+ function parseMetadataInput(value: unknown): Record<string, unknown> {
14
+ if (!value) return {}
15
+ if (typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>
16
+ if (typeof value === 'string' && value.trim()) {
17
+ try {
18
+ const parsed = JSON.parse(value)
19
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as Record<string, unknown>
20
+ } catch {
21
+ return {}
22
+ }
23
+ }
24
+ return {}
25
+ }
26
+
27
+ function resolveFilePath(cwd: string, value: unknown): string {
28
+ if (typeof value !== 'string' || !value.trim()) throw new Error('filePath is required.')
29
+ return path.isAbsolute(value) ? path.resolve(value) : safePath(cwd, value)
30
+ }
31
+
32
+ function previewTables(tables: Awaited<ReturnType<typeof extractDocumentArtifact>>['tables']) {
33
+ return tables.map((table) => ({
34
+ name: table.name,
35
+ headers: table.headers,
36
+ rowCount: table.rowCount,
37
+ rows: table.rows.slice(0, 20),
38
+ truncated: table.rowCount > 20,
39
+ }))
40
+ }
41
+
42
+ function searchStoredDocuments(documents: Record<string, DocumentEntry>, query: string, limit: number) {
43
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean)
44
+ return Object.values(documents)
45
+ .map((doc) => {
46
+ const hay = `${doc.title}\n${doc.fileName}\n${doc.content}`.toLowerCase()
47
+ if (!terms.every((term) => hay.includes(term))) return null
48
+ let score = hay.includes(query.toLowerCase()) ? 10 : 0
49
+ for (const term of terms) {
50
+ let at = hay.indexOf(term)
51
+ while (at !== -1) {
52
+ score += 1
53
+ at = hay.indexOf(term, at + term.length)
54
+ }
55
+ }
56
+ const firstTerm = terms[0] || query
57
+ const at = hay.indexOf(firstTerm.toLowerCase())
58
+ const start = at >= 0 ? Math.max(0, at - 120) : 0
59
+ const end = Math.min(doc.content.length, start + 360)
60
+ return {
61
+ id: doc.id,
62
+ title: doc.title,
63
+ fileName: doc.fileName,
64
+ score,
65
+ snippet: doc.content.slice(start, end).replace(/\s+/g, ' ').trim(),
66
+ updatedAt: doc.updatedAt,
67
+ }
68
+ })
69
+ .filter((entry): entry is NonNullable<typeof entry> => !!entry)
70
+ .sort((a, b) => b.score - a.score)
71
+ .slice(0, limit)
72
+ }
73
+
74
+ async function executeDocumentAction(
75
+ args: Record<string, unknown>,
76
+ bctx: { cwd: string; sessionId?: string | null; agentId?: string | null },
77
+ ) {
78
+ const normalized = normalizeToolInputArgs(args)
79
+ const action = String(normalized.action || 'status').trim().toLowerCase()
80
+
81
+ try {
82
+ if (action === 'status') {
83
+ return JSON.stringify({
84
+ pdftotext: findBinaryOnPath('pdftotext') || null,
85
+ textutil: findBinaryOnPath('textutil') || null,
86
+ tesseract: findBinaryOnPath('tesseract') || null,
87
+ supports: ['read', 'metadata', 'ocr', 'extract_tables', 'store', 'list', 'search', 'get', 'delete'],
88
+ })
89
+ }
90
+
91
+ if (action === 'list' || action === 'list_stored') {
92
+ const documents = loadDocuments() as Record<string, DocumentEntry>
93
+ const limit = typeof normalized.limit === 'number' ? Math.max(1, Math.min(normalized.limit, 200)) : 50
94
+ return JSON.stringify(
95
+ Object.values(documents)
96
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
97
+ .slice(0, limit)
98
+ .map((doc) => ({
99
+ id: doc.id,
100
+ title: doc.title,
101
+ fileName: doc.fileName,
102
+ sourcePath: doc.sourcePath,
103
+ textLength: doc.textLength,
104
+ method: doc.method,
105
+ metadata: doc.metadata || {},
106
+ createdAt: doc.createdAt,
107
+ updatedAt: doc.updatedAt,
108
+ })),
109
+ )
110
+ }
111
+
112
+ if (action === 'search' || action === 'search_stored') {
113
+ const query = typeof normalized.query === 'string' ? normalized.query.trim() : ''
114
+ if (!query) return 'Error: query is required.'
115
+ const documents = loadDocuments() as Record<string, DocumentEntry>
116
+ const limit = typeof normalized.limit === 'number' ? Math.max(1, Math.min(normalized.limit, 50)) : 10
117
+ const matches = searchStoredDocuments(documents, query, limit)
118
+ return JSON.stringify({ query, total: matches.length, matches })
119
+ }
120
+
121
+ if (action === 'get' || action === 'get_stored') {
122
+ const id = typeof normalized.id === 'string' ? normalized.id.trim() : ''
123
+ if (!id) return 'Error: id is required.'
124
+ const documents = loadDocuments() as Record<string, DocumentEntry>
125
+ const doc = documents[id]
126
+ if (!doc) return `Error: document "${id}" not found.`
127
+ return JSON.stringify({
128
+ ...doc,
129
+ content: doc.content.length > 80_000 ? `${doc.content.slice(0, 80_000)}\n... [truncated]` : doc.content,
130
+ })
131
+ }
132
+
133
+ if (action === 'delete' || action === 'delete_stored') {
134
+ const id = typeof normalized.id === 'string' ? normalized.id.trim() : ''
135
+ if (!id) return 'Error: id is required.'
136
+ const documents = loadDocuments() as Record<string, DocumentEntry>
137
+ if (!documents[id]) return `Error: document "${id}" not found.`
138
+ delete documents[id]
139
+ saveDocuments(documents)
140
+ return JSON.stringify({ ok: true, id })
141
+ }
142
+
143
+ const filePath = resolveFilePath(bctx.cwd, normalized.filePath ?? normalized.path)
144
+ const artifact = await extractDocumentArtifact(filePath, {
145
+ preferOcr: action === 'ocr' || normalized.preferOcr === true,
146
+ maxChars: typeof normalized.maxChars === 'number' ? Math.max(5_000, normalized.maxChars) : undefined,
147
+ })
148
+
149
+ if (action === 'metadata') {
150
+ return JSON.stringify({
151
+ filePath: artifact.filePath,
152
+ fileName: artifact.fileName,
153
+ ext: artifact.ext,
154
+ method: artifact.method,
155
+ metadata: artifact.metadata,
156
+ textLength: artifact.text.length,
157
+ tableCount: artifact.tables.length,
158
+ })
159
+ }
160
+
161
+ if (action === 'extract_tables') {
162
+ return JSON.stringify({
163
+ filePath: artifact.filePath,
164
+ fileName: artifact.fileName,
165
+ tableCount: artifact.tables.length,
166
+ tables: previewTables(artifact.tables),
167
+ })
168
+ }
169
+
170
+ if (action === 'store') {
171
+ if (!artifact.text.trim()) return 'Error: extracted document text is empty.'
172
+ const documents = loadDocuments() as Record<string, DocumentEntry>
173
+ const now = Date.now()
174
+ const docId = genId(8)
175
+ const entry: DocumentEntry = {
176
+ id: docId,
177
+ title: typeof normalized.title === 'string' && normalized.title.trim() ? normalized.title.trim() : artifact.fileName,
178
+ fileName: artifact.fileName,
179
+ sourcePath: artifact.filePath,
180
+ content: artifact.text,
181
+ method: artifact.method,
182
+ textLength: artifact.text.length,
183
+ metadata: {
184
+ ...artifact.metadata,
185
+ ...parseMetadataInput(normalized.metadata),
186
+ ext: artifact.ext,
187
+ tableCount: artifact.tables.length,
188
+ storedByAgentId: bctx.agentId || null,
189
+ storedInSessionId: bctx.sessionId || null,
190
+ },
191
+ createdAt: now,
192
+ updatedAt: now,
193
+ }
194
+ documents[entry.id] = entry
195
+ saveDocuments(documents)
196
+ return JSON.stringify({
197
+ id: entry.id,
198
+ title: entry.title,
199
+ fileName: entry.fileName,
200
+ textLength: entry.textLength,
201
+ method: entry.method,
202
+ metadata: entry.metadata,
203
+ })
204
+ }
205
+
206
+ if (action === 'read' || action === 'ocr') {
207
+ return JSON.stringify({
208
+ filePath: artifact.filePath,
209
+ fileName: artifact.fileName,
210
+ ext: artifact.ext,
211
+ method: artifact.method,
212
+ text: artifact.text,
213
+ textLength: artifact.text.length,
214
+ metadata: artifact.metadata,
215
+ tableCount: artifact.tables.length,
216
+ tables: previewTables(artifact.tables),
217
+ })
218
+ }
219
+
220
+ return `Error: Unknown action "${action}".`
221
+ } catch (err: unknown) {
222
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
223
+ }
224
+ }
225
+
226
+ const DocumentPlugin: Plugin = {
227
+ name: 'Document',
228
+ enabledByDefault: false,
229
+ description: 'Extract text/tables/OCR from local documents and optionally store them for later retrieval.',
230
+ hooks: {
231
+ getCapabilityDescription: () =>
232
+ 'I can parse local documents with `document`, including PDFs, office docs, OCR-able images, CSV/XLSX tables, and stored document search.',
233
+ } as PluginHooks,
234
+ tools: [
235
+ {
236
+ name: 'document',
237
+ description: 'Document parsing tool. Actions: status, read, metadata, ocr, extract_tables, store, list, list_stored, search, search_stored, get, get_stored, delete, delete_stored.',
238
+ parameters: {
239
+ type: 'object',
240
+ properties: {
241
+ action: {
242
+ type: 'string',
243
+ enum: ['status', 'read', 'metadata', 'ocr', 'extract_tables', 'store', 'list', 'list_stored', 'search', 'search_stored', 'get', 'get_stored', 'delete', 'delete_stored'],
244
+ },
245
+ filePath: { type: 'string' },
246
+ id: { type: 'string' },
247
+ title: { type: 'string' },
248
+ query: { type: 'string' },
249
+ metadata: {},
250
+ limit: { type: 'number' },
251
+ maxChars: { type: 'number' },
252
+ preferOcr: { type: 'boolean' },
253
+ },
254
+ required: ['action'],
255
+ },
256
+ execute: async (args, context) => executeDocumentAction(args, {
257
+ cwd: context.session.cwd || process.cwd(),
258
+ sessionId: context.session.id,
259
+ agentId: context.session.agentId || null,
260
+ }),
261
+ },
262
+ ],
263
+ }
264
+
265
+ getPluginManager().registerBuiltin('document', DocumentPlugin)
266
+
267
+ export function buildDocumentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
268
+ if (!bctx.hasPlugin('document')) return []
269
+ return [
270
+ tool(
271
+ async (args) => executeDocumentAction(args, {
272
+ cwd: bctx.cwd,
273
+ sessionId: bctx.ctx?.sessionId || null,
274
+ agentId: bctx.ctx?.agentId || null,
275
+ }),
276
+ {
277
+ name: 'document',
278
+ description: DocumentPlugin.tools![0].description,
279
+ schema: z.object({}).passthrough(),
280
+ },
281
+ ),
282
+ ]
283
+ }
@@ -43,7 +43,9 @@ async function executeEditFile(args: { filePath: string; oldString: string; newS
43
43
  const EditFilePlugin: Plugin = {
44
44
  name: 'Core Edit File',
45
45
  description: 'Surgical search-and-replace within existing files.',
46
- hooks: {} as PluginHooks,
46
+ hooks: {
47
+ getCapabilityDescription: () => 'I can make precise edits to files (`edit_file`) — surgical find-and-replace without rewriting the whole file.',
48
+ } as PluginHooks,
47
49
  tools: [
48
50
  {
49
51
  name: 'edit_file',
@@ -68,7 +70,7 @@ getPluginManager().registerBuiltin('edit_file', EditFilePlugin)
68
70
  * Legacy Bridge
69
71
  */
70
72
  export function buildEditFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
71
- if (!bctx.hasTool('edit_file')) return []
73
+ if (!bctx.hasPlugin('edit_file')) return []
72
74
  return [
73
75
  tool(
74
76
  async (args) => executeEditFile(args as any, { cwd: bctx.cwd }),
@@ -0,0 +1,320 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { Plugin, PluginHooks } from '@/types'
4
+ import { getPluginManager } from '../plugins'
5
+ import { normalizeToolInputArgs } from './normalize-tool-args'
6
+ import type { ToolBuildContext } from './context'
7
+
8
+ interface SmtpConfig {
9
+ host: string
10
+ port: number
11
+ secure: boolean
12
+ username: string
13
+ password: string
14
+ fromAddress: string
15
+ fromName: string
16
+ }
17
+
18
+ function getSmtpConfig(): SmtpConfig {
19
+ const ps = getPluginManager().getPluginSettings('email')
20
+ return {
21
+ host: (ps.host as string) || '',
22
+ port: Number(ps.port) || 587,
23
+ secure: ps.secure === true || ps.secure === 'true',
24
+ username: (ps.username as string) || '',
25
+ password: (ps.password as string) || '',
26
+ fromAddress: (ps.fromAddress as string) || '',
27
+ fromName: (ps.fromName as string) || 'SwarmClaw Agent',
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Minimal SMTP client using raw sockets.
33
+ * Avoids nodemailer dependency — uses Node's built-in net/tls.
34
+ */
35
+ async function sendSmtpEmail(cfg: SmtpConfig, to: string[], subject: string, body: string, html?: string): Promise<string> {
36
+ const net = await import('net')
37
+ const tls = await import('tls')
38
+
39
+ return new Promise((resolve, reject) => {
40
+ const timeout = setTimeout(() => reject(new Error('SMTP timeout (30s)')), 30_000)
41
+ let socket: import('net').Socket
42
+ const lines: string[] = []
43
+ let phase = 'connect'
44
+
45
+ const cleanup = () => { clearTimeout(timeout); try { socket.destroy() } catch { /* ok */ } }
46
+
47
+ const readLine = (data: Buffer) => {
48
+ const text = data.toString()
49
+ lines.push(text)
50
+ const code = parseInt(text.slice(0, 3), 10)
51
+ return { text, code }
52
+ }
53
+
54
+ const send = (cmd: string) => { socket.write(cmd + '\r\n') }
55
+
56
+ // Build MIME message
57
+ const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36).slice(2)}`
58
+ const date = new Date().toUTCString()
59
+ const msgId = `<${Date.now()}.${Math.random().toString(36).slice(2)}@${cfg.host}>`
60
+ const toHeader = to.join(', ')
61
+
62
+ let message = `From: ${cfg.fromName ? `"${cfg.fromName}" ` : ''}<${cfg.fromAddress}>\r\n`
63
+ message += `To: ${toHeader}\r\n`
64
+ message += `Subject: ${subject}\r\n`
65
+ message += `Date: ${date}\r\n`
66
+ message += `Message-ID: ${msgId}\r\n`
67
+ message += `MIME-Version: 1.0\r\n`
68
+
69
+ if (html) {
70
+ message += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`
71
+ message += `--${boundary}\r\n`
72
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`
73
+ message += body + '\r\n'
74
+ message += `--${boundary}\r\n`
75
+ message += `Content-Type: text/html; charset=utf-8\r\n\r\n`
76
+ message += html + '\r\n'
77
+ message += `--${boundary}--\r\n`
78
+ } else {
79
+ message += `Content-Type: text/plain; charset=utf-8\r\n\r\n`
80
+ message += body + '\r\n'
81
+ }
82
+
83
+ const connectOpts = { host: cfg.host, port: cfg.port }
84
+
85
+ const handleData = (data: Buffer) => {
86
+ const { code } = readLine(data)
87
+
88
+ switch (phase) {
89
+ case 'connect':
90
+ if (code === 220) { phase = 'ehlo'; send(`EHLO ${cfg.host}`) }
91
+ else { cleanup(); reject(new Error(`SMTP connect failed: ${data.toString().trim()}`)) }
92
+ break
93
+ case 'ehlo':
94
+ if (code === 250) {
95
+ if (cfg.secure && !('encrypted' in socket)) {
96
+ phase = 'starttls'; send('STARTTLS')
97
+ } else if (cfg.username) {
98
+ phase = 'auth'; send('AUTH LOGIN')
99
+ } else {
100
+ phase = 'mail_from'; send(`MAIL FROM:<${cfg.fromAddress}>`)
101
+ }
102
+ }
103
+ break
104
+ case 'starttls':
105
+ if (code === 220) {
106
+ const tlsSocket = tls.connect({ socket, host: cfg.host, rejectUnauthorized: false }, () => {
107
+ socket = tlsSocket as unknown as import('net').Socket
108
+ socket.on('data', handleData)
109
+ phase = 'ehlo2'; send(`EHLO ${cfg.host}`)
110
+ })
111
+ tlsSocket.on('error', (err: Error) => { cleanup(); reject(err) })
112
+ }
113
+ break
114
+ case 'ehlo2':
115
+ if (code === 250) {
116
+ if (cfg.username) { phase = 'auth'; send('AUTH LOGIN') }
117
+ else { phase = 'mail_from'; send(`MAIL FROM:<${cfg.fromAddress}>`) }
118
+ }
119
+ break
120
+ case 'auth':
121
+ if (code === 334) { phase = 'auth_user'; send(Buffer.from(cfg.username).toString('base64')) }
122
+ else { cleanup(); reject(new Error(`SMTP AUTH failed: ${data.toString().trim()}`)) }
123
+ break
124
+ case 'auth_user':
125
+ if (code === 334) { phase = 'auth_pass'; send(Buffer.from(cfg.password).toString('base64')) }
126
+ break
127
+ case 'auth_pass':
128
+ if (code === 235) { phase = 'mail_from'; send(`MAIL FROM:<${cfg.fromAddress}>`) }
129
+ else { cleanup(); reject(new Error(`SMTP auth failed: ${data.toString().trim()}`)) }
130
+ break
131
+ case 'mail_from':
132
+ if (code === 250) { phase = 'rcpt_to'; send(`RCPT TO:<${to[0]}>`) }
133
+ break
134
+ case 'rcpt_to':
135
+ if (code === 250) { phase = 'data'; send('DATA') }
136
+ else { cleanup(); reject(new Error(`SMTP RCPT rejected: ${data.toString().trim()}`)) }
137
+ break
138
+ case 'data':
139
+ if (code === 354) { phase = 'message'; send(message + '\r\n.') }
140
+ break
141
+ case 'message':
142
+ if (code === 250) { phase = 'quit'; send('QUIT'); cleanup(); resolve('Email sent successfully.') }
143
+ else { cleanup(); reject(new Error(`SMTP send failed: ${data.toString().trim()}`)) }
144
+ break
145
+ case 'quit':
146
+ cleanup()
147
+ break
148
+ }
149
+ }
150
+
151
+ if (cfg.secure && cfg.port === 465) {
152
+ socket = tls.connect({ ...connectOpts, rejectUnauthorized: false }, () => {
153
+ (socket as unknown as Record<string, boolean>).encrypted = true
154
+ }) as unknown as import('net').Socket
155
+ } else {
156
+ socket = net.createConnection(connectOpts)
157
+ }
158
+
159
+ socket.on('data', handleData)
160
+ socket.on('error', (err: Error) => { cleanup(); reject(err) })
161
+ })
162
+ }
163
+
164
+ async function executeEmail(args: Record<string, unknown>): Promise<string> {
165
+ const normalized = normalizeToolInputArgs(args)
166
+ const action = String(normalized.action || 'send')
167
+
168
+ if (action === 'send') {
169
+ const to = normalized.to
170
+ const recipients: string[] = Array.isArray(to) ? to.map(String) : typeof to === 'string' ? to.split(/[,;\s]+/).filter(Boolean) : []
171
+ if (recipients.length === 0) return 'Error: "to" (recipient email addresses) is required.'
172
+
173
+ const subject = String(normalized.subject || '').trim()
174
+ if (!subject) return 'Error: "subject" is required.'
175
+
176
+ const body = String(normalized.body || '').trim()
177
+ if (!body) return 'Error: "body" (plain text content) is required.'
178
+
179
+ const html = typeof normalized.html === 'string' ? normalized.html : undefined
180
+
181
+ const cfg = getSmtpConfig()
182
+ if (!cfg.host) return 'Error: SMTP host not configured. Ask the user to configure email in Plugin Settings > Email.'
183
+ if (!cfg.fromAddress) return 'Error: From address not configured in email plugin settings.'
184
+
185
+ try {
186
+ const result = await sendSmtpEmail(cfg, recipients, subject, body, html)
187
+ return `${result}\nTo: ${recipients.join(', ')}\nSubject: ${subject}`
188
+ } catch (err: unknown) {
189
+ return `Error sending email: ${err instanceof Error ? err.message : String(err)}`
190
+ }
191
+ }
192
+
193
+ if (action === 'status') {
194
+ const cfg = getSmtpConfig()
195
+ if (!cfg.host) return 'Email plugin not configured. No SMTP host set.'
196
+ return JSON.stringify({
197
+ configured: true,
198
+ host: cfg.host,
199
+ port: cfg.port,
200
+ secure: cfg.secure,
201
+ from: cfg.fromAddress,
202
+ fromName: cfg.fromName,
203
+ })
204
+ }
205
+
206
+ return `Error: Unknown action "${action}". Use "send" or "status".`
207
+ }
208
+
209
+ const EmailPlugin: Plugin = {
210
+ name: 'Email',
211
+ enabledByDefault: false,
212
+ description: 'Send emails via SMTP. Supports plain text and HTML, multiple recipients.',
213
+ hooks: {
214
+ getCapabilityDescription: () =>
215
+ 'I can send emails using `email`. Supports plain text and HTML bodies, multiple recipients.',
216
+ } as PluginHooks,
217
+ tools: [
218
+ {
219
+ name: 'email',
220
+ description: 'Send an email or check email configuration status. For sending: provide to, subject, and body. Optionally include html for rich formatting.',
221
+ parameters: {
222
+ type: 'object',
223
+ properties: {
224
+ action: { type: 'string', enum: ['send', 'status'], description: 'Action to perform (default: send)' },
225
+ to: {
226
+ anyOf: [
227
+ { type: 'string', description: 'Recipient email address(es), comma-separated' },
228
+ { type: 'array', items: { type: 'string' }, description: 'Array of recipient email addresses' },
229
+ ],
230
+ },
231
+ subject: { type: 'string', description: 'Email subject line' },
232
+ body: { type: 'string', description: 'Plain text email body' },
233
+ html: { type: 'string', description: 'Optional HTML email body (sent as multipart/alternative alongside plain text)' },
234
+ },
235
+ required: ['action'],
236
+ },
237
+ execute: async (args) => executeEmail(args),
238
+ },
239
+ ],
240
+ ui: {
241
+ settingsFields: [
242
+ {
243
+ key: 'host',
244
+ label: 'SMTP Host',
245
+ type: 'text',
246
+ required: true,
247
+ placeholder: 'smtp.gmail.com',
248
+ help: 'SMTP server hostname.',
249
+ },
250
+ {
251
+ key: 'port',
252
+ label: 'SMTP Port',
253
+ type: 'number',
254
+ defaultValue: 587,
255
+ help: '587 for STARTTLS, 465 for SSL, 25 for unencrypted.',
256
+ },
257
+ {
258
+ key: 'secure',
259
+ label: 'Use SSL/TLS (port 465)',
260
+ type: 'boolean',
261
+ defaultValue: false,
262
+ help: 'Enable for direct TLS connections (port 465). Leave off for STARTTLS (port 587).',
263
+ },
264
+ {
265
+ key: 'username',
266
+ label: 'Username',
267
+ type: 'text',
268
+ placeholder: 'you@gmail.com',
269
+ help: 'SMTP authentication username (usually your email address).',
270
+ },
271
+ {
272
+ key: 'password',
273
+ label: 'Password',
274
+ type: 'secret',
275
+ required: true,
276
+ placeholder: 'App password or SMTP password',
277
+ help: 'SMTP password. For Gmail, use an App Password.',
278
+ },
279
+ {
280
+ key: 'fromAddress',
281
+ label: 'From Address',
282
+ type: 'text',
283
+ required: true,
284
+ placeholder: 'agent@example.com',
285
+ help: 'The sender email address.',
286
+ },
287
+ {
288
+ key: 'fromName',
289
+ label: 'From Name',
290
+ type: 'text',
291
+ defaultValue: 'SwarmClaw Agent',
292
+ placeholder: 'SwarmClaw Agent',
293
+ help: 'Display name shown to recipients.',
294
+ },
295
+ ],
296
+ },
297
+ }
298
+
299
+ getPluginManager().registerBuiltin('email', EmailPlugin)
300
+
301
+ export function buildEmailTools(bctx: ToolBuildContext): StructuredToolInterface[] {
302
+ if (!bctx.hasPlugin('email')) return []
303
+
304
+ return [
305
+ tool(
306
+ async (args) => executeEmail(args),
307
+ {
308
+ name: 'email',
309
+ description: EmailPlugin.tools![0].description,
310
+ schema: z.object({
311
+ action: z.enum(['send', 'status']).optional().describe('Action (default: send)'),
312
+ to: z.union([z.string(), z.array(z.string())]).optional().describe('Recipient email address(es)'),
313
+ subject: z.string().optional().describe('Email subject line'),
314
+ body: z.string().optional().describe('Plain text email body'),
315
+ html: z.string().optional().describe('Optional HTML body'),
316
+ }),
317
+ },
318
+ ),
319
+ ]
320
+ }