@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.
- package/README.md +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /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 {
|
|
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 &&
|
|
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 {
|
|
100
|
-
|
|
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 {
|
|
122
|
-
|
|
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: {
|
|
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.
|
|
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
|
+
}
|