@swarmclawai/swarmclaw 0.7.2 → 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 +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- 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 +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- 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/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- 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 +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- 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 +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- 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 +245 -46
- 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 +74 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- 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/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- 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/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 +250 -61
- 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 +45 -5
- 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 +946 -110
- 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/daemon-state.ts +59 -1
- 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 +13 -39
- 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 +27 -967
- 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 +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- 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 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +70 -32
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery.ts +22 -4
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- 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 +237 -24
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +86 -23
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- 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 +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- 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/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Agent, IdentityContinuityState, Session } from '@/types'
|
|
2
|
+
|
|
3
|
+
function normalizeText(value: unknown, maxChars: number): string | null {
|
|
4
|
+
if (typeof value !== 'string') return null
|
|
5
|
+
const normalized = value.replace(/\s+/g, ' ').trim()
|
|
6
|
+
return normalized ? normalized.slice(0, maxChars) : null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeList(value: unknown, maxItems: number, maxChars: number): string[] {
|
|
10
|
+
if (!Array.isArray(value)) return []
|
|
11
|
+
const seen = new Set<string>()
|
|
12
|
+
const out: string[] = []
|
|
13
|
+
for (const raw of value) {
|
|
14
|
+
const normalized = normalizeText(raw, maxChars)
|
|
15
|
+
if (!normalized) continue
|
|
16
|
+
const key = normalized.toLowerCase()
|
|
17
|
+
if (seen.has(key)) continue
|
|
18
|
+
seen.add(key)
|
|
19
|
+
out.push(normalized)
|
|
20
|
+
if (out.length >= maxItems) break
|
|
21
|
+
}
|
|
22
|
+
return out
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeIdentityContinuityState(raw: unknown): IdentityContinuityState | null {
|
|
26
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
27
|
+
const record = raw as Record<string, unknown>
|
|
28
|
+
const state: IdentityContinuityState = {
|
|
29
|
+
selfSummary: normalizeText(record.selfSummary, 320),
|
|
30
|
+
relationshipSummary: normalizeText(record.relationshipSummary, 320),
|
|
31
|
+
personaLabel: normalizeText(record.personaLabel, 120),
|
|
32
|
+
toneStyle: normalizeText(record.toneStyle, 120),
|
|
33
|
+
boundaries: normalizeList(record.boundaries, 6, 180),
|
|
34
|
+
continuityNotes: normalizeList(record.continuityNotes, 8, 220),
|
|
35
|
+
updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
|
|
36
|
+
? Math.trunc(record.updatedAt)
|
|
37
|
+
: null,
|
|
38
|
+
}
|
|
39
|
+
return state
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fallbackSelfSummary(agent?: Partial<Agent> | null): string | null {
|
|
43
|
+
const description = normalizeText(agent?.description, 220)
|
|
44
|
+
if (description) return `${agent?.name || 'Agent'}: ${description}`
|
|
45
|
+
const soul = normalizeText(agent?.soul, 220)
|
|
46
|
+
if (soul) return `${agent?.name || 'Agent'}: ${soul}`
|
|
47
|
+
const name = normalizeText(agent?.name, 80)
|
|
48
|
+
return name ? `${name}: persistent companion agent` : null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fallbackPersonaLabel(session?: Partial<Session> | null, agent?: Partial<Agent> | null): string | null {
|
|
52
|
+
const threadPersona = normalizeText(session?.connectorContext?.threadPersonaLabel, 120)
|
|
53
|
+
if (threadPersona) return threadPersona
|
|
54
|
+
const threadTitle = normalizeText(session?.connectorContext?.threadTitle, 120)
|
|
55
|
+
if (threadTitle) return threadTitle
|
|
56
|
+
const threadId = normalizeText(session?.connectorContext?.threadId, 80)
|
|
57
|
+
if (threadId) return `${agent?.name || 'Agent'} thread ${threadId}`
|
|
58
|
+
const sessionName = normalizeText(session?.name, 120)
|
|
59
|
+
if (sessionName && !/^new chat$/i.test(sessionName)) return sessionName
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fallbackRelationshipSummary(session?: Partial<Session> | null): string | null {
|
|
64
|
+
const sender = normalizeText(session?.connectorContext?.senderName, 80)
|
|
65
|
+
if (sender) return `Ongoing conversation with ${sender}.`
|
|
66
|
+
const user = normalizeText(session?.user, 80)
|
|
67
|
+
if (user && user !== 'user') return `Ongoing conversation with ${user}.`
|
|
68
|
+
return 'Ongoing conversation with the user.'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildIdentityContinuityContext(
|
|
72
|
+
session?: Partial<Session> | null,
|
|
73
|
+
agent?: Partial<Agent> | null,
|
|
74
|
+
): string {
|
|
75
|
+
const agentState = normalizeIdentityContinuityState(agent?.identityState)
|
|
76
|
+
const sessionState = normalizeIdentityContinuityState(session?.identityState)
|
|
77
|
+
const selfSummary = sessionState?.selfSummary || agentState?.selfSummary || fallbackSelfSummary(agent)
|
|
78
|
+
const relationshipSummary = sessionState?.relationshipSummary || agentState?.relationshipSummary || fallbackRelationshipSummary(session)
|
|
79
|
+
const personaLabel = sessionState?.personaLabel || fallbackPersonaLabel(session, agent)
|
|
80
|
+
const toneStyle = sessionState?.toneStyle || normalizeText(session?.conversationTone, 80) || agentState?.toneStyle
|
|
81
|
+
const boundaries = sessionState?.boundaries?.length
|
|
82
|
+
? sessionState.boundaries
|
|
83
|
+
: agentState?.boundaries?.length
|
|
84
|
+
? agentState.boundaries
|
|
85
|
+
: []
|
|
86
|
+
const continuityNotes = [
|
|
87
|
+
...(agentState?.continuityNotes || []),
|
|
88
|
+
...(sessionState?.continuityNotes || []),
|
|
89
|
+
].slice(-6)
|
|
90
|
+
|
|
91
|
+
const lines: string[] = []
|
|
92
|
+
if (selfSummary) lines.push(`Core self: ${selfSummary}`)
|
|
93
|
+
if (personaLabel) lines.push(`Current persona: ${personaLabel}`)
|
|
94
|
+
if (relationshipSummary) lines.push(`Relationship context: ${relationshipSummary}`)
|
|
95
|
+
if (toneStyle) lines.push(`Observed tone: ${toneStyle}`)
|
|
96
|
+
if (boundaries.length) lines.push(`Boundaries: ${boundaries.join(' | ')}`)
|
|
97
|
+
if (continuityNotes.length) lines.push(`Continuity notes: ${continuityNotes.join(' | ')}`)
|
|
98
|
+
if (!lines.length) return ''
|
|
99
|
+
return `## Identity Continuity\n${lines.join('\n')}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function refreshSessionIdentityState(
|
|
103
|
+
session: Session,
|
|
104
|
+
agent?: Partial<Agent> | null,
|
|
105
|
+
now = Date.now(),
|
|
106
|
+
): IdentityContinuityState {
|
|
107
|
+
const existing = normalizeIdentityContinuityState(session.identityState) || {}
|
|
108
|
+
const agentState = normalizeIdentityContinuityState(agent?.identityState) || {}
|
|
109
|
+
const boundaries = existing.boundaries?.length ? existing.boundaries : (agentState.boundaries || [])
|
|
110
|
+
const continuityNotes = [
|
|
111
|
+
...(agentState.continuityNotes || []),
|
|
112
|
+
...(existing.continuityNotes || []),
|
|
113
|
+
].slice(-8)
|
|
114
|
+
|
|
115
|
+
const next: IdentityContinuityState = {
|
|
116
|
+
selfSummary: existing.selfSummary || agentState.selfSummary || fallbackSelfSummary(agent),
|
|
117
|
+
relationshipSummary: existing.relationshipSummary || agentState.relationshipSummary || fallbackRelationshipSummary(session),
|
|
118
|
+
personaLabel: existing.personaLabel || fallbackPersonaLabel(session, agent),
|
|
119
|
+
toneStyle: normalizeText(session.conversationTone, 80) || existing.toneStyle || agentState.toneStyle,
|
|
120
|
+
boundaries,
|
|
121
|
+
continuityNotes,
|
|
122
|
+
updatedAt: now,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
session.identityState = next
|
|
126
|
+
return next
|
|
127
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { ImapFlow } from 'imapflow'
|
|
4
|
+
import { createTransport } from 'nodemailer'
|
|
5
|
+
import { simpleParser } from 'mailparser'
|
|
6
|
+
import { UPLOAD_DIR, loadConnectors } from './storage'
|
|
7
|
+
import { getPluginManager } from './plugins'
|
|
8
|
+
|
|
9
|
+
export interface MailboxConfig {
|
|
10
|
+
imapHost: string
|
|
11
|
+
imapPort: number
|
|
12
|
+
smtpHost: string
|
|
13
|
+
smtpPort: number
|
|
14
|
+
user: string
|
|
15
|
+
password: string
|
|
16
|
+
smtpUsername: string
|
|
17
|
+
smtpPassword: string
|
|
18
|
+
folder: string
|
|
19
|
+
subjectPrefix?: string
|
|
20
|
+
fromAddress: string
|
|
21
|
+
fromName: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MailboxAttachment {
|
|
25
|
+
id: string
|
|
26
|
+
filename: string
|
|
27
|
+
contentType: string | null
|
|
28
|
+
sizeBytes: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MailboxMessage {
|
|
32
|
+
id: string
|
|
33
|
+
uid: number
|
|
34
|
+
messageId: string | null
|
|
35
|
+
subject: string
|
|
36
|
+
from: string
|
|
37
|
+
fromName: string
|
|
38
|
+
date: string | null
|
|
39
|
+
snippet: string
|
|
40
|
+
text: string
|
|
41
|
+
html: string | null
|
|
42
|
+
threadKey: string
|
|
43
|
+
references: string[]
|
|
44
|
+
hasAttachments: boolean
|
|
45
|
+
attachments: MailboxAttachment[]
|
|
46
|
+
flags: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pickString(...values: unknown[]): string {
|
|
50
|
+
for (const value of values) {
|
|
51
|
+
if (typeof value !== 'string') continue
|
|
52
|
+
const trimmed = value.trim()
|
|
53
|
+
if (trimmed) return trimmed
|
|
54
|
+
}
|
|
55
|
+
return ''
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pickNumber(fallback: number, ...values: unknown[]): number {
|
|
59
|
+
for (const value of values) {
|
|
60
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN
|
|
61
|
+
if (Number.isFinite(parsed) && parsed > 0) return Math.trunc(parsed)
|
|
62
|
+
}
|
|
63
|
+
return fallback
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeThreadKey(subject: string, references: string[]): string {
|
|
67
|
+
if (references.length > 0) return references[references.length - 1]
|
|
68
|
+
return subject.replace(/^re:\s*/i, '').trim().toLowerCase()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sanitizeAttachmentName(value: string | undefined, fallback: string): string {
|
|
72
|
+
const cleaned = String(value || '').replace(/[^a-zA-Z0-9._-]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '')
|
|
73
|
+
return cleaned || fallback
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getMailboxConfig(): MailboxConfig {
|
|
77
|
+
const pluginManager = getPluginManager()
|
|
78
|
+
const mailboxSettings = pluginManager.getPluginSettings('mailbox') as Record<string, unknown>
|
|
79
|
+
const emailSettings = pluginManager.getPluginSettings('email') as Record<string, unknown>
|
|
80
|
+
const connectors = loadConnectors()
|
|
81
|
+
const emailConnector = Object.values(connectors)
|
|
82
|
+
.find((entry) => entry && typeof entry === 'object' && String((entry as Record<string, unknown>).platform || '').toLowerCase() === 'email') as Record<string, unknown> | undefined
|
|
83
|
+
const connectorConfig = emailConnector && typeof emailConnector.config === 'object' && emailConnector.config
|
|
84
|
+
? emailConnector.config as Record<string, unknown>
|
|
85
|
+
: {}
|
|
86
|
+
|
|
87
|
+
const user = pickString(mailboxSettings.user, connectorConfig.user)
|
|
88
|
+
const password = pickString(mailboxSettings.password, connectorConfig.password)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
imapHost: pickString(mailboxSettings.imapHost, connectorConfig.imapHost),
|
|
92
|
+
imapPort: pickNumber(993, mailboxSettings.imapPort, connectorConfig.imapPort),
|
|
93
|
+
smtpHost: pickString(mailboxSettings.smtpHost, emailSettings.host, connectorConfig.smtpHost),
|
|
94
|
+
smtpPort: pickNumber(587, mailboxSettings.smtpPort, emailSettings.port, connectorConfig.smtpPort),
|
|
95
|
+
user,
|
|
96
|
+
password,
|
|
97
|
+
smtpUsername: pickString(mailboxSettings.smtpUsername, emailSettings.username, connectorConfig.user, user),
|
|
98
|
+
smtpPassword: pickString(mailboxSettings.smtpPassword, emailSettings.password, connectorConfig.password, password),
|
|
99
|
+
folder: pickString(mailboxSettings.folder, connectorConfig.folder, 'INBOX') || 'INBOX',
|
|
100
|
+
subjectPrefix: pickString(mailboxSettings.subjectPrefix, connectorConfig.subjectPrefix) || undefined,
|
|
101
|
+
fromAddress: pickString(mailboxSettings.fromAddress, emailSettings.fromAddress, connectorConfig.user, user),
|
|
102
|
+
fromName: pickString(mailboxSettings.fromName, emailSettings.fromName, 'SwarmClaw Agent'),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ensureMailboxConfigured(config: MailboxConfig): void {
|
|
107
|
+
if (!config.imapHost || !config.user || !config.password) {
|
|
108
|
+
throw new Error('Mailbox plugin requires IMAP host, user, and password.')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function withImapClient<T>(config: MailboxConfig, fn: (client: ImapFlow) => Promise<T>): Promise<T> {
|
|
113
|
+
ensureMailboxConfigured(config)
|
|
114
|
+
const client = new ImapFlow({
|
|
115
|
+
host: config.imapHost,
|
|
116
|
+
port: config.imapPort,
|
|
117
|
+
secure: config.imapPort === 993,
|
|
118
|
+
auth: {
|
|
119
|
+
user: config.user,
|
|
120
|
+
pass: config.password,
|
|
121
|
+
},
|
|
122
|
+
logger: false,
|
|
123
|
+
})
|
|
124
|
+
await client.connect()
|
|
125
|
+
try {
|
|
126
|
+
return await fn(client)
|
|
127
|
+
} finally {
|
|
128
|
+
try { await client.logout() } catch { /* ignore */ }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function messageMatchesFilters(message: MailboxMessage, filters: {
|
|
133
|
+
query?: string
|
|
134
|
+
from?: string
|
|
135
|
+
subjectContains?: string
|
|
136
|
+
bodyContains?: string
|
|
137
|
+
unreadOnly?: boolean
|
|
138
|
+
hasAttachments?: boolean
|
|
139
|
+
uidGreaterThan?: number
|
|
140
|
+
}) {
|
|
141
|
+
if (typeof filters.uidGreaterThan === 'number' && message.uid <= filters.uidGreaterThan) return false
|
|
142
|
+
if (filters.unreadOnly === true && message.flags.includes('\\Seen')) return false
|
|
143
|
+
if (filters.hasAttachments === true && !message.hasAttachments) return false
|
|
144
|
+
const from = filters.from?.trim().toLowerCase()
|
|
145
|
+
if (from && !message.from.toLowerCase().includes(from) && !message.fromName.toLowerCase().includes(from)) return false
|
|
146
|
+
const subjectContains = filters.subjectContains?.trim().toLowerCase()
|
|
147
|
+
if (subjectContains && !message.subject.toLowerCase().includes(subjectContains)) return false
|
|
148
|
+
const bodyContains = filters.bodyContains?.trim().toLowerCase()
|
|
149
|
+
if (bodyContains && !message.text.toLowerCase().includes(bodyContains)) return false
|
|
150
|
+
const query = filters.query?.trim().toLowerCase()
|
|
151
|
+
if (query) {
|
|
152
|
+
const hay = `${message.subject}\n${message.from}\n${message.fromName}\n${message.text}`.toLowerCase()
|
|
153
|
+
if (!hay.includes(query)) return false
|
|
154
|
+
}
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function toMailboxMessage(raw: {
|
|
159
|
+
uid: number
|
|
160
|
+
envelope?: {
|
|
161
|
+
from?: Array<{ name?: string; address?: string }>
|
|
162
|
+
subject?: string
|
|
163
|
+
messageId?: string
|
|
164
|
+
date?: Date
|
|
165
|
+
inReplyTo?: string
|
|
166
|
+
references?: string[]
|
|
167
|
+
}
|
|
168
|
+
flags?: Set<string>
|
|
169
|
+
source?: Buffer
|
|
170
|
+
}, parsed: Awaited<ReturnType<typeof simpleParser>>): MailboxMessage {
|
|
171
|
+
const fromAddress = raw.envelope?.from?.[0]?.address || parsed.from?.value?.[0]?.address || 'unknown'
|
|
172
|
+
const fromName = raw.envelope?.from?.[0]?.name || parsed.from?.value?.[0]?.name || fromAddress
|
|
173
|
+
const references = [
|
|
174
|
+
...(Array.isArray(raw.envelope?.references) ? raw.envelope?.references : []),
|
|
175
|
+
...(parsed.references ? (Array.isArray(parsed.references) ? parsed.references : [parsed.references]) : []),
|
|
176
|
+
].filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
id: String(raw.uid),
|
|
180
|
+
uid: raw.uid,
|
|
181
|
+
messageId: raw.envelope?.messageId || parsed.messageId || null,
|
|
182
|
+
subject: raw.envelope?.subject || parsed.subject || '(no subject)',
|
|
183
|
+
from: fromAddress,
|
|
184
|
+
fromName,
|
|
185
|
+
date: raw.envelope?.date ? raw.envelope.date.toISOString() : (parsed.date ? parsed.date.toISOString() : null),
|
|
186
|
+
snippet: (parsed.text || parsed.html || '').replace(/\s+/g, ' ').trim().slice(0, 240),
|
|
187
|
+
text: (parsed.text || '').trim(),
|
|
188
|
+
html: typeof parsed.html === 'string' ? parsed.html : null,
|
|
189
|
+
threadKey: normalizeThreadKey(raw.envelope?.subject || parsed.subject || '', references),
|
|
190
|
+
references,
|
|
191
|
+
hasAttachments: parsed.attachments.length > 0,
|
|
192
|
+
attachments: parsed.attachments.map((attachment, index) => ({
|
|
193
|
+
id: `${raw.uid}:${index}`,
|
|
194
|
+
filename: sanitizeAttachmentName(attachment.filename || undefined, `attachment-${index + 1}`),
|
|
195
|
+
contentType: attachment.contentType || null,
|
|
196
|
+
sizeBytes: attachment.size || 0,
|
|
197
|
+
})),
|
|
198
|
+
flags: Array.from(raw.flags || new Set<string>()),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function getMailboxHighwaterUid(config = getMailboxConfig(), folder?: string): Promise<number> {
|
|
203
|
+
return withImapClient(config, async (client) => {
|
|
204
|
+
const targetFolder = folder || config.folder || 'INBOX'
|
|
205
|
+
const lock = await client.getMailboxLock(targetFolder)
|
|
206
|
+
try {
|
|
207
|
+
const status = await client.status(targetFolder, { uidNext: true })
|
|
208
|
+
return typeof status.uidNext === 'number' ? Math.max(0, status.uidNext - 1) : 0
|
|
209
|
+
} finally {
|
|
210
|
+
lock.release()
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function fetchMailboxMessages(filters?: {
|
|
216
|
+
folder?: string
|
|
217
|
+
query?: string
|
|
218
|
+
from?: string
|
|
219
|
+
subjectContains?: string
|
|
220
|
+
bodyContains?: string
|
|
221
|
+
unreadOnly?: boolean
|
|
222
|
+
hasAttachments?: boolean
|
|
223
|
+
uidGreaterThan?: number
|
|
224
|
+
limit?: number
|
|
225
|
+
}): Promise<MailboxMessage[]> {
|
|
226
|
+
const config = getMailboxConfig()
|
|
227
|
+
return withImapClient(config, async (client) => {
|
|
228
|
+
const folder = filters?.folder || config.folder || 'INBOX'
|
|
229
|
+
const limit = Math.max(1, Math.min(filters?.limit || 20, 100))
|
|
230
|
+
const lock = await client.getMailboxLock(folder)
|
|
231
|
+
try {
|
|
232
|
+
const status = await client.status(folder, { uidNext: true })
|
|
233
|
+
const endUid = typeof status.uidNext === 'number' ? Math.max(0, status.uidNext - 1) : 0
|
|
234
|
+
if (endUid <= 0) return []
|
|
235
|
+
const startUid = Math.max(1, endUid - Math.max(limit * 4, 60) + 1)
|
|
236
|
+
const messages: MailboxMessage[] = []
|
|
237
|
+
for await (const raw of client.fetch(`${startUid}:${endUid}`, {
|
|
238
|
+
uid: true,
|
|
239
|
+
envelope: true,
|
|
240
|
+
flags: true,
|
|
241
|
+
source: true,
|
|
242
|
+
}, { uid: true })) {
|
|
243
|
+
if (!raw.source) continue
|
|
244
|
+
const parsed = await simpleParser(raw.source)
|
|
245
|
+
const message = toMailboxMessage(raw, parsed)
|
|
246
|
+
if (!messageMatchesFilters(message, filters || {})) continue
|
|
247
|
+
if (config.subjectPrefix && !message.subject.startsWith(config.subjectPrefix)) continue
|
|
248
|
+
messages.push(message)
|
|
249
|
+
}
|
|
250
|
+
return messages.sort((a, b) => b.uid - a.uid).slice(0, limit)
|
|
251
|
+
} finally {
|
|
252
|
+
lock.release()
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function fetchMailboxMessageByUid(uid: number, folder?: string): Promise<MailboxMessage | null> {
|
|
258
|
+
const messages = await fetchMailboxMessages({ folder, uidGreaterThan: uid - 1, limit: 100 })
|
|
259
|
+
return messages.find((message) => message.uid === uid) || null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function downloadMailboxAttachment(params: {
|
|
263
|
+
uid: number
|
|
264
|
+
attachmentId?: string
|
|
265
|
+
attachmentName?: string
|
|
266
|
+
folder?: string
|
|
267
|
+
saveTo?: string
|
|
268
|
+
cwd?: string
|
|
269
|
+
}): Promise<{ filePath: string; fileName: string; url: string | null }> {
|
|
270
|
+
const config = getMailboxConfig()
|
|
271
|
+
return withImapClient(config, async (client) => {
|
|
272
|
+
const folder = params.folder || config.folder || 'INBOX'
|
|
273
|
+
const lock = await client.getMailboxLock(folder)
|
|
274
|
+
try {
|
|
275
|
+
for await (const raw of client.fetch(String(params.uid), { uid: true, source: true }, { uid: true })) {
|
|
276
|
+
if (!raw.source) continue
|
|
277
|
+
const parsed = await simpleParser(raw.source)
|
|
278
|
+
const selected = parsed.attachments.find((attachment, index) => {
|
|
279
|
+
const generatedId = `${params.uid}:${index}`
|
|
280
|
+
if (params.attachmentId && generatedId === params.attachmentId) return true
|
|
281
|
+
if (params.attachmentName && attachment.filename === params.attachmentName) return true
|
|
282
|
+
return !params.attachmentId && !params.attachmentName && index === 0
|
|
283
|
+
})
|
|
284
|
+
if (!selected) throw new Error('Attachment not found.')
|
|
285
|
+
|
|
286
|
+
const fileName = sanitizeAttachmentName(selected.filename || undefined, `attachment-${params.uid}`)
|
|
287
|
+
const targetPath = params.saveTo
|
|
288
|
+
? path.resolve(params.cwd || process.cwd(), params.saveTo)
|
|
289
|
+
: path.join(UPLOAD_DIR, `${Date.now()}-${fileName}`)
|
|
290
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
291
|
+
fs.writeFileSync(targetPath, selected.content)
|
|
292
|
+
|
|
293
|
+
const publicPath = targetPath.startsWith(UPLOAD_DIR)
|
|
294
|
+
? targetPath
|
|
295
|
+
: path.join(UPLOAD_DIR, `${Date.now()}-${path.basename(targetPath)}`)
|
|
296
|
+
if (publicPath !== targetPath) fs.copyFileSync(targetPath, publicPath)
|
|
297
|
+
return {
|
|
298
|
+
filePath: targetPath,
|
|
299
|
+
fileName,
|
|
300
|
+
url: `/api/uploads/${path.basename(publicPath)}`,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
throw new Error(`Mailbox message not found: ${params.uid}`)
|
|
304
|
+
} finally {
|
|
305
|
+
lock.release()
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function replyMailboxMessage(params: {
|
|
311
|
+
uid: number
|
|
312
|
+
text: string
|
|
313
|
+
html?: string
|
|
314
|
+
subject?: string
|
|
315
|
+
folder?: string
|
|
316
|
+
}): Promise<{ to: string; subject: string }> {
|
|
317
|
+
const config = getMailboxConfig()
|
|
318
|
+
if (!config.smtpHost || !config.fromAddress) {
|
|
319
|
+
throw new Error('Mailbox reply requires SMTP host and fromAddress configuration.')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const message = await fetchMailboxMessageByUid(params.uid, params.folder)
|
|
323
|
+
if (!message) throw new Error(`Mailbox message not found: ${params.uid}`)
|
|
324
|
+
|
|
325
|
+
const transport = createTransport({
|
|
326
|
+
host: config.smtpHost,
|
|
327
|
+
port: config.smtpPort,
|
|
328
|
+
secure: config.smtpPort === 465,
|
|
329
|
+
auth: {
|
|
330
|
+
user: config.smtpUsername || config.user,
|
|
331
|
+
pass: config.smtpPassword || config.password,
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const subject = params.subject?.trim() || `Re: ${message.subject.replace(/^Re:\s*/i, '')}`
|
|
336
|
+
await transport.sendMail({
|
|
337
|
+
from: config.fromName ? `"${config.fromName}" <${config.fromAddress}>` : config.fromAddress,
|
|
338
|
+
to: message.from,
|
|
339
|
+
subject,
|
|
340
|
+
text: params.text,
|
|
341
|
+
html: params.html,
|
|
342
|
+
inReplyTo: message.messageId || undefined,
|
|
343
|
+
references: message.messageId || undefined,
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return { to: message.from, subject }
|
|
347
|
+
}
|