@swarmclawai/swarmclaw 0.6.4 → 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 (92) 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/cli/index.js +15 -0
  16. package/src/cli/spec.js +14 -0
  17. package/src/components/agents/agent-avatar.tsx +15 -1
  18. package/src/components/agents/agent-card.tsx +1 -0
  19. package/src/components/agents/agent-chat-list.tsx +1 -1
  20. package/src/components/agents/agent-sheet.tsx +112 -26
  21. package/src/components/chat/chat-area.tsx +2 -2
  22. package/src/components/chat/chat-header.tsx +48 -19
  23. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  24. package/src/components/chat/delegation-banner.test.ts +27 -0
  25. package/src/components/chat/delegation-banner.tsx +109 -23
  26. package/src/components/chat/message-bubble.tsx +3 -2
  27. package/src/components/chat/message-list.tsx +5 -4
  28. package/src/components/chat/streaming-bubble.tsx +3 -2
  29. package/src/components/chat/thinking-indicator.tsx +3 -2
  30. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  31. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  32. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  33. package/src/components/chatrooms/chatroom-message.tsx +1 -1
  34. package/src/components/chatrooms/chatroom-sheet.tsx +1 -1
  35. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  36. package/src/components/chatrooms/chatroom-view.tsx +1 -1
  37. package/src/components/connectors/connector-list.tsx +1 -1
  38. package/src/components/home/home-view.tsx +2 -1
  39. package/src/components/knowledge/knowledge-list.tsx +1 -1
  40. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  41. package/src/components/layout/app-layout.tsx +18 -3
  42. package/src/components/memory/memory-agent-list.tsx +1 -1
  43. package/src/components/memory/memory-browser.tsx +1 -0
  44. package/src/components/memory/memory-card.tsx +3 -2
  45. package/src/components/memory/memory-detail.tsx +3 -3
  46. package/src/components/memory/memory-sheet.tsx +2 -2
  47. package/src/components/projects/project-detail.tsx +4 -4
  48. package/src/components/secrets/secret-sheet.tsx +1 -1
  49. package/src/components/secrets/secrets-list.tsx +1 -1
  50. package/src/components/sessions/session-card.tsx +1 -1
  51. package/src/components/shared/agent-picker-list.tsx +1 -1
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  54. package/src/components/skills/skill-list.tsx +1 -1
  55. package/src/components/skills/skill-sheet.tsx +1 -1
  56. package/src/components/tasks/task-board.tsx +3 -3
  57. package/src/components/tasks/task-sheet.tsx +21 -1
  58. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  59. package/src/components/wallets/wallet-panel.tsx +616 -0
  60. package/src/components/wallets/wallet-section.tsx +100 -0
  61. package/src/lib/server/agent-registry.ts +2 -2
  62. package/src/lib/server/chat-execution.ts +35 -3
  63. package/src/lib/server/chatroom-health.ts +60 -0
  64. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  65. package/src/lib/server/chatroom-helpers.ts +64 -11
  66. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  67. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  68. package/src/lib/server/connectors/manager.ts +80 -2
  69. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  70. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  71. package/src/lib/server/connectors/whatsapp.ts +8 -5
  72. package/src/lib/server/orchestrator-lg.ts +12 -2
  73. package/src/lib/server/orchestrator.ts +6 -1
  74. package/src/lib/server/queue-followups.test.ts +224 -0
  75. package/src/lib/server/queue.ts +226 -24
  76. package/src/lib/server/scheduler.ts +3 -0
  77. package/src/lib/server/session-tools/chatroom.ts +11 -2
  78. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  79. package/src/lib/server/session-tools/index.ts +6 -2
  80. package/src/lib/server/session-tools/memory.ts +1 -1
  81. package/src/lib/server/session-tools/shell.ts +1 -1
  82. package/src/lib/server/session-tools/wallet.ts +124 -0
  83. package/src/lib/server/session-tools/web.ts +2 -2
  84. package/src/lib/server/solana.ts +122 -0
  85. package/src/lib/server/storage.ts +38 -0
  86. package/src/lib/server/stream-agent-chat.ts +126 -63
  87. package/src/lib/server/task-mention.test.ts +41 -0
  88. package/src/lib/server/task-mention.ts +3 -2
  89. package/src/lib/tool-definitions.ts +1 -0
  90. package/src/lib/view-routes.ts +1 -0
  91. package/src/stores/use-app-store.ts +8 -0
  92. package/src/types/index.ts +60 -1
@@ -187,7 +187,7 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
187
187
  },
188
188
  {
189
189
  name: 'memory_tool',
190
- description: 'Store and retrieve long-term memories that persist across sessions. Memories can be shared or agent-scoped. Supports file references, image attachments, and linking memories together with depth traversal. Also supports a cross-agent knowledge base via "knowledge_store" and "knowledge_search". Use "store", "get", "search", "list", "delete", "link", "unlink", "knowledge_store", or "knowledge_search".',
190
+ description: 'My long-term memory things I remember across conversations. I can store personal notes, recall past context, and build up knowledge over time. Memories can be private to me or shared with other agents. I can also attach files, link related memories, and contribute to a shared knowledge base. Actions: store, get, search, list, delete, link, unlink, knowledge_store, knowledge_search.',
191
191
  schema: z.object({
192
192
  action: z.enum(['store', 'get', 'search', 'list', 'delete', 'link', 'unlink', 'knowledge_store', 'knowledge_search']).describe('The action to perform'),
193
193
  key: z.string().describe('For store: memory title. For get/delete/link/unlink: memory ID. For search: optional query fallback.'),
@@ -49,7 +49,7 @@ export function buildShellTools(bctx: ToolBuildContext): StructuredToolInterface
49
49
  },
50
50
  {
51
51
  name: 'execute_command',
52
- description: 'Execute a shell command in the session working directory. This is the PRIMARY tool for running servers, dev servers, installing packages, running scripts, git operations, and any command the user wants to run or test. Use background=true for long-running processes like servers. Supports timeout/yield controls.',
52
+ description: 'Run a shell command in my working directory. This is how I run servers, install packages, execute scripts, use git, and do anything hands-on. Use background=true for long-running processes like dev servers. Supports timeout/yield controls.',
53
53
  schema: z.object({
54
54
  command: z.string().describe('The shell command to execute'),
55
55
  background: z.boolean().optional().describe('If true, start command in background immediately'),
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { ToolBuildContext } from './context'
4
+ import { loadWallets, loadWalletTransactions } from '../storage'
5
+ import type { AgentWallet, WalletTransaction } from '@/types'
6
+
7
+ export function buildWalletTools(bctx: ToolBuildContext): StructuredToolInterface[] {
8
+ if (!bctx.hasTool('wallet')) return []
9
+
10
+ const agentId = bctx.ctx?.agentId
11
+
12
+ function getAgentWallet(): AgentWallet | null {
13
+ if (!agentId) return null
14
+ const wallets = loadWallets() as Record<string, AgentWallet>
15
+ return Object.values(wallets).find((w) => w.agentId === agentId) ?? null
16
+ }
17
+
18
+ return [
19
+ tool(
20
+ async ({ action, toAddress, amountSol, memo, limit }) => {
21
+ const wallet = getAgentWallet()
22
+ if (!wallet) {
23
+ return JSON.stringify({ error: 'No wallet linked to this agent. Ask the user to create one in the Wallets section.' })
24
+ }
25
+
26
+ switch (action) {
27
+ case 'balance': {
28
+ try {
29
+ const { getBalance, lamportsToSol } = await import('../solana')
30
+ const balanceLamports = await getBalance(wallet.publicKey)
31
+ return JSON.stringify({
32
+ address: wallet.publicKey,
33
+ chain: wallet.chain,
34
+ balanceLamports,
35
+ balanceSol: lamportsToSol(balanceLamports),
36
+ })
37
+ } catch (err: unknown) {
38
+ return JSON.stringify({ error: `Failed to fetch balance: ${err instanceof Error ? err.message : String(err)}` })
39
+ }
40
+ }
41
+
42
+ case 'address': {
43
+ return JSON.stringify({
44
+ address: wallet.publicKey,
45
+ chain: wallet.chain,
46
+ })
47
+ }
48
+
49
+ case 'send': {
50
+ if (!toAddress) return JSON.stringify({ error: 'toAddress is required for send action' })
51
+ if (!amountSol || amountSol <= 0) return JSON.stringify({ error: 'amountSol must be positive' })
52
+
53
+ const { isValidSolanaAddress, solToLamports, lamportsToSol } = await import('../solana')
54
+ if (!isValidSolanaAddress(toAddress)) {
55
+ return JSON.stringify({ error: 'Invalid Solana address' })
56
+ }
57
+
58
+ const amountLamports = solToLamports(amountSol)
59
+
60
+ // Check per-tx limit
61
+ const perTxLimit = wallet.spendingLimitLamports ?? 100_000_000
62
+ if (amountLamports > perTxLimit) {
63
+ return JSON.stringify({
64
+ error: `Amount ${amountSol} SOL exceeds per-transaction limit of ${lamportsToSol(perTxLimit)} SOL`,
65
+ })
66
+ }
67
+
68
+ // Send via API to enforce all limits and approval flow
69
+ try {
70
+ const baseUrl = process.env.NEXTAUTH_URL || `http://localhost:${process.env.PORT || 3456}`
71
+ const res = await fetch(`${baseUrl}/api/wallets/${wallet.id}/send`, {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ 'X-Access-Key': process.env.ACCESS_KEY || '',
76
+ },
77
+ body: JSON.stringify({ toAddress, amountLamports, memo }),
78
+ })
79
+ const result = await res.json()
80
+ return JSON.stringify(result)
81
+ } catch (err: unknown) {
82
+ return JSON.stringify({ error: `Send failed: ${err instanceof Error ? err.message : String(err)}` })
83
+ }
84
+ }
85
+
86
+ case 'transactions': {
87
+ const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
88
+ const walletTxs = Object.values(allTxs)
89
+ .filter((tx) => tx.walletId === wallet.id)
90
+ .sort((a, b) => b.timestamp - a.timestamp)
91
+ .slice(0, limit ?? 10)
92
+ .map((tx) => ({
93
+ id: tx.id,
94
+ type: tx.type,
95
+ status: tx.status,
96
+ amountLamports: tx.amountLamports,
97
+ toAddress: tx.toAddress,
98
+ fromAddress: tx.fromAddress,
99
+ signature: tx.signature || undefined,
100
+ memo: tx.memo,
101
+ timestamp: tx.timestamp,
102
+ }))
103
+
104
+ return JSON.stringify({ transactions: walletTxs, count: walletTxs.length })
105
+ }
106
+
107
+ default:
108
+ return JSON.stringify({ error: `Unknown action: ${action}. Use balance, address, send, or transactions.` })
109
+ }
110
+ },
111
+ {
112
+ name: 'wallet_tool',
113
+ description: 'Manage your own crypto wallet. Actions: balance (check your SOL balance), address (get your wallet address), send (send SOL from your wallet — subject to your spending limits and user approval), transactions (view your recent transaction history).',
114
+ schema: z.object({
115
+ action: z.enum(['balance', 'address', 'send', 'transactions']).describe('Wallet action to perform'),
116
+ toAddress: z.string().optional().describe('Recipient Solana address (required for send)'),
117
+ amountSol: z.number().optional().describe('Amount in SOL to send (required for send)'),
118
+ memo: z.string().optional().describe('Reason or memo for the transaction'),
119
+ limit: z.number().optional().describe('Number of transactions to return (default 10, for transactions action)'),
120
+ }),
121
+ },
122
+ ),
123
+ ]
124
+ }
@@ -141,7 +141,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
141
141
  },
142
142
  {
143
143
  name: 'web_search',
144
- description: 'Search the web. Returns an array of results with title, url, and snippet.',
144
+ description: 'Search the web for information. Returns results with title, url, and snippet.',
145
145
  schema: z.object({
146
146
  query: z.string().describe('Search query'),
147
147
  maxResults: z.number().optional().describe('Maximum results to return (default 5, max 10)'),
@@ -179,7 +179,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
179
179
  },
180
180
  {
181
181
  name: 'web_fetch',
182
- description: 'Fetch a URL and return its text content (HTML stripped). Useful for reading web pages.',
182
+ description: 'Fetch a URL and read its content (HTML stripped to text). How I read web pages and pull in external information.',
183
183
  schema: z.object({
184
184
  url: z.string().describe('The URL to fetch'),
185
185
  }),
@@ -0,0 +1,122 @@
1
+ import { Keypair, Connection, PublicKey, SystemProgram, Transaction, LAMPORTS_PER_SOL, sendAndConfirmTransaction } from '@solana/web3.js'
2
+ import bs58 from 'bs58'
3
+ import { encryptKey, decryptKey } from './storage'
4
+
5
+ const DEFAULT_RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Keypair generation & encryption
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export function generateSolanaKeypair(): { publicKey: string; encryptedPrivateKey: string } {
12
+ const keypair = Keypair.generate()
13
+ const secretKeyBase58 = bs58.encode(keypair.secretKey)
14
+ return {
15
+ publicKey: keypair.publicKey.toBase58(),
16
+ encryptedPrivateKey: encryptKey(secretKeyBase58),
17
+ }
18
+ }
19
+
20
+ export function getKeypairFromEncrypted(encryptedPrivateKey: string): Keypair {
21
+ const secretKeyBase58 = decryptKey(encryptedPrivateKey)
22
+ const secretKey = bs58.decode(secretKeyBase58)
23
+ return Keypair.fromSecretKey(secretKey)
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Connection
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export function getConnection(rpcUrl?: string): Connection {
31
+ return new Connection(rpcUrl || DEFAULT_RPC_URL, 'confirmed')
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Balance
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export async function getBalance(publicKey: string, rpcUrl?: string): Promise<number> {
39
+ const connection = getConnection(rpcUrl)
40
+ const pk = new PublicKey(publicKey)
41
+ return connection.getBalance(pk)
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Send SOL
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export async function sendSol(
49
+ encryptedPrivateKey: string,
50
+ toAddress: string,
51
+ lamports: number,
52
+ rpcUrl?: string,
53
+ ): Promise<{ signature: string; fee: number }> {
54
+ const connection = getConnection(rpcUrl)
55
+ const fromKeypair = getKeypairFromEncrypted(encryptedPrivateKey)
56
+ const toPublicKey = new PublicKey(toAddress)
57
+
58
+ const transaction = new Transaction().add(
59
+ SystemProgram.transfer({
60
+ fromPubkey: fromKeypair.publicKey,
61
+ toPubkey: toPublicKey,
62
+ lamports,
63
+ }),
64
+ )
65
+
66
+ const signature = await sendAndConfirmTransaction(connection, transaction, [fromKeypair])
67
+
68
+ // Fetch fee from confirmed tx
69
+ let fee = 5000 // default fee estimate
70
+ try {
71
+ const txInfo = await connection.getTransaction(signature, { commitment: 'confirmed' })
72
+ if (txInfo?.meta?.fee) fee = txInfo.meta.fee
73
+ } catch {
74
+ // use default
75
+ }
76
+
77
+ return { signature, fee }
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Recent transactions
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export async function getRecentTransactions(
85
+ publicKey: string,
86
+ limit = 20,
87
+ rpcUrl?: string,
88
+ ): Promise<Array<{ signature: string; blockTime: number | null; err: unknown }>> {
89
+ const connection = getConnection(rpcUrl)
90
+ const pk = new PublicKey(publicKey)
91
+ const signatures = await connection.getSignaturesForAddress(pk, { limit })
92
+ return signatures.map((s) => ({
93
+ signature: s.signature,
94
+ blockTime: s.blockTime ?? null,
95
+ err: s.err,
96
+ }))
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Validate address
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export function isValidSolanaAddress(address: string): boolean {
104
+ try {
105
+ new PublicKey(address)
106
+ return true
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Helpers
114
+ // ---------------------------------------------------------------------------
115
+
116
+ export function lamportsToSol(lamports: number): number {
117
+ return lamports / LAMPORTS_PER_SOL
118
+ }
119
+
120
+ export function solToLamports(sol: number): number {
121
+ return Math.round(sol * LAMPORTS_PER_SOL)
122
+ }
@@ -54,6 +54,9 @@ const COLLECTIONS = [
54
54
  'webhook_retry_queue',
55
55
  'notifications',
56
56
  'chatrooms',
57
+ 'wallets',
58
+ 'wallet_transactions',
59
+ 'wallet_balance_history',
57
60
  ] as const
58
61
 
59
62
  for (const table of COLLECTIONS) {
@@ -826,6 +829,41 @@ export function markNotificationRead(id: string) {
826
829
  }
827
830
  }
828
831
 
832
+ // --- Wallets ---
833
+ export function loadWallets(): Record<string, unknown> {
834
+ return loadCollection('wallets')
835
+ }
836
+
837
+ export function upsertWallet(id: string, wallet: unknown) {
838
+ upsertCollectionItem('wallets', id, wallet)
839
+ }
840
+
841
+ export function deleteWallet(id: string) {
842
+ deleteCollectionItem('wallets', id)
843
+ }
844
+
845
+ // --- Wallet Transactions ---
846
+ export function loadWalletTransactions(): Record<string, unknown> {
847
+ return loadCollection('wallet_transactions')
848
+ }
849
+
850
+ export function upsertWalletTransaction(id: string, tx: unknown) {
851
+ upsertCollectionItem('wallet_transactions', id, tx)
852
+ }
853
+
854
+ export function deleteWalletTransaction(id: string) {
855
+ deleteCollectionItem('wallet_transactions', id)
856
+ }
857
+
858
+ // --- Wallet Balance History ---
859
+ export function loadWalletBalanceHistory(): Record<string, unknown> {
860
+ return loadCollection('wallet_balance_history')
861
+ }
862
+
863
+ export function upsertWalletBalanceSnapshot(id: string, snapshot: unknown) {
864
+ upsertCollectionItem('wallet_balance_history', id, snapshot)
865
+ }
866
+
829
867
  export function getSessionMessages(sessionId: string): Message[] {
830
868
  const stmt = db.prepare('SELECT data FROM sessions WHERE id = ?')
831
869
  const row = stmt.get(sessionId) as { data: string } | undefined