@swarmclawai/swarmclaw 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +5 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +23 -5
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/connectors/[id]/route.ts +7 -4
- package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +4 -3
- package/src/app/api/openclaw/discover/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +3 -2
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/api/secrets/[id]/route.ts +1 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +18 -1
- package/src/app/api/sessions/[id]/route.ts +12 -3
- package/src/app/api/sessions/route.ts +6 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/cli/index.js +33 -1
- package/src/cli/spec.js +40 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +82 -3
- package/src/components/agents/agent-sheet.tsx +31 -0
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +14 -2
- package/src/components/chat/chat-header.tsx +168 -4
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +89 -16
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/projects/project-list.tsx +1 -0
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/settings/settings-page.tsx +1 -0
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +42 -5
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/providers/openclaw.ts +17 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/chat-execution.ts +44 -2
- package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
- package/src/lib/server/connectors/bluebubbles.ts +5 -2
- package/src/lib/server/connectors/googlechat.ts +14 -10
- package/src/lib/server/connectors/manager.ts +37 -15
- package/src/lib/server/connectors/openclaw.ts +1 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/main-agent-loop.ts +2 -3
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +11 -10
- package/src/lib/server/queue.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +4 -4
- package/src/lib/server/session-tools/delegate.ts +19 -3
- package/src/lib/server/session-tools/file.ts +20 -20
- package/src/lib/server/session-tools/index.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
- package/src/lib/server/session-tools/sandbox.ts +2 -2
- package/src/lib/server/session-tools/search-providers.ts +13 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +8 -8
- package/src/lib/server/storage.ts +62 -11
- package/src/lib/server/stream-agent-chat.ts +24 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/stores/use-app-store.ts +53 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +153 -5
- package/src/types/index.ts +127 -1
- package/tsconfig.json +13 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { PersonalityDraft } from '@/types'
|
|
2
|
+
|
|
3
|
+
// --- IDENTITY.md ---
|
|
4
|
+
|
|
5
|
+
export function parseIdentityMd(content: string): PersonalityDraft['identity'] {
|
|
6
|
+
const result: PersonalityDraft['identity'] = {}
|
|
7
|
+
const lines = content.split('\n')
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
const match = line.match(/^\s*-\s*(.+?):\s*(.+)$/)
|
|
10
|
+
if (!match) continue
|
|
11
|
+
const key = match[1].trim().toLowerCase()
|
|
12
|
+
const value = match[2].trim()
|
|
13
|
+
if (key === 'name') result.name = value
|
|
14
|
+
else if (key === 'creature' || key === 'species' || key === 'type') result.creature = value
|
|
15
|
+
else if (key === 'vibe' || key === 'personality') result.vibe = value
|
|
16
|
+
else if (key === 'emoji' || key === 'icon') result.emoji = value
|
|
17
|
+
}
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function serializeIdentityMd(draft: PersonalityDraft['identity']): string {
|
|
22
|
+
const lines: string[] = ['# Identity', '']
|
|
23
|
+
if (draft.name) lines.push(`- Name: ${draft.name}`)
|
|
24
|
+
if (draft.creature) lines.push(`- Creature: ${draft.creature}`)
|
|
25
|
+
if (draft.vibe) lines.push(`- Vibe: ${draft.vibe}`)
|
|
26
|
+
if (draft.emoji) lines.push(`- Emoji: ${draft.emoji}`)
|
|
27
|
+
return lines.join('\n') + '\n'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- USER.md ---
|
|
31
|
+
|
|
32
|
+
export function parseUserMd(content: string): PersonalityDraft['user'] {
|
|
33
|
+
const result: PersonalityDraft['user'] = {}
|
|
34
|
+
const contextIdx = content.indexOf('## Context')
|
|
35
|
+
const headerPart = contextIdx >= 0 ? content.slice(0, contextIdx) : content
|
|
36
|
+
const contextPart = contextIdx >= 0 ? content.slice(contextIdx + '## Context'.length).trim() : ''
|
|
37
|
+
|
|
38
|
+
const lines = headerPart.split('\n')
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const match = line.match(/^\s*-\s*(.+?):\s*(.+)$/)
|
|
41
|
+
if (!match) continue
|
|
42
|
+
const key = match[1].trim().toLowerCase()
|
|
43
|
+
const value = match[2].trim()
|
|
44
|
+
if (key === 'name') result.name = value
|
|
45
|
+
else if (key === 'call them' || key === 'nickname') result.callThem = value
|
|
46
|
+
else if (key === 'pronouns') result.pronouns = value
|
|
47
|
+
else if (key === 'timezone') result.timezone = value
|
|
48
|
+
else if (key === 'notes') result.notes = value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (contextPart) result.context = contextPart
|
|
52
|
+
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function serializeUserMd(draft: PersonalityDraft['user']): string {
|
|
57
|
+
const lines: string[] = ['# User', '']
|
|
58
|
+
if (draft.name) lines.push(`- Name: ${draft.name}`)
|
|
59
|
+
if (draft.callThem) lines.push(`- Call them: ${draft.callThem}`)
|
|
60
|
+
if (draft.pronouns) lines.push(`- Pronouns: ${draft.pronouns}`)
|
|
61
|
+
if (draft.timezone) lines.push(`- Timezone: ${draft.timezone}`)
|
|
62
|
+
if (draft.notes) lines.push(`- Notes: ${draft.notes}`)
|
|
63
|
+
if (draft.context) {
|
|
64
|
+
lines.push('', '## Context', '', draft.context)
|
|
65
|
+
}
|
|
66
|
+
return lines.join('\n') + '\n'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- SOUL.md ---
|
|
70
|
+
|
|
71
|
+
export function parseSoulMd(content: string): PersonalityDraft['soul'] {
|
|
72
|
+
const result: PersonalityDraft['soul'] = {}
|
|
73
|
+
const sections = content.split(/^##\s+/m)
|
|
74
|
+
|
|
75
|
+
for (const section of sections) {
|
|
76
|
+
const nlIdx = section.indexOf('\n')
|
|
77
|
+
if (nlIdx < 0) continue
|
|
78
|
+
const heading = section.slice(0, nlIdx).trim().toLowerCase()
|
|
79
|
+
const body = section.slice(nlIdx + 1).trim()
|
|
80
|
+
|
|
81
|
+
if (heading.startsWith('core truth')) result.coreTruths = body
|
|
82
|
+
else if (heading.startsWith('boundar')) result.boundaries = body
|
|
83
|
+
else if (heading.startsWith('vibe')) result.vibe = body
|
|
84
|
+
else if (heading.startsWith('continuity')) result.continuity = body
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function serializeSoulMd(draft: PersonalityDraft['soul']): string {
|
|
91
|
+
const lines: string[] = ['# Soul', '']
|
|
92
|
+
if (draft.coreTruths) lines.push('## Core Truths', '', draft.coreTruths, '')
|
|
93
|
+
if (draft.boundaries) lines.push('## Boundaries', '', draft.boundaries, '')
|
|
94
|
+
if (draft.vibe) lines.push('## Vibe', '', draft.vibe, '')
|
|
95
|
+
if (draft.continuity) lines.push('## Continuity', '', draft.continuity, '')
|
|
96
|
+
return lines.join('\n')
|
|
97
|
+
}
|
|
@@ -65,6 +65,7 @@ function tryLoadIdentityFile(filePath: string): DeviceIdentity | null {
|
|
|
65
65
|
function loadOrCreateDeviceIdentity(): DeviceIdentity {
|
|
66
66
|
// 0. Check shared device token for cross-synced identity
|
|
67
67
|
try {
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
68
69
|
const { getSharedDeviceToken } = require('../server/openclaw-sync')
|
|
69
70
|
const sharedToken = getSharedDeviceToken()
|
|
70
71
|
if (sharedToken) {
|
|
@@ -344,8 +345,22 @@ export function streamOpenClawChat({ session, message, imagePath, apiKey, write,
|
|
|
344
345
|
for (const p of payloads) {
|
|
345
346
|
const text = typeof p.text === 'string' ? p.text.trimEnd() : ''
|
|
346
347
|
if (text) {
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
// Detect [[trace]], [[tool]], [[tool-result]], [[meta]] prefixes
|
|
349
|
+
const traceMatch = text.match(/^\[\[(thinking|tool|tool-result|trace|meta)\]\]/)
|
|
350
|
+
if (traceMatch) {
|
|
351
|
+
const traceType = traceMatch[1]
|
|
352
|
+
const traceContent = text.slice(traceMatch[0].length)
|
|
353
|
+
if (traceType === 'meta') {
|
|
354
|
+
write(`data: ${JSON.stringify({ t: 'md', text: traceContent })}\n\n`)
|
|
355
|
+
} else {
|
|
356
|
+
// Include as text (client-side will parse trace markers)
|
|
357
|
+
fullResponse += text
|
|
358
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
fullResponse += text
|
|
362
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
363
|
+
}
|
|
349
364
|
}
|
|
350
365
|
}
|
|
351
366
|
if (!fullResponse && msg.payload?.summary) {
|
package/src/lib/runtime-loop.ts
CHANGED
|
@@ -11,5 +11,5 @@ export const DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES = 60
|
|
|
11
11
|
|
|
12
12
|
// Tool/process timeouts
|
|
13
13
|
export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC = 30
|
|
14
|
-
export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC =
|
|
15
|
-
export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC =
|
|
14
|
+
export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC = 1800
|
|
15
|
+
export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC = 1800
|
|
@@ -343,13 +343,27 @@ function resolveApiKeyForSession(session: SessionWithCredentials, provider: Prov
|
|
|
343
343
|
|
|
344
344
|
function classifyHeartbeatResponse(text: string, ackMaxChars: number): 'suppress' | 'strip' | 'keep' {
|
|
345
345
|
const trimmed = text.trim()
|
|
346
|
-
if (trimmed === 'HEARTBEAT_OK') return 'suppress'
|
|
347
|
-
const stripped = trimmed.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
346
|
+
if (trimmed === 'HEARTBEAT_OK' || trimmed === 'NO_MESSAGE') return 'suppress'
|
|
347
|
+
const stripped = trimmed.replace(/HEARTBEAT_OK/gi, '').replace(/NO_MESSAGE/gi, '').trim()
|
|
348
348
|
if (!stripped) return 'suppress'
|
|
349
349
|
if (stripped.length <= ackMaxChars) return 'suppress'
|
|
350
350
|
return stripped.length < trimmed.length ? 'strip' : 'keep'
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
function estimateConversationTone(text: string): string {
|
|
354
|
+
const t = text || ''
|
|
355
|
+
// Technical: code blocks, function signatures, technical terms
|
|
356
|
+
if (/```/.test(t) || /\b(function|const|let|var|import|export|class|interface|async|await|return)\b/.test(t)) return 'technical'
|
|
357
|
+
if (/\b(error|bug|debug|stack trace|exception|null|undefined|TypeError)\b/i.test(t)) return 'technical'
|
|
358
|
+
// Empathetic: emotional/supportive language
|
|
359
|
+
if (/\b(understand|feel|sorry|empathize|appreciate|grateful|tough|difficult|challenging)\b/i.test(t)) return 'empathetic'
|
|
360
|
+
// Formal: academic/business language
|
|
361
|
+
if (/\b(furthermore|regarding|consequently|therefore|henceforth|pursuant|accordingly|notwithstanding)\b/i.test(t)) return 'formal'
|
|
362
|
+
// Casual: contractions, exclamations, informal language
|
|
363
|
+
if (/\b(gonna|wanna|gotta|yeah|hey|awesome|cool|lol|btw|tbh)\b/i.test(t) || /!{2,}/.test(t)) return 'casual'
|
|
364
|
+
return 'neutral'
|
|
365
|
+
}
|
|
366
|
+
|
|
353
367
|
const AUTO_MEMORY_MIN_INTERVAL_MS = 45 * 60 * 1000
|
|
354
368
|
|
|
355
369
|
function normalizeMemoryText(value: string): string {
|
|
@@ -816,6 +830,26 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
816
830
|
const finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
817
831
|
const textForPersistence = stripMainLoopMetaForPersistence(finalText, internal)
|
|
818
832
|
|
|
833
|
+
// Emit status SSE event from [MAIN_LOOP_META] if present
|
|
834
|
+
if (internal && finalText) {
|
|
835
|
+
const metaMatch = finalText.match(/\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i)
|
|
836
|
+
if (metaMatch) {
|
|
837
|
+
try {
|
|
838
|
+
const meta = JSON.parse(metaMatch[1])
|
|
839
|
+
const statusPayload: Record<string, string | undefined> = {}
|
|
840
|
+
if (meta.goal) statusPayload.goal = String(meta.goal)
|
|
841
|
+
if (meta.status) statusPayload.status = String(meta.status)
|
|
842
|
+
if (meta.summary) statusPayload.summary = String(meta.summary)
|
|
843
|
+
if (meta.next_action) statusPayload.nextAction = String(meta.next_action)
|
|
844
|
+
if (Object.keys(statusPayload).length > 0) {
|
|
845
|
+
emit({ t: 'status', text: JSON.stringify(statusPayload) })
|
|
846
|
+
}
|
|
847
|
+
} catch {
|
|
848
|
+
// ignore malformed meta JSON
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
819
853
|
// HEARTBEAT_OK suppression
|
|
820
854
|
const heartbeatConfig = input.heartbeatConfig
|
|
821
855
|
let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
|
|
@@ -875,6 +909,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
875
909
|
})
|
|
876
910
|
changed = true
|
|
877
911
|
|
|
912
|
+
// Conversation tone detection
|
|
913
|
+
if (!internal) {
|
|
914
|
+
const tone = estimateConversationTone(persistedText)
|
|
915
|
+
if (tone !== current.conversationTone) {
|
|
916
|
+
current.conversationTone = tone
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
878
920
|
// Target routing for non-suppressed heartbeat alerts
|
|
879
921
|
if (isHeartbeatRun && heartbeatConfig?.target && heartbeatConfig.target !== 'none' && heartbeatConfig.showAlerts !== false) {
|
|
880
922
|
try {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
3
|
import bluebubbles from './bluebubbles.ts'
|
|
4
|
+
import type { Connector } from '@/types'
|
|
5
|
+
import type { InboundMessage } from './types'
|
|
4
6
|
|
|
5
7
|
type FetchCall = {
|
|
6
8
|
url: string
|
|
@@ -10,7 +12,7 @@ type FetchCall = {
|
|
|
10
12
|
type MockResponse = {
|
|
11
13
|
ok: boolean
|
|
12
14
|
status: number
|
|
13
|
-
json: () => Promise<
|
|
15
|
+
json: () => Promise<unknown>
|
|
14
16
|
text: () => Promise<string>
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -37,6 +39,7 @@ function textResponse(status: number, text: string): MockResponse {
|
|
|
37
39
|
const originalFetch = globalThis.fetch
|
|
38
40
|
|
|
39
41
|
test.afterEach(() => {
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
43
|
;(globalThis as any).fetch = originalFetch
|
|
41
44
|
})
|
|
42
45
|
|
|
@@ -47,14 +50,15 @@ test('bluebubbles connector processes inbound webhook payloads and sends replies
|
|
|
47
50
|
jsonResponse(200, { data: { guid: 'msg-1' } }), // send reply
|
|
48
51
|
]
|
|
49
52
|
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
54
|
;(globalThis as any).fetch = async (url: string, init?: RequestInit) => {
|
|
51
55
|
calls.push({ url: String(url), init })
|
|
52
56
|
const next = queue.shift()
|
|
53
57
|
assert.ok(next, 'unexpected fetch call')
|
|
54
|
-
return next
|
|
58
|
+
return next
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
const received:
|
|
61
|
+
const received: InboundMessage[] = []
|
|
58
62
|
const connector = {
|
|
59
63
|
id: 'bb-1',
|
|
60
64
|
name: 'BlueBubbles Test',
|
|
@@ -68,7 +72,7 @@ test('bluebubbles connector processes inbound webhook payloads and sends replies
|
|
|
68
72
|
status: 'running',
|
|
69
73
|
createdAt: Date.now(),
|
|
70
74
|
updatedAt: Date.now(),
|
|
71
|
-
} as
|
|
75
|
+
} as unknown as Connector
|
|
72
76
|
|
|
73
77
|
const instance = await bluebubbles.start(connector, 'pw-test', async (msg) => {
|
|
74
78
|
received.push(msg)
|
|
@@ -76,8 +80,10 @@ test('bluebubbles connector processes inbound webhook payloads and sends replies
|
|
|
76
80
|
})
|
|
77
81
|
|
|
78
82
|
try {
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
84
|
const handler = (globalThis as any).__swarmclaw_bluebubbles_handler_bb_1__
|
|
80
85
|
assert.equal(typeof handler, 'undefined', 'sanity: wrong handler key should be undefined')
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
87
|
const validHandler = (globalThis as any)[`__swarmclaw_bluebubbles_handler_${connector.id}__`]
|
|
82
88
|
assert.equal(typeof validHandler, 'function')
|
|
83
89
|
|
|
@@ -116,11 +122,12 @@ test('bluebubbles connector supports array-wrapped webhook payload and NO_MESSAG
|
|
|
116
122
|
jsonResponse(200, { ok: true }), // ping
|
|
117
123
|
]
|
|
118
124
|
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
126
|
;(globalThis as any).fetch = async (url: string, init?: RequestInit) => {
|
|
120
127
|
calls.push({ url: String(url), init })
|
|
121
128
|
const next = queue.shift()
|
|
122
129
|
assert.ok(next, 'unexpected fetch call')
|
|
123
|
-
return next
|
|
130
|
+
return next
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
const connector = {
|
|
@@ -136,11 +143,12 @@ test('bluebubbles connector supports array-wrapped webhook payload and NO_MESSAG
|
|
|
136
143
|
status: 'running',
|
|
137
144
|
createdAt: Date.now(),
|
|
138
145
|
updatedAt: Date.now(),
|
|
139
|
-
} as
|
|
146
|
+
} as unknown as Connector
|
|
140
147
|
|
|
141
148
|
const instance = await bluebubbles.start(connector, 'pw-test', async () => 'NO_MESSAGE')
|
|
142
149
|
|
|
143
150
|
try {
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
152
|
const handler = (globalThis as any)[`__swarmclaw_bluebubbles_handler_${connector.id}__`]
|
|
145
153
|
assert.equal(typeof handler, 'function')
|
|
146
154
|
|
|
@@ -171,11 +179,12 @@ test('bluebubbles sendMessage posts to message/text endpoint', async () => {
|
|
|
171
179
|
textResponse(200, ''), // send
|
|
172
180
|
]
|
|
173
181
|
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
183
|
;(globalThis as any).fetch = async (url: string, init?: RequestInit) => {
|
|
175
184
|
calls.push({ url: String(url), init })
|
|
176
185
|
const next = queue.shift()
|
|
177
186
|
assert.ok(next, 'unexpected fetch call')
|
|
178
|
-
return next
|
|
187
|
+
return next
|
|
179
188
|
}
|
|
180
189
|
|
|
181
190
|
const connector = {
|
|
@@ -191,7 +200,7 @@ test('bluebubbles sendMessage posts to message/text endpoint', async () => {
|
|
|
191
200
|
status: 'running',
|
|
192
201
|
createdAt: Date.now(),
|
|
193
202
|
updatedAt: Date.now(),
|
|
194
|
-
} as
|
|
203
|
+
} as unknown as Connector
|
|
195
204
|
|
|
196
205
|
const instance = await bluebubbles.start(connector, 'pw-test', async () => 'ok')
|
|
197
206
|
|
|
@@ -274,6 +274,7 @@ const bluebubbles: PlatformConnector = {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
const handlerKey = `__swarmclaw_bluebubbles_handler_${connector.id}__`
|
|
277
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
277
278
|
;(globalThis as any)[handlerKey] = processWebhookEvent
|
|
278
279
|
|
|
279
280
|
const pingUrl = resolveRequestUrl(serverUrl, '/api/v1/ping', password)
|
|
@@ -299,6 +300,7 @@ const bluebubbles: PlatformConnector = {
|
|
|
299
300
|
},
|
|
300
301
|
async stop() {
|
|
301
302
|
stopped = true
|
|
303
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
302
304
|
delete (globalThis as any)[handlerKey]
|
|
303
305
|
console.log(`[bluebubbles] Connector stopped`)
|
|
304
306
|
},
|
|
@@ -341,8 +343,9 @@ async function sendBlueBubblesText(params: {
|
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
try {
|
|
344
|
-
const body = await res.json() as
|
|
345
|
-
const
|
|
346
|
+
const body = await res.json() as Record<string, unknown>
|
|
347
|
+
const data = body?.data && typeof body.data === 'object' ? body.data as Record<string, unknown> : null
|
|
348
|
+
const id = data?.guid || body?.guid || data?.id || body?.id
|
|
346
349
|
return { messageId: typeof id === 'string' ? id : undefined }
|
|
347
350
|
} catch (err) {
|
|
348
351
|
// BlueBubbles may return empty body on success in some setups.
|
|
@@ -7,7 +7,7 @@ const googlechat: PlatformConnector = {
|
|
|
7
7
|
const { google } = await import(/* webpackIgnore: true */ pkg)
|
|
8
8
|
|
|
9
9
|
// Parse service account credentials from botToken
|
|
10
|
-
let credentials:
|
|
10
|
+
let credentials: Record<string, unknown>
|
|
11
11
|
try {
|
|
12
12
|
credentials = JSON.parse(botToken)
|
|
13
13
|
} catch {
|
|
@@ -41,28 +41,30 @@ const googlechat: PlatformConnector = {
|
|
|
41
41
|
return txt.replace(/<users\/[^>]+>/g, '').trim()
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function processWebhookEvent(event:
|
|
44
|
+
async function processWebhookEvent(event: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
45
45
|
if (stopped) throw new Error('Connector is stopped')
|
|
46
46
|
|
|
47
|
-
const msg = event?.message
|
|
47
|
+
const msg = event?.message as Record<string, unknown> | undefined
|
|
48
48
|
if (!msg) return {}
|
|
49
49
|
|
|
50
|
-
const
|
|
50
|
+
const msgSpace = msg?.space as Record<string, unknown> | undefined
|
|
51
|
+
const eventSpace = event?.space as Record<string, unknown> | undefined
|
|
52
|
+
const spaceName: string = (msgSpace?.name as string) || (eventSpace?.name as string) || ''
|
|
51
53
|
if (allowedSpaces && !allowedSpaces.some((s) => spaceName.includes(s))) {
|
|
52
54
|
return {}
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
const rawText = msg?.argumentText || msg?.text || ''
|
|
57
|
+
const rawText = (msg?.argumentText as string) || (msg?.text as string) || ''
|
|
56
58
|
const text = cleanInboundText(rawText)
|
|
57
59
|
if (!text) return {}
|
|
58
60
|
|
|
59
|
-
const sender = msg?.sender || event?.user || {}
|
|
60
|
-
const senderName = sender?.displayName || sender?.name || 'Google Chat User'
|
|
61
|
-
const senderId = sender?.name || ''
|
|
61
|
+
const sender = (msg?.sender || event?.user || {}) as Record<string, unknown>
|
|
62
|
+
const senderName = (sender?.displayName as string) || (sender?.name as string) || 'Google Chat User'
|
|
63
|
+
const senderId = (sender?.name as string) || ''
|
|
62
64
|
const inbound: InboundMessage = {
|
|
63
65
|
platform: 'googlechat',
|
|
64
|
-
channelId: spaceName || (msg?.thread?.name || 'space:unknown'
|
|
65
|
-
channelName:
|
|
66
|
+
channelId: spaceName || ((msg?.thread as Record<string, unknown>)?.name as string) || 'space:unknown',
|
|
67
|
+
channelName: (msgSpace?.displayName as string) || spaceName || 'Google Chat',
|
|
66
68
|
senderId,
|
|
67
69
|
senderName,
|
|
68
70
|
text,
|
|
@@ -73,6 +75,7 @@ const googlechat: PlatformConnector = {
|
|
|
73
75
|
return { text: response }
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
79
|
;(globalThis as any)[handlerKey] = processWebhookEvent
|
|
77
80
|
|
|
78
81
|
return {
|
|
@@ -95,6 +98,7 @@ const googlechat: PlatformConnector = {
|
|
|
95
98
|
},
|
|
96
99
|
async stop() {
|
|
97
100
|
stopped = true
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
98
102
|
delete (globalThis as any)[handlerKey]
|
|
99
103
|
console.log(`[googlechat] Bot disconnected`)
|
|
100
104
|
},
|
|
@@ -33,18 +33,25 @@ export function isNoMessage(text: string): boolean {
|
|
|
33
33
|
* Stored on globalThis to survive HMR reloads in dev mode —
|
|
34
34
|
* prevents duplicate sockets fighting for the same WhatsApp session. */
|
|
35
35
|
const globalKey = '__swarmclaw_running_connectors__' as const
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
const g = globalThis as any
|
|
36
38
|
const running: Map<string, ConnectorInstance> =
|
|
37
|
-
|
|
39
|
+
g[globalKey] ?? (g[globalKey] = new Map<string, ConnectorInstance>())
|
|
38
40
|
|
|
39
41
|
/** Most recent inbound channel per connector (used for proactive replies/default outbound target) */
|
|
40
42
|
const lastInboundKey = '__swarmclaw_connector_last_inbound__' as const
|
|
41
43
|
const lastInboundChannelByConnector: Map<string, string> =
|
|
42
|
-
|
|
44
|
+
g[lastInboundKey] ?? (g[lastInboundKey] = new Map<string, string>())
|
|
45
|
+
|
|
46
|
+
/** Last inbound message timestamp per connector (for presence indicators) */
|
|
47
|
+
const lastInboundTimeKey = '__swarmclaw_connector_last_inbound_time__' as const
|
|
48
|
+
const lastInboundTimeByConnector: Map<string, number> =
|
|
49
|
+
g[lastInboundTimeKey] ?? (g[lastInboundTimeKey] = new Map<string, number>())
|
|
43
50
|
|
|
44
51
|
/** Per-connector lock to prevent concurrent start/stop operations */
|
|
45
52
|
const lockKey = '__swarmclaw_connector_locks__' as const
|
|
46
53
|
const locks: Map<string, Promise<void>> =
|
|
47
|
-
|
|
54
|
+
g[lockKey] ?? (g[lockKey] = new Map<string, Promise<void>>())
|
|
48
55
|
|
|
49
56
|
/** Get platform implementation lazily */
|
|
50
57
|
export async function getPlatform(platform: string) {
|
|
@@ -117,14 +124,16 @@ function parseConnectorCommand(text: string): ParsedConnectorCommand | null {
|
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
function pushSessionMessage(session: Record<string, any>, role: 'user' | 'assistant', text: string): void {
|
|
121
129
|
if (!text.trim()) return
|
|
122
130
|
if (!Array.isArray(session.messages)) session.messages = []
|
|
123
131
|
session.messages.push({ role, text: text.trim(), time: Date.now() })
|
|
124
132
|
session.lastActiveAt = Date.now()
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
function persistSession(session: Record<string, any>): void {
|
|
128
137
|
const sessions = loadSessions()
|
|
129
138
|
sessions[session.id] = session
|
|
130
139
|
saveSessions(sessions)
|
|
@@ -278,7 +287,8 @@ function enforceInboundAccessPolicy(connector: Connector, msg: InboundMessage):
|
|
|
278
287
|
async function handleConnectorCommand(params: {
|
|
279
288
|
command: ParsedConnectorCommand
|
|
280
289
|
connector: Connector
|
|
281
|
-
|
|
290
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
291
|
+
session: Record<string, any>
|
|
282
292
|
msg: InboundMessage
|
|
283
293
|
agentName: string
|
|
284
294
|
}): Promise<string> {
|
|
@@ -303,8 +313,8 @@ async function handleConnectorCommand(params: {
|
|
|
303
313
|
|
|
304
314
|
if (command.name === 'status') {
|
|
305
315
|
const all = Array.isArray(session.messages) ? session.messages : []
|
|
306
|
-
const userCount = all.filter((m:
|
|
307
|
-
const assistantCount = all.filter((m:
|
|
316
|
+
const userCount = all.filter((m: { role?: string }) => m?.role === 'user').length
|
|
317
|
+
const assistantCount = all.filter((m: { role?: string }) => m?.role === 'assistant').length
|
|
308
318
|
const toolsCount = Array.isArray(session.tools) ? session.tools.length : 0
|
|
309
319
|
const statusText = [
|
|
310
320
|
`Status for ${connector.platform} / ${connector.name}:`,
|
|
@@ -399,6 +409,7 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
399
409
|
if (msg?.channelId) {
|
|
400
410
|
lastInboundChannelByConnector.set(connector.id, msg.channelId)
|
|
401
411
|
}
|
|
412
|
+
lastInboundTimeByConnector.set(connector.id, Date.now())
|
|
402
413
|
|
|
403
414
|
const agents = loadAgents()
|
|
404
415
|
const effectiveAgentId = msg.agentIdOverride || connector.agentId
|
|
@@ -408,6 +419,7 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
408
419
|
// Log connector trigger
|
|
409
420
|
const triggerSessionKey = `connector:${connector.id}:${msg.channelId}`
|
|
410
421
|
const allSessions = loadSessions()
|
|
422
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
411
423
|
const existingSession = Object.values(allSessions).find((s: any) => s.name === triggerSessionKey)
|
|
412
424
|
if (existingSession) {
|
|
413
425
|
logExecution(existingSession.id, 'trigger', `${msg.platform} message from ${msg.senderName}`, {
|
|
@@ -437,6 +449,7 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
437
449
|
// Find or create a session keyed by platform + channel
|
|
438
450
|
const sessionKey = `connector:${connector.id}:${msg.channelId}`
|
|
439
451
|
const sessions = loadSessions()
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
440
453
|
let session = Object.values(sessions).find((s: any) => s.name === sessionKey)
|
|
441
454
|
if (!session) {
|
|
442
455
|
const id = genId()
|
|
@@ -586,9 +599,10 @@ The test: would a thoughtful friend feel compelled to type something back? If no
|
|
|
586
599
|
// Use finalResponse for connectors — strips intermediate planning/tool-use text
|
|
587
600
|
fullText = result.finalResponse
|
|
588
601
|
console.log(`[connector] streamAgentChat returned ${result.fullText.length} chars total, ${fullText.length} chars final`)
|
|
589
|
-
} catch (err:
|
|
590
|
-
|
|
591
|
-
|
|
602
|
+
} catch (err: unknown) {
|
|
603
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
604
|
+
console.error(`[connector] streamAgentChat error:`, message)
|
|
605
|
+
return `[Error] ${message}`
|
|
592
606
|
}
|
|
593
607
|
} else {
|
|
594
608
|
// Use the provider directly
|
|
@@ -726,10 +740,10 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
726
740
|
notify('connectors')
|
|
727
741
|
|
|
728
742
|
console.log(`[connector] Started ${connector.platform} connector: ${connector.name}`)
|
|
729
|
-
} catch (err:
|
|
743
|
+
} catch (err: unknown) {
|
|
730
744
|
connector.status = 'error'
|
|
731
745
|
connector.isEnabled = false
|
|
732
|
-
connector.lastError = err.message
|
|
746
|
+
connector.lastError = err instanceof Error ? err.message : String(err)
|
|
733
747
|
connector.updatedAt = Date.now()
|
|
734
748
|
connectors[connectorId] = connector
|
|
735
749
|
saveConnectors(connectors)
|
|
@@ -818,8 +832,8 @@ export async function autoStartConnectors(): Promise<void> {
|
|
|
818
832
|
try {
|
|
819
833
|
console.log(`[connector] Auto-starting ${connector.platform} connector: ${connector.name}`)
|
|
820
834
|
await startConnector(connector.id)
|
|
821
|
-
} catch (err:
|
|
822
|
-
console.error(`[connector] Failed to auto-start ${connector.name}:`, err.message)
|
|
835
|
+
} catch (err: unknown) {
|
|
836
|
+
console.error(`[connector] Failed to auto-start ${connector.name}:`, err instanceof Error ? err.message : err)
|
|
823
837
|
}
|
|
824
838
|
}
|
|
825
839
|
}
|
|
@@ -878,6 +892,14 @@ export function getConnectorRecentChannelId(connectorId: string): string | null
|
|
|
878
892
|
return lastInboundChannelByConnector.get(connectorId) || null
|
|
879
893
|
}
|
|
880
894
|
|
|
895
|
+
/** Get presence info for a connector */
|
|
896
|
+
export function getConnectorPresence(connectorId: string): { lastMessageAt: number | null; channelId: string | null } {
|
|
897
|
+
return {
|
|
898
|
+
lastMessageAt: lastInboundTimeByConnector.get(connectorId) ?? null,
|
|
899
|
+
channelId: lastInboundChannelByConnector.get(connectorId) ?? null,
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
881
903
|
/** Get a running connector instance (internal use for rich messaging). */
|
|
882
904
|
export function getRunningInstance(connectorId: string): ConnectorInstance | undefined {
|
|
883
905
|
return running.get(connectorId)
|
|
@@ -830,6 +830,7 @@ const openclaw: PlatformConnector = {
|
|
|
830
830
|
// Cross-sync device token for provider identity resolution
|
|
831
831
|
if (normalized) {
|
|
832
832
|
try {
|
|
833
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
833
834
|
const { setSharedDeviceToken } = require('../openclaw-sync')
|
|
834
835
|
setSharedDeviceToken(normalized)
|
|
835
836
|
} catch { /* openclaw-sync not available */ }
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getConnectorStatus,
|
|
13
13
|
} from './connectors/manager'
|
|
14
14
|
import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus } from './heartbeat-service'
|
|
15
|
+
import { hasOpenClawAgents, ensureGatewayConnected, disconnectGateway, getGateway } from './openclaw-gateway'
|
|
15
16
|
|
|
16
17
|
const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
|
|
17
18
|
const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
|
|
@@ -179,6 +180,16 @@ function startQueueProcessor() {
|
|
|
179
180
|
await processNext()
|
|
180
181
|
ds.lastProcessedAt = Date.now()
|
|
181
182
|
}
|
|
183
|
+
// OpenClaw gateway lifecycle: lazy connect when openclaw agents exist, disconnect when none remain
|
|
184
|
+
try {
|
|
185
|
+
if (hasOpenClawAgents()) {
|
|
186
|
+
if (!getGateway()?.connected) {
|
|
187
|
+
await ensureGatewayConnected()
|
|
188
|
+
}
|
|
189
|
+
} else if (getGateway()?.connected) {
|
|
190
|
+
disconnectGateway()
|
|
191
|
+
}
|
|
192
|
+
} catch { /* gateway errors are non-fatal */ }
|
|
182
193
|
}, QUEUE_CHECK_INTERVAL)
|
|
183
194
|
}
|
|
184
195
|
|
|
@@ -4,14 +4,13 @@ import type { GoalContract, MessageToolEvent } from '@/types'
|
|
|
4
4
|
import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks } from './storage'
|
|
5
5
|
import { log } from './logger'
|
|
6
6
|
import { getMemoryDb } from './memory-db'
|
|
7
|
+
import { isProtectedMainSession } from './main-session'
|
|
7
8
|
import {
|
|
8
9
|
mergeGoalContracts,
|
|
9
10
|
parseGoalContractFromText,
|
|
10
11
|
parseMainLoopPlan,
|
|
11
12
|
parseMainLoopReview,
|
|
12
13
|
} from './autonomy-contract'
|
|
13
|
-
|
|
14
|
-
const MAIN_SESSION_NAME = '__main__'
|
|
15
14
|
const MAX_PENDING_EVENTS = 40
|
|
16
15
|
const MAX_TIMELINE_EVENTS = 80
|
|
17
16
|
const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
@@ -669,7 +668,7 @@ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: bool
|
|
|
669
668
|
}
|
|
670
669
|
|
|
671
670
|
export function isMainSession(session: any): boolean {
|
|
672
|
-
return session
|
|
671
|
+
return isProtectedMainSession(session)
|
|
673
672
|
}
|
|
674
673
|
|
|
675
674
|
export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const MAIN_SESSION_NAME = '__main__'
|
|
2
|
+
|
|
3
|
+
export function isProtectedMainSession(session: any): boolean {
|
|
4
|
+
if (!session || typeof session !== 'object') return false
|
|
5
|
+
if (session.mainSession === true) return true
|
|
6
|
+
|
|
7
|
+
const name = typeof session.name === 'string' ? session.name.trim() : ''
|
|
8
|
+
if (name === MAIN_SESSION_NAME) return true
|
|
9
|
+
|
|
10
|
+
const id = typeof session.id === 'string' ? session.id.trim() : ''
|
|
11
|
+
if (id.startsWith('main-')) return true
|
|
12
|
+
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ensureMainSessionFlag(session: any): void {
|
|
17
|
+
if (!session || typeof session !== 'object') return
|
|
18
|
+
if (isProtectedMainSession(session)) {
|
|
19
|
+
session.mainSession = true
|
|
20
|
+
}
|
|
21
|
+
}
|