@swarmclawai/swarmclaw 0.6.3 → 0.6.6

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 (106) hide show
  1. package/README.md +5 -3
  2. package/package.json +5 -1
  3. package/src/app/api/chatrooms/[id]/chat/route.ts +41 -2
  4. package/src/app/api/chatrooms/[id]/route.ts +15 -1
  5. package/src/app/api/chatrooms/route.ts +15 -2
  6. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  7. package/src/app/api/tasks/route.ts +24 -0
  8. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  9. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  10. package/src/app/api/wallets/[id]/route.ts +118 -0
  11. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  12. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  13. package/src/app/api/wallets/route.ts +74 -0
  14. package/src/app/globals.css +8 -0
  15. package/src/app/page.tsx +7 -3
  16. package/src/cli/index.js +15 -0
  17. package/src/cli/spec.js +14 -0
  18. package/src/components/agents/agent-avatar.tsx +15 -1
  19. package/src/components/agents/agent-card.tsx +1 -0
  20. package/src/components/agents/agent-chat-list.tsx +1 -1
  21. package/src/components/agents/agent-sheet.tsx +112 -26
  22. package/src/components/auth/access-key-gate.tsx +22 -11
  23. package/src/components/chat/chat-area.tsx +2 -2
  24. package/src/components/chat/chat-header.tsx +48 -19
  25. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  26. package/src/components/chat/delegation-banner.test.ts +27 -0
  27. package/src/components/chat/delegation-banner.tsx +109 -23
  28. package/src/components/chat/message-bubble.tsx +14 -3
  29. package/src/components/chat/message-list.tsx +5 -4
  30. package/src/components/chat/streaming-bubble.tsx +3 -2
  31. package/src/components/chat/thinking-indicator.tsx +3 -2
  32. package/src/components/chat/tool-call-bubble.test.ts +28 -0
  33. package/src/components/chat/tool-call-bubble.tsx +13 -1
  34. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  35. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  36. package/src/components/chatrooms/chatroom-input.tsx +7 -6
  37. package/src/components/chatrooms/chatroom-message.tsx +1 -1
  38. package/src/components/chatrooms/chatroom-sheet.tsx +1 -1
  39. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  40. package/src/components/chatrooms/chatroom-view.tsx +1 -1
  41. package/src/components/connectors/connector-list.tsx +1 -1
  42. package/src/components/home/home-view.tsx +2 -1
  43. package/src/components/input/chat-input.tsx +5 -4
  44. package/src/components/knowledge/knowledge-list.tsx +1 -1
  45. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  46. package/src/components/layout/app-layout.tsx +23 -9
  47. package/src/components/logs/log-list.tsx +7 -7
  48. package/src/components/memory/memory-agent-list.tsx +1 -1
  49. package/src/components/memory/memory-browser.tsx +1 -0
  50. package/src/components/memory/memory-card.tsx +3 -2
  51. package/src/components/memory/memory-detail.tsx +3 -3
  52. package/src/components/memory/memory-sheet.tsx +2 -2
  53. package/src/components/projects/project-detail.tsx +4 -4
  54. package/src/components/secrets/secret-sheet.tsx +1 -1
  55. package/src/components/secrets/secrets-list.tsx +1 -1
  56. package/src/components/sessions/new-session-sheet.tsx +4 -3
  57. package/src/components/sessions/session-card.tsx +1 -1
  58. package/src/components/shared/agent-picker-list.tsx +1 -1
  59. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  60. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  61. package/src/components/skills/skill-list.tsx +1 -1
  62. package/src/components/skills/skill-sheet.tsx +1 -1
  63. package/src/components/tasks/task-board.tsx +3 -3
  64. package/src/components/tasks/task-sheet.tsx +21 -1
  65. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  66. package/src/components/wallets/wallet-panel.tsx +616 -0
  67. package/src/components/wallets/wallet-section.tsx +100 -0
  68. package/src/hooks/use-media-query.ts +30 -4
  69. package/src/lib/api-client.ts +6 -18
  70. package/src/lib/fetch-timeout.ts +17 -0
  71. package/src/lib/notification-sounds.ts +4 -4
  72. package/src/lib/safe-storage.ts +42 -0
  73. package/src/lib/server/agent-registry.ts +2 -2
  74. package/src/lib/server/chat-execution.ts +35 -3
  75. package/src/lib/server/chatroom-health.ts +60 -0
  76. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  77. package/src/lib/server/chatroom-helpers.ts +64 -11
  78. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  79. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  80. package/src/lib/server/connectors/manager.ts +80 -2
  81. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  82. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  83. package/src/lib/server/connectors/whatsapp.ts +8 -5
  84. package/src/lib/server/orchestrator-lg.ts +12 -2
  85. package/src/lib/server/orchestrator.ts +6 -1
  86. package/src/lib/server/queue-followups.test.ts +224 -0
  87. package/src/lib/server/queue.ts +226 -24
  88. package/src/lib/server/scheduler.ts +3 -0
  89. package/src/lib/server/session-tools/chatroom.ts +11 -2
  90. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  91. package/src/lib/server/session-tools/index.ts +6 -2
  92. package/src/lib/server/session-tools/memory.ts +1 -1
  93. package/src/lib/server/session-tools/shell.ts +1 -1
  94. package/src/lib/server/session-tools/wallet.ts +124 -0
  95. package/src/lib/server/session-tools/web-output.test.ts +29 -0
  96. package/src/lib/server/session-tools/web-output.ts +16 -0
  97. package/src/lib/server/session-tools/web.ts +7 -3
  98. package/src/lib/server/solana.ts +122 -0
  99. package/src/lib/server/storage.ts +38 -0
  100. package/src/lib/server/stream-agent-chat.ts +126 -63
  101. package/src/lib/server/task-mention.test.ts +41 -0
  102. package/src/lib/server/task-mention.ts +3 -2
  103. package/src/lib/tool-definitions.ts +1 -0
  104. package/src/lib/view-routes.ts +6 -1
  105. package/src/stores/use-app-store.ts +17 -11
  106. package/src/types/index.ts +60 -1
@@ -0,0 +1,26 @@
1
+ import removeMarkdown from 'remove-markdown'
2
+
3
+ export function stripMarkdownForPlainChat(raw: string): string {
4
+ const source = String(raw || '').replace(/\r\n?/g, '\n')
5
+ if (!source) return ''
6
+
7
+ let text = removeMarkdown(source, {
8
+ gfm: true,
9
+ useImgAltText: true,
10
+ replaceLinksWithURL: true,
11
+ separateLinksAndTexts: ': ',
12
+ })
13
+
14
+ // Collapse duplicate "url: url" patterns when link label already equals URL.
15
+ text = text.replace(/(https?:\/\/[^\s]+): \1/g, '$1')
16
+ text = text.replace(/\n{3,}/g, '\n\n')
17
+ return text.trim()
18
+ }
19
+
20
+ /**
21
+ * Convert markdown-heavy model output into WhatsApp-friendly plain text.
22
+ * Uses a markdown parser package instead of ad-hoc regex-only stripping.
23
+ */
24
+ export function formatTextForWhatsApp(raw: string): string {
25
+ return stripMarkdownForPlainChat(raw)
26
+ }
@@ -12,6 +12,7 @@ import type { Connector } from '@/types'
12
12
  import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
13
13
  import { saveInboundMediaBuffer, mimeFromPath, isImageMime, isAudioMime } from './media'
14
14
  import { isNoMessage } from './manager'
15
+ import { formatTextForWhatsApp } from './whatsapp-text'
15
16
 
16
17
  import { DATA_DIR } from '../data-dir'
17
18
 
@@ -67,15 +68,17 @@ const whatsapp: PlatformConnector = {
67
68
  hasCredentials: hasStoredCreds(authDir),
68
69
  async sendMessage(channelId, text, options) {
69
70
  if (!sock) throw new Error('WhatsApp connector is not connected')
71
+ const normalizedText = formatTextForWhatsApp(text || '')
72
+ const normalizedCaption = formatTextForWhatsApp(options?.caption || normalizedText)
70
73
  // Local file path takes priority
71
74
  if (options?.mediaPath) {
72
75
  if (!fs.existsSync(options.mediaPath)) throw new Error(`File not found: ${options.mediaPath}`)
73
76
  const buf = fs.readFileSync(options.mediaPath)
74
77
  const mime = options.mimeType || mimeFromPath(options.mediaPath)
75
- const caption = options.caption || text || undefined
78
+ const caption = normalizedCaption || undefined
76
79
  const fName = options.fileName || path.basename(options.mediaPath)
77
80
  let sent
78
- if (isImageMime(mime)) {
81
+ if (isImageMime(mime) || mime.startsWith('video/')) {
79
82
  try {
80
83
  sent = await sock.sendMessage(channelId, { image: buf, caption, mimetype: mime })
81
84
  } catch (err: unknown) {
@@ -94,7 +97,7 @@ const whatsapp: PlatformConnector = {
94
97
  if (options?.imageUrl) {
95
98
  const sent = await sock.sendMessage(channelId, {
96
99
  image: { url: options.imageUrl },
97
- caption: options.caption || text || undefined,
100
+ caption: normalizedCaption || undefined,
98
101
  })
99
102
  if (sent?.key?.id) sentMessageIds.add(sent.key.id)
100
103
  return { messageId: sent?.key?.id || undefined }
@@ -104,13 +107,13 @@ const whatsapp: PlatformConnector = {
104
107
  document: { url: options.fileUrl },
105
108
  fileName: options.fileName || 'attachment',
106
109
  mimetype: options.mimeType || 'application/octet-stream',
107
- caption: options.caption || text || undefined,
110
+ caption: normalizedCaption || undefined,
108
111
  })
109
112
  if (sent?.key?.id) sentMessageIds.add(sent.key.id)
110
113
  return { messageId: sent?.key?.id || undefined }
111
114
  }
112
115
 
113
- const payload = text || options?.caption || ''
116
+ const payload = normalizedText || normalizedCaption || ''
114
117
  const chunks = payload.length <= 4096 ? [payload] : (payload.match(/[\s\S]{1,4000}/g) || [payload])
115
118
  let lastMessageId: string | undefined
116
119
  for (const chunk of chunks) {
@@ -124,7 +124,12 @@ async function executeSubTaskViaCli(agent: Agent, task: string, parentSessionId:
124
124
  }
125
125
  ss(sessions)
126
126
 
127
- const result = await callProvider(agent, agent.systemPrompt, [{ role: 'user', text: task }])
127
+ // Build system prompt with identity
128
+ const subPromptParts: string[] = []
129
+ subPromptParts.push(`## My Identity\nMy name is ${agent.name}.${agent.description ? ' ' + agent.description : ''} I should always refer to myself by this name.`)
130
+ if (agent.soul) subPromptParts.push(agent.soul)
131
+ if (agent.systemPrompt) subPromptParts.push(agent.systemPrompt)
132
+ const result = await callProvider(agent, subPromptParts.join('\n\n'), [{ role: 'user', text: task }])
128
133
 
129
134
  const s2 = ls()
130
135
  if (s2[childId]) {
@@ -348,9 +353,14 @@ export async function executeLangGraphOrchestrator(
348
353
  apiKey: engine.apiKey,
349
354
  apiEndpoint: engine.apiEndpoint,
350
355
  })
351
- // Build system message: [userPrompt] \n\n [soul] \n\n [systemPrompt] \n\n [orchestrator context]
356
+ // Build system message: [identity] \n\n [userPrompt] \n\n [soul] \n\n [systemPrompt] \n\n [orchestrator context]
352
357
  const settings = loadSettings()
353
358
  const promptParts: string[] = []
359
+ // Identity block
360
+ const orchIdentity = [`## My Identity`, `My name is ${orchestrator.name}.`]
361
+ if (orchestrator.description) orchIdentity.push(orchestrator.description)
362
+ orchIdentity.push('I should always refer to myself by this name.')
363
+ promptParts.push(orchIdentity.join(' '))
354
364
  if (settings.userPrompt) promptParts.push(settings.userPrompt)
355
365
  promptParts.push(buildCurrentDateTimePromptContext())
356
366
  if (orchestrator.soul) promptParts.push(orchestrator.soul)
@@ -296,7 +296,12 @@ async function executeSubTask(
296
296
  saveSessions(sessions)
297
297
 
298
298
  const history = [{ role: 'user', text: task }]
299
- const result = await callProvider(agent, agent.systemPrompt, history)
299
+ // Build system prompt with identity so the agent knows who it is
300
+ const promptParts: string[] = []
301
+ promptParts.push(`## My Identity\nMy name is ${agent.name}.${agent.description ? ' ' + agent.description : ''} I should always refer to myself by this name.`)
302
+ if (agent.soul) promptParts.push(agent.soul)
303
+ if (agent.systemPrompt) promptParts.push(agent.systemPrompt)
304
+ const result = await callProvider(agent, promptParts.join('\n\n'), history)
300
305
 
301
306
  childSession.messages.push({ role: 'user', text: task, time: Date.now() })
302
307
  childSession.messages.push({ role: 'assistant', text: result, time: Date.now() })
@@ -0,0 +1,224 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { BoardTask } from '@/types'
4
+ import { resolveTaskOriginConnectorFollowupTarget } from './queue'
5
+
6
+ function makeTask(partial?: Partial<BoardTask> & { createdInSessionId?: string | null }): BoardTask {
7
+ const now = Date.now()
8
+ return {
9
+ id: 'task-1',
10
+ title: 'Test task',
11
+ description: 'desc',
12
+ status: 'queued',
13
+ agentId: 'agent-a',
14
+ createdAt: now,
15
+ updatedAt: now,
16
+ ...(partial || {}),
17
+ } as BoardTask
18
+ }
19
+
20
+ type SessionFixtureMap = Record<string, {
21
+ messages: Array<{
22
+ role: string
23
+ text?: string
24
+ source?: {
25
+ connectorId?: string
26
+ channelId?: string
27
+ }
28
+ }>
29
+ }>
30
+
31
+ describe('resolveTaskOriginConnectorFollowupTarget', () => {
32
+ it('uses connector source channel from origin session and normalizes WhatsApp numbers', () => {
33
+ const task = makeTask({ createdInSessionId: 'session-1' })
34
+ const sessions = {
35
+ 'session-1': {
36
+ messages: [
37
+ { role: 'assistant', text: 'ok' },
38
+ {
39
+ role: 'user',
40
+ text: 'please update me',
41
+ source: {
42
+ connectorId: 'conn-wa',
43
+ channelId: '+44 7700 900123',
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ }
49
+ const connectors = {
50
+ 'conn-wa': {
51
+ id: 'conn-wa',
52
+ platform: 'whatsapp',
53
+ agentId: 'agent-a',
54
+ config: {},
55
+ },
56
+ }
57
+ const running = [
58
+ {
59
+ id: 'conn-wa',
60
+ platform: 'whatsapp',
61
+ agentId: 'agent-a',
62
+ supportsSend: true,
63
+ configuredTargets: [],
64
+ recentChannelId: '185200000000000@lid',
65
+ },
66
+ ]
67
+
68
+ const target = resolveTaskOriginConnectorFollowupTarget({
69
+ task,
70
+ sessions: sessions as SessionFixtureMap,
71
+ connectors,
72
+ running,
73
+ })
74
+
75
+ assert.deepEqual(target, {
76
+ connectorId: 'conn-wa',
77
+ channelId: '447700900123@s.whatsapp.net',
78
+ })
79
+ })
80
+
81
+ it('falls back to runtime recent channel when source channel is unavailable', () => {
82
+ const task = makeTask({ createdInSessionId: 'session-1' })
83
+ const sessions = {
84
+ 'session-1': {
85
+ messages: [
86
+ {
87
+ role: 'user',
88
+ text: 'run this later',
89
+ source: {
90
+ connectorId: 'conn-telegram',
91
+ },
92
+ },
93
+ ],
94
+ },
95
+ }
96
+ const connectors = {
97
+ 'conn-telegram': {
98
+ id: 'conn-telegram',
99
+ platform: 'telegram',
100
+ agentId: 'agent-a',
101
+ config: {},
102
+ },
103
+ }
104
+ const running = [
105
+ {
106
+ id: 'conn-telegram',
107
+ platform: 'telegram',
108
+ agentId: 'agent-a',
109
+ supportsSend: true,
110
+ configuredTargets: [],
111
+ recentChannelId: 'tg-chat-42',
112
+ },
113
+ ]
114
+
115
+ const target = resolveTaskOriginConnectorFollowupTarget({
116
+ task,
117
+ sessions: sessions as SessionFixtureMap,
118
+ connectors,
119
+ running,
120
+ })
121
+
122
+ assert.deepEqual(target, {
123
+ connectorId: 'conn-telegram',
124
+ channelId: 'tg-chat-42',
125
+ })
126
+ })
127
+
128
+ it('returns null when the source connector belongs to a different agent', () => {
129
+ const task = makeTask({ createdInSessionId: 'session-1' })
130
+ const sessions = {
131
+ 'session-1': {
132
+ messages: [
133
+ {
134
+ role: 'user',
135
+ text: 'do it',
136
+ source: {
137
+ connectorId: 'conn-wa',
138
+ channelId: '+15551230000',
139
+ },
140
+ },
141
+ ],
142
+ },
143
+ }
144
+ const connectors = {
145
+ 'conn-wa': {
146
+ id: 'conn-wa',
147
+ platform: 'whatsapp',
148
+ agentId: 'different-agent',
149
+ config: {},
150
+ },
151
+ }
152
+ const running = [
153
+ {
154
+ id: 'conn-wa',
155
+ platform: 'whatsapp',
156
+ agentId: 'different-agent',
157
+ supportsSend: true,
158
+ configuredTargets: [],
159
+ recentChannelId: null,
160
+ },
161
+ ]
162
+
163
+ const target = resolveTaskOriginConnectorFollowupTarget({
164
+ task,
165
+ sessions: sessions as SessionFixtureMap,
166
+ connectors,
167
+ running,
168
+ })
169
+
170
+ assert.equal(target, null)
171
+ })
172
+
173
+ it('allows delegated tasks to follow up via the delegating agent connector', () => {
174
+ const task = makeTask({
175
+ agentId: 'worker-agent',
176
+ delegatedByAgentId: 'delegator-agent',
177
+ createdInSessionId: 'session-1',
178
+ })
179
+ const sessions = {
180
+ 'session-1': {
181
+ messages: [
182
+ {
183
+ role: 'user',
184
+ text: 'run and update me here',
185
+ source: {
186
+ connectorId: 'conn-wa',
187
+ channelId: '+44 7700 900123',
188
+ },
189
+ },
190
+ ],
191
+ },
192
+ }
193
+ const connectors = {
194
+ 'conn-wa': {
195
+ id: 'conn-wa',
196
+ platform: 'whatsapp',
197
+ agentId: 'delegator-agent',
198
+ config: {},
199
+ },
200
+ }
201
+ const running = [
202
+ {
203
+ id: 'conn-wa',
204
+ platform: 'whatsapp',
205
+ agentId: 'delegator-agent',
206
+ supportsSend: true,
207
+ configuredTargets: [],
208
+ recentChannelId: null,
209
+ },
210
+ ]
211
+
212
+ const target = resolveTaskOriginConnectorFollowupTarget({
213
+ task,
214
+ sessions: sessions as SessionFixtureMap,
215
+ connectors,
216
+ running,
217
+ })
218
+
219
+ assert.deepEqual(target, {
220
+ connectorId: 'conn-wa',
221
+ channelId: '447700900123@s.whatsapp.net',
222
+ })
223
+ })
224
+ })