@swarmclawai/swarmclaw 0.6.0 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +56 -42
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +16 -35
  15. package/src/app/api/tts/stream/route.ts +14 -42
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +76 -24
  31. package/src/components/chat/chat-header.tsx +522 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +113 -8
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +84 -17
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +125 -14
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/connector-routing.test.ts +118 -1
  78. package/src/lib/server/connectors/discord.ts +31 -8
  79. package/src/lib/server/connectors/manager.ts +594 -16
  80. package/src/lib/server/connectors/media.ts +5 -0
  81. package/src/lib/server/connectors/telegram.ts +12 -2
  82. package/src/lib/server/connectors/types.ts +2 -0
  83. package/src/lib/server/connectors/whatsapp.ts +28 -2
  84. package/src/lib/server/elevenlabs.test.ts +60 -0
  85. package/src/lib/server/elevenlabs.ts +103 -0
  86. package/src/lib/server/heartbeat-service.ts +8 -1
  87. package/src/lib/server/main-agent-loop.ts +1 -1
  88. package/src/lib/server/memory-consolidation.ts +15 -2
  89. package/src/lib/server/memory-db.ts +134 -6
  90. package/src/lib/server/mime.ts +51 -0
  91. package/src/lib/server/openclaw-gateway.ts +2 -2
  92. package/src/lib/server/orchestrator-lg.ts +2 -0
  93. package/src/lib/server/orchestrator.ts +5 -2
  94. package/src/lib/server/playwright-proxy.mjs +2 -3
  95. package/src/lib/server/prompt-runtime-context.ts +53 -0
  96. package/src/lib/server/queue.ts +182 -8
  97. package/src/lib/server/session-tools/canvas.ts +67 -0
  98. package/src/lib/server/session-tools/connector.ts +583 -63
  99. package/src/lib/server/session-tools/crud.ts +21 -0
  100. package/src/lib/server/session-tools/delegate.ts +68 -4
  101. package/src/lib/server/session-tools/file.ts +26 -7
  102. package/src/lib/server/session-tools/git.ts +71 -0
  103. package/src/lib/server/session-tools/http.ts +57 -0
  104. package/src/lib/server/session-tools/index.ts +8 -0
  105. package/src/lib/server/session-tools/memory.ts +1 -0
  106. package/src/lib/server/session-tools/search-providers.ts +16 -8
  107. package/src/lib/server/session-tools/subagent.ts +106 -0
  108. package/src/lib/server/session-tools/web.ts +118 -8
  109. package/src/lib/server/stream-agent-chat.ts +39 -10
  110. package/src/lib/server/task-mention.ts +41 -0
  111. package/src/lib/sessions.ts +10 -0
  112. package/src/lib/soul-library.ts +103 -0
  113. package/src/lib/task-dedupe.ts +26 -0
  114. package/src/lib/tool-definitions.ts +2 -0
  115. package/src/lib/tts.ts +2 -2
  116. package/src/stores/use-app-store.ts +5 -1
  117. package/src/stores/use-chat-store.ts +65 -2
  118. package/src/types/index.ts +32 -2
@@ -0,0 +1,146 @@
1
+ import { loadSettings, loadSkills, loadCredentials, decryptKey } from './storage'
2
+ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
3
+ import type { Chatroom, Agent, Session, Message } from '@/types'
4
+
5
+ /** Resolve API key from an agent's credentialId */
6
+ export function resolveApiKey(credentialId: string | null | undefined): string | null {
7
+ if (!credentialId) return null
8
+ const creds = loadCredentials()
9
+ const cred = creds[credentialId]
10
+ if (!cred?.encryptedKey) return null
11
+ try { return decryptKey(cred.encryptedKey) } catch { return null }
12
+ }
13
+
14
+ /** Parse @mentions from message text, returns matching agentIds */
15
+ export function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
16
+ if (/@all\b/i.test(text)) return [...memberIds]
17
+ const mentionPattern = /@(\S+)/g
18
+ const mentioned: string[] = []
19
+ let match: RegExpExecArray | null
20
+ while ((match = mentionPattern.exec(text)) !== null) {
21
+ const name = match[1].toLowerCase()
22
+ for (const id of memberIds) {
23
+ const agent = agents[id]
24
+ if (agent && agent.name.toLowerCase().replace(/\s+/g, '') === name) {
25
+ if (!mentioned.includes(id)) mentioned.push(id)
26
+ }
27
+ }
28
+ }
29
+ return mentioned
30
+ }
31
+
32
+ /** Build chatroom context as a system prompt addendum with agent profiles and collaboration guidelines */
33
+ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<string, Agent>, agentId: string): string {
34
+ const selfAgent = agents[agentId]
35
+ const selfName = selfAgent?.name || agentId
36
+
37
+ // Build team profiles with capabilities
38
+ const teamProfiles = chatroom.agentIds
39
+ .filter((id) => id !== agentId)
40
+ .map((id) => {
41
+ const a = agents[id]
42
+ if (!a) return null
43
+ const tools = a.tools?.length ? `Tools: ${a.tools.join(', ')}` : 'No specialized tools'
44
+ const desc = a.description || a.soul || 'No description'
45
+ return `- **${a.name}**: ${desc}\n ${tools}`
46
+ })
47
+ .filter(Boolean)
48
+ .join('\n')
49
+
50
+ const recentMessages = chatroom.messages.slice(-30).map((m) => {
51
+ return `[${m.senderName}]: ${m.text}`
52
+ }).join('\n')
53
+
54
+ const memberCount = chatroom.agentIds.length
55
+ const otherNames = chatroom.agentIds
56
+ .filter((id) => id !== agentId)
57
+ .map((id) => agents[id]?.name)
58
+ .filter(Boolean)
59
+
60
+ return [
61
+ `## Chatroom Context`,
62
+ `You are **${selfName}** in a group chatroom called "${chatroom.name}" with ${memberCount} participants (you, ${otherNames.join(', ') || 'others'}, and the user).`,
63
+ selfAgent?.description ? `Your role: ${selfAgent.description}` : '',
64
+ selfAgent?.tools?.length ? `Your available tools: ${selfAgent.tools.join(', ')}` : '',
65
+ '',
66
+ '## Team Members',
67
+ teamProfiles || '(no other agents)',
68
+ '',
69
+ '## How to Behave in This Chatroom',
70
+ '- **You are in a group chat.** Talk like you are in a real-time conversation with teammates — be direct, casual, and concise.',
71
+ '- **Be yourself.** Respond with personality. Don\'t give generic "let me know if you need anything" responses. Actually engage with what was said.',
72
+ '- **Answer the question or react to the message.** If someone says "how are you doing?" just answer naturally. If someone asks a question you can help with, help directly.',
73
+ '- **Keep responses short** unless depth is needed. A few sentences is usually enough. This is a chat, not an essay.',
74
+ '- **@mention teammates** only when you genuinely need their specific expertise. Don\'t tag people just to be polite.',
75
+ '- **Don\'t narrate your capabilities** unless asked. Just demonstrate them by doing things.',
76
+ '- **Read the room.** Look at recent messages to understand context. Don\'t repeat what others already said.',
77
+ '',
78
+ '## Recent Messages',
79
+ recentMessages || '(no messages yet)',
80
+ ].filter((line) => line !== undefined).join('\n')
81
+ }
82
+
83
+ /** Build a synthetic session object for an agent in a chatroom */
84
+ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
85
+ return {
86
+ id: `chatroom-${chatroomId}-${agent.id}`,
87
+ name: `Chatroom session for ${agent.name}`,
88
+ cwd: process.cwd(),
89
+ user: 'chatroom',
90
+ provider: agent.provider,
91
+ model: agent.model,
92
+ credentialId: agent.credentialId ?? null,
93
+ fallbackCredentialIds: agent.fallbackCredentialIds,
94
+ apiEndpoint: agent.apiEndpoint ?? null,
95
+ claudeSessionId: null,
96
+ messages: [],
97
+ createdAt: Date.now(),
98
+ lastActiveAt: Date.now(),
99
+ tools: agent.tools || [],
100
+ agentId: agent.id,
101
+ }
102
+ }
103
+
104
+ /** Build agent's system prompt including skills */
105
+ export function buildAgentSystemPromptForChatroom(agent: Agent): string {
106
+ const settings = loadSettings()
107
+ const parts: string[] = []
108
+ if (settings.userPrompt) parts.push(settings.userPrompt)
109
+ parts.push(buildCurrentDateTimePromptContext())
110
+ if (agent.soul) parts.push(agent.soul)
111
+ if (agent.systemPrompt) parts.push(agent.systemPrompt)
112
+ if (agent.skillIds?.length) {
113
+ const allSkills = loadSkills()
114
+ for (const skillId of agent.skillIds) {
115
+ const skill = allSkills[skillId]
116
+ if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
117
+ }
118
+ }
119
+ return parts.join('\n\n')
120
+ }
121
+
122
+ /** Convert chatroom messages to Message history format for LLM */
123
+ export function buildHistoryForAgent(chatroom: Chatroom, agentId: string, imagePath?: string, attachedFiles?: string[]): Message[] {
124
+ const history = chatroom.messages.slice(-50).map((m) => {
125
+ let msgText = `[${m.senderName}]: ${m.text}`
126
+ // Include attachment info in history
127
+ if (m.attachedFiles?.length) {
128
+ const names = m.attachedFiles.map((f) => f.split('/').pop()).join(', ')
129
+ msgText += `\n[Attached: ${names}]`
130
+ }
131
+ return {
132
+ role: m.senderId === agentId ? 'assistant' as const : 'user' as const,
133
+ text: msgText,
134
+ time: m.time,
135
+ ...(m.imagePath ? { imagePath: m.imagePath } : {}),
136
+ ...(m.attachedFiles ? { attachedFiles: m.attachedFiles } : {}),
137
+ }
138
+ })
139
+ // Pass through imagePath/attachedFiles from the current message to the last history entry
140
+ if (history.length > 0 && (imagePath || attachedFiles)) {
141
+ const last = history[history.length - 1]
142
+ if (imagePath && !last.imagePath) last.imagePath = imagePath
143
+ if (attachedFiles && !last.attachedFiles) last.attachedFiles = attachedFiles
144
+ }
145
+ return history
146
+ }
@@ -1,9 +1,12 @@
1
1
  import { describe, it } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import { getPlatform, isNoMessage, formatMediaLine, formatInboundUserText } from './manager.ts'
3
+ import { getPlatform, isNoMessage, formatMediaLine, formatInboundUserText, extractEmbeddedMedia, selectOutboundMediaFiles } from './manager.ts'
4
4
  import { handleSignalEvent } from './signal.ts'
5
5
  import type { PlatformConnector } from './types.ts'
6
6
  import type { InboundMessage, InboundMedia } from './types.ts'
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { UPLOAD_DIR } from '../storage'
7
10
 
8
11
  // ---------------------------------------------------------------------------
9
12
  // 1. Connector module resolution (getPlatform)
@@ -241,3 +244,117 @@ describe('formatInboundUserText', () => {
241
244
  assert.ok(result.includes('...and 2 more attachment(s)'))
242
245
  })
243
246
  })
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // 6. extractEmbeddedMedia
250
+ // ---------------------------------------------------------------------------
251
+ describe('extractEmbeddedMedia', () => {
252
+ it('extracts markdown image and file links for uploaded assets', async () => {
253
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true })
254
+ const token = `test-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
255
+ const imgName = `${token}-foo.png`
256
+ const pdfName = `${token}-report.pdf`
257
+ const img = path.join(UPLOAD_DIR, imgName)
258
+ const pdf = path.join(UPLOAD_DIR, pdfName)
259
+ fs.writeFileSync(img, 'img')
260
+ fs.writeFileSync(pdf, 'pdf')
261
+
262
+ try {
263
+ const input = [
264
+ 'Here you go:',
265
+ `![chart](/api/uploads/${imgName})`,
266
+ `[Report](/api/uploads/${pdfName})`,
267
+ ].join('\n')
268
+
269
+ const out = extractEmbeddedMedia(input)
270
+ assert.equal(out.files.length, 2)
271
+ assert.equal(out.files[0].path, img)
272
+ assert.equal(out.files[0].alt, 'chart')
273
+ assert.equal(out.files[1].path, pdf)
274
+ assert.equal(out.files[1].alt, 'Report')
275
+ assert.equal(out.cleanText, 'Here you go:')
276
+ } finally {
277
+ fs.rmSync(img, { force: true })
278
+ fs.rmSync(pdf, { force: true })
279
+ }
280
+ })
281
+
282
+ it('extracts bare /api/uploads URLs and de-duplicates duplicate references', async () => {
283
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true })
284
+ const token = `test-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
285
+ const pdfName = `${token}-duplicate.pdf`
286
+ const pdf = path.join(UPLOAD_DIR, pdfName)
287
+ fs.writeFileSync(pdf, 'pdf')
288
+ try {
289
+ const input = [
290
+ `File: /api/uploads/${pdfName}`,
291
+ `[Again](/api/uploads/${pdfName})`,
292
+ ].join('\n')
293
+ const out = extractEmbeddedMedia(input)
294
+ assert.equal(out.files.length, 1)
295
+ assert.equal(out.files[0].path, pdf)
296
+ assert.equal(out.cleanText, 'File:')
297
+ } finally {
298
+ fs.rmSync(pdf, { force: true })
299
+ }
300
+ })
301
+ })
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // 7. selectOutboundMediaFiles
305
+ // ---------------------------------------------------------------------------
306
+ describe('selectOutboundMediaFiles', () => {
307
+ it('deduplicates browser/screenshot variants and selects one file by default', () => {
308
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true })
309
+ const ts = Date.now()
310
+ const browserPng = path.join(UPLOAD_DIR, `browser-${ts}.png`)
311
+ const screenshotPng = path.join(UPLOAD_DIR, `screenshot-${ts + 1}.png`)
312
+ const finalPng = path.join(UPLOAD_DIR, `${Date.now()}-wikipedia_screenshot.png`)
313
+ fs.writeFileSync(browserPng, 'browser')
314
+ fs.writeFileSync(screenshotPng, 'shot')
315
+ fs.writeFileSync(finalPng, 'final')
316
+ try {
317
+ const selected = selectOutboundMediaFiles(
318
+ [
319
+ { path: browserPng, alt: 'Screenshot' },
320
+ { path: screenshotPng, alt: 'Screenshot' },
321
+ { path: finalPng, alt: 'wikipedia_screenshot.png' },
322
+ ],
323
+ 'Can you send me a screenshot of Wikipedia?',
324
+ )
325
+ assert.equal(selected.length, 1)
326
+ assert.equal(selected[0].path, finalPng)
327
+ } finally {
328
+ fs.rmSync(browserPng, { force: true })
329
+ fs.rmSync(screenshotPng, { force: true })
330
+ fs.rmSync(finalPng, { force: true })
331
+ }
332
+ })
333
+
334
+ it('allows multiple files only when the user explicitly asks for many', () => {
335
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true })
336
+ const ts = Date.now()
337
+ const browserPng = path.join(UPLOAD_DIR, `browser-${ts}.png`)
338
+ const screenshotPng = path.join(UPLOAD_DIR, `screenshot-${ts + 1}.png`)
339
+ const pdf = path.join(UPLOAD_DIR, `${Date.now()}-report.pdf`)
340
+ fs.writeFileSync(browserPng, 'browser')
341
+ fs.writeFileSync(screenshotPng, 'shot')
342
+ fs.writeFileSync(pdf, 'pdf')
343
+ try {
344
+ const selected = selectOutboundMediaFiles(
345
+ [
346
+ { path: browserPng, alt: 'Screenshot' },
347
+ { path: screenshotPng, alt: 'Screenshot' },
348
+ { path: pdf, alt: 'Report' },
349
+ ],
350
+ 'Send both screenshots and the PDF',
351
+ )
352
+ assert.equal(selected.length, 2)
353
+ assert.deepEqual(selected.map((f) => path.basename(f.path)).sort(), [path.basename(browserPng), path.basename(pdf)].sort())
354
+ } finally {
355
+ fs.rmSync(browserPng, { force: true })
356
+ fs.rmSync(screenshotPng, { force: true })
357
+ fs.rmSync(pdf, { force: true })
358
+ }
359
+ })
360
+ })
@@ -3,7 +3,7 @@ import fs from 'fs'
3
3
  import path from 'path'
4
4
  import type { Connector } from '@/types'
5
5
  import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
6
- import { inferInboundMediaType, mimeFromPath, isImageMime } from './media'
6
+ import { downloadInboundMediaToUpload, inferInboundMediaType } from './media'
7
7
  import { isNoMessage } from './manager'
8
8
 
9
9
  const discord: PlatformConnector = {
@@ -32,13 +32,36 @@ const discord: PlatformConnector = {
32
32
  if (allowedChannels && !allowedChannels.includes(message.channelId)) return
33
33
 
34
34
  const attachmentList = Array.from(message.attachments.values())
35
- const media = attachmentList.map((a) => ({
36
- type: inferInboundMediaType(a.contentType || undefined, a.name || undefined),
37
- fileName: a.name || undefined,
38
- mimeType: a.contentType || undefined,
39
- sizeBytes: a.size || undefined,
40
- url: a.url || undefined,
41
- }))
35
+ const media: NonNullable<InboundMessage['media']> = []
36
+ for (const attachment of attachmentList) {
37
+ const mediaType = inferInboundMediaType(attachment.contentType || undefined, attachment.name || undefined)
38
+ const sourceUrl = attachment.url || undefined
39
+ if (sourceUrl) {
40
+ try {
41
+ const stored = await downloadInboundMediaToUpload({
42
+ connectorId: connector.id,
43
+ mediaType,
44
+ url: sourceUrl,
45
+ fileName: attachment.name || undefined,
46
+ mimeType: attachment.contentType || undefined,
47
+ })
48
+ if (stored) {
49
+ media.push(stored)
50
+ continue
51
+ }
52
+ } catch (err: unknown) {
53
+ const errMsg = err instanceof Error ? err.message : String(err)
54
+ console.warn(`[discord] Media download failed (${attachment.name || 'file'}):`, errMsg)
55
+ }
56
+ }
57
+ media.push({
58
+ type: mediaType,
59
+ fileName: attachment.name || undefined,
60
+ mimeType: attachment.contentType || undefined,
61
+ sizeBytes: attachment.size || undefined,
62
+ url: sourceUrl,
63
+ })
64
+ }
42
65
  const firstImage = media.find((m) => m.type === 'image' && m.url)
43
66
 
44
67
  const inbound: InboundMessage = {