@swarmclawai/swarmclaw 0.7.7 → 0.8.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.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -1,12 +1,95 @@
1
- import { Keypair, Connection, PublicKey, SystemProgram, Transaction, LAMPORTS_PER_SOL, sendAndConfirmTransaction } from '@solana/web3.js'
1
+ import {
2
+ Connection,
3
+ Keypair,
4
+ LAMPORTS_PER_SOL,
5
+ PublicKey,
6
+ SystemProgram,
7
+ Transaction,
8
+ VersionedTransaction,
9
+ sendAndConfirmTransaction,
10
+ } from '@solana/web3.js'
2
11
  import bs58 from 'bs58'
3
- import { encryptKey, decryptKey } from './storage'
12
+ import nacl from 'tweetnacl'
13
+
14
+ import { decryptKey, encryptKey } from './storage'
15
+
16
+ export type SolanaCluster = 'mainnet-beta' | 'devnet' | 'testnet'
17
+
18
+ export interface SolanaExecutionOptions {
19
+ cluster?: SolanaCluster | string | null
20
+ rpcUrl?: string | null
21
+ }
22
+
23
+ export interface SolanaMessageInput {
24
+ message?: string | null
25
+ messageHex?: string | null
26
+ messageBase64?: string | null
27
+ }
4
28
 
5
29
  const DEFAULT_RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
6
30
 
7
- // ---------------------------------------------------------------------------
8
- // Keypair generation & encryption
9
- // ---------------------------------------------------------------------------
31
+ function getClusterRpcUrl(cluster: SolanaCluster): string {
32
+ if (cluster === 'devnet') return process.env.SOLANA_DEVNET_RPC_URL || 'https://api.devnet.solana.com'
33
+ if (cluster === 'testnet') return process.env.SOLANA_TESTNET_RPC_URL || 'https://api.testnet.solana.com'
34
+ return process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
35
+ }
36
+
37
+ function normalizeHexMessage(value: string): Uint8Array {
38
+ const trimmed = value.trim()
39
+ if (!/^0x[0-9a-fA-F]*$/.test(trimmed)) {
40
+ throw new Error('messageHex must be a 0x-prefixed hex string')
41
+ }
42
+ return Uint8Array.from(Buffer.from(trimmed.slice(2), 'hex'))
43
+ }
44
+
45
+ function normalizeMessageBytes(input: SolanaMessageInput): Uint8Array {
46
+ if (typeof input.messageHex === 'string' && input.messageHex.trim()) return normalizeHexMessage(input.messageHex)
47
+ if (typeof input.messageBase64 === 'string' && input.messageBase64.trim()) {
48
+ return Uint8Array.from(Buffer.from(input.messageBase64.trim(), 'base64'))
49
+ }
50
+ if (typeof input.message === 'string') return new TextEncoder().encode(input.message)
51
+ throw new Error('message, messageHex, or messageBase64 is required')
52
+ }
53
+
54
+ function deserializeTransactionBase64(value: string): Transaction | VersionedTransaction {
55
+ const bytes = Buffer.from(value, 'base64')
56
+ try {
57
+ return VersionedTransaction.deserialize(bytes)
58
+ } catch {
59
+ return Transaction.from(bytes)
60
+ }
61
+ }
62
+
63
+ function serializeTransactionBase64(transaction: Transaction | VersionedTransaction): string {
64
+ return Buffer.from(transaction.serialize()).toString('base64')
65
+ }
66
+
67
+ function collectTransactionSignatures(transaction: Transaction | VersionedTransaction): string[] {
68
+ if (transaction instanceof VersionedTransaction) {
69
+ return transaction.signatures
70
+ .map((signature) => bs58.encode(signature))
71
+ .filter(Boolean)
72
+ }
73
+ return transaction.signatures
74
+ .map((entry) => (entry.signature ? bs58.encode(entry.signature) : ''))
75
+ .filter(Boolean)
76
+ }
77
+
78
+ function signTransactionWithWallet(
79
+ encryptedPrivateKey: string,
80
+ transaction: Transaction | VersionedTransaction,
81
+ ): { transaction: Transaction | VersionedTransaction; publicKey: string } {
82
+ const keypair = getKeypairFromEncrypted(encryptedPrivateKey)
83
+ if (transaction instanceof VersionedTransaction) {
84
+ transaction.sign([keypair])
85
+ } else {
86
+ transaction.sign(keypair)
87
+ }
88
+ return {
89
+ transaction,
90
+ publicKey: keypair.publicKey.toBase58(),
91
+ }
92
+ }
10
93
 
11
94
  export function generateSolanaKeypair(): { publicKey: string; encryptedPrivateKey: string } {
12
95
  const keypair = Keypair.generate()
@@ -23,17 +106,36 @@ export function getKeypairFromEncrypted(encryptedPrivateKey: string): Keypair {
23
106
  return Keypair.fromSecretKey(secretKey)
24
107
  }
25
108
 
26
- // ---------------------------------------------------------------------------
27
- // Connection
28
- // ---------------------------------------------------------------------------
109
+ export function normalizeSolanaCluster(value: unknown, fallback: SolanaCluster = 'mainnet-beta'): SolanaCluster {
110
+ const normalized = String(value ?? '').trim().toLowerCase()
111
+ if (!normalized) return fallback
112
+ if (normalized === 'mainnet' || normalized === 'mainnet-beta' || normalized === 'solana') return 'mainnet-beta'
113
+ if (normalized === 'devnet') return 'devnet'
114
+ if (normalized === 'testnet') return 'testnet'
115
+ throw new Error(`Unsupported Solana cluster: ${String(value)}`)
116
+ }
117
+
118
+ export function getSolanaClusterLabel(value?: unknown): string {
119
+ const cluster = normalizeSolanaCluster(value)
120
+ if (cluster === 'devnet') return 'Solana Devnet'
121
+ if (cluster === 'testnet') return 'Solana Testnet'
122
+ return 'Solana Mainnet'
123
+ }
124
+
125
+ export function getSolanaExplorerUrl(cluster: SolanaCluster | string | null | undefined, kind: 'address' | 'transaction', value: string): string {
126
+ const normalized = normalizeSolanaCluster(cluster)
127
+ const prefix = kind === 'address' ? 'address' : 'tx'
128
+ const clusterSuffix = normalized === 'mainnet-beta' ? '' : `?cluster=${normalized}`
129
+ return `https://explorer.solana.com/${prefix}/${value}${clusterSuffix}`
130
+ }
29
131
 
30
132
  export function getConnection(rpcUrl?: string): Connection {
31
133
  return new Connection(rpcUrl || DEFAULT_RPC_URL, 'confirmed')
32
134
  }
33
135
 
34
- // ---------------------------------------------------------------------------
35
- // Balance
36
- // ---------------------------------------------------------------------------
136
+ export function getConnectionForCluster(cluster?: SolanaCluster | string | null, rpcUrl?: string | null): Connection {
137
+ return new Connection(rpcUrl || getClusterRpcUrl(normalizeSolanaCluster(cluster)), 'confirmed')
138
+ }
37
139
 
38
140
  export async function getBalance(publicKey: string, rpcUrl?: string): Promise<number> {
39
141
  const connection = getConnection(rpcUrl)
@@ -41,10 +143,6 @@ export async function getBalance(publicKey: string, rpcUrl?: string): Promise<nu
41
143
  return connection.getBalance(pk)
42
144
  }
43
145
 
44
- // ---------------------------------------------------------------------------
45
- // Send SOL
46
- // ---------------------------------------------------------------------------
47
-
48
146
  export async function sendSol(
49
147
  encryptedPrivateKey: string,
50
148
  toAddress: string,
@@ -65,21 +163,116 @@ export async function sendSol(
65
163
 
66
164
  const signature = await sendAndConfirmTransaction(connection, transaction, [fromKeypair])
67
165
 
68
- // Fetch fee from confirmed tx
69
- let fee = 5000 // default fee estimate
166
+ let fee = 5000
70
167
  try {
71
168
  const txInfo = await connection.getTransaction(signature, { commitment: 'confirmed' })
72
169
  if (txInfo?.meta?.fee) fee = txInfo.meta.fee
73
170
  } catch {
74
- // use default
171
+ // keep default
75
172
  }
76
173
 
77
174
  return { signature, fee }
78
175
  }
79
176
 
80
- // ---------------------------------------------------------------------------
81
- // Recent transactions
82
- // ---------------------------------------------------------------------------
177
+ export async function signSolanaMessage(
178
+ encryptedPrivateKey: string,
179
+ input: SolanaMessageInput,
180
+ ): Promise<{ signature: string; publicKey: string }> {
181
+ const keypair = getKeypairFromEncrypted(encryptedPrivateKey)
182
+ const signature = nacl.sign.detached(normalizeMessageBytes(input), keypair.secretKey)
183
+ return {
184
+ signature: bs58.encode(signature),
185
+ publicKey: keypair.publicKey.toBase58(),
186
+ }
187
+ }
188
+
189
+ export async function signSolanaTransaction(
190
+ encryptedPrivateKey: string,
191
+ transactionBase64: string,
192
+ ): Promise<{
193
+ signedTransactionBase64: string
194
+ signatures: string[]
195
+ publicKey: string
196
+ versioned: boolean
197
+ }> {
198
+ const unsignedTx = deserializeTransactionBase64(transactionBase64)
199
+ const { transaction, publicKey } = signTransactionWithWallet(encryptedPrivateKey, unsignedTx)
200
+ return {
201
+ signedTransactionBase64: serializeTransactionBase64(transaction),
202
+ signatures: collectTransactionSignatures(transaction),
203
+ publicKey,
204
+ versioned: transaction instanceof VersionedTransaction,
205
+ }
206
+ }
207
+
208
+ export async function simulateSolanaTransaction(
209
+ encryptedPrivateKey: string,
210
+ transactionBase64: string,
211
+ options?: SolanaExecutionOptions,
212
+ ): Promise<{
213
+ signatures: string[]
214
+ publicKey: string
215
+ logs: string[]
216
+ unitsConsumed?: number
217
+ err?: unknown
218
+ versioned: boolean
219
+ }> {
220
+ const unsignedTx = deserializeTransactionBase64(transactionBase64)
221
+ const { transaction, publicKey } = signTransactionWithWallet(encryptedPrivateKey, unsignedTx)
222
+ const connection = getConnectionForCluster(options?.cluster, options?.rpcUrl)
223
+ const simulation = transaction instanceof VersionedTransaction
224
+ ? await connection.simulateTransaction(transaction)
225
+ : await connection.simulateTransaction(transaction)
226
+ return {
227
+ signatures: collectTransactionSignatures(transaction),
228
+ publicKey,
229
+ logs: simulation.value.logs || [],
230
+ unitsConsumed: simulation.value.unitsConsumed ?? undefined,
231
+ err: simulation.value.err ?? undefined,
232
+ versioned: transaction instanceof VersionedTransaction,
233
+ }
234
+ }
235
+
236
+ export async function sendSolanaTransaction(
237
+ encryptedPrivateKey: string,
238
+ input: {
239
+ transactionBase64?: string | null
240
+ signedTransactionBase64?: string | null
241
+ waitForConfirmation?: boolean
242
+ },
243
+ options?: SolanaExecutionOptions,
244
+ ): Promise<{
245
+ signature: string
246
+ publicKey: string
247
+ explorerUrl: string
248
+ versioned: boolean
249
+ }> {
250
+ const connection = getConnectionForCluster(options?.cluster, options?.rpcUrl)
251
+ const keypair = getKeypairFromEncrypted(encryptedPrivateKey)
252
+ const waitForConfirmation = input.waitForConfirmation !== false
253
+
254
+ let transaction: Transaction | VersionedTransaction
255
+ if (typeof input.signedTransactionBase64 === 'string' && input.signedTransactionBase64.trim()) {
256
+ transaction = deserializeTransactionBase64(input.signedTransactionBase64.trim())
257
+ } else if (typeof input.transactionBase64 === 'string' && input.transactionBase64.trim()) {
258
+ transaction = signTransactionWithWallet(encryptedPrivateKey, deserializeTransactionBase64(input.transactionBase64.trim())).transaction
259
+ } else {
260
+ throw new Error('transactionBase64 or signedTransactionBase64 is required')
261
+ }
262
+
263
+ const raw = transaction.serialize()
264
+ const signature = await connection.sendRawTransaction(raw)
265
+ if (waitForConfirmation) {
266
+ await connection.confirmTransaction(signature, 'confirmed')
267
+ }
268
+
269
+ return {
270
+ signature,
271
+ publicKey: keypair.publicKey.toBase58(),
272
+ explorerUrl: getSolanaExplorerUrl(options?.cluster, 'transaction', signature),
273
+ versioned: transaction instanceof VersionedTransaction,
274
+ }
275
+ }
83
276
 
84
277
  export async function getRecentTransactions(
85
278
  publicKey: string,
@@ -96,10 +289,6 @@ export async function getRecentTransactions(
96
289
  }))
97
290
  }
98
291
 
99
- // ---------------------------------------------------------------------------
100
- // Validate address
101
- // ---------------------------------------------------------------------------
102
-
103
292
  export function isValidSolanaAddress(address: string): boolean {
104
293
  try {
105
294
  new PublicKey(address)
@@ -109,10 +298,6 @@ export function isValidSolanaAddress(address: string): boolean {
109
298
  }
110
299
  }
111
300
 
112
- // ---------------------------------------------------------------------------
113
- // Helpers
114
- // ---------------------------------------------------------------------------
115
-
116
301
  export function lamportsToSol(lamports: number): number {
117
302
  return lamports / LAMPORTS_PER_SOL
118
303
  }
@@ -2,9 +2,10 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
  import crypto from 'crypto'
4
4
  import os from 'os'
5
+ import type { ChildProcess } from 'node:child_process'
5
6
  import Database from 'better-sqlite3'
6
7
 
7
- import { DATA_DIR, WORKSPACE_DIR } from './data-dir'
8
+ import { DATA_DIR, IS_BUILD_BOOTSTRAP, WORKSPACE_DIR } from './data-dir'
8
9
  import { normalizeHeartbeatSettingFields } from '@/lib/heartbeat-defaults'
9
10
  import { normalizeRuntimeSettingFields } from '@/lib/runtime-loop'
10
11
  import type { ExternalAgentRuntime, GatewayProfile, Message } from '@/types'
@@ -110,7 +111,6 @@ for (const dir of [DATA_DIR, UPLOAD_DIR, WORKSPACE_DIR]) {
110
111
  }
111
112
 
112
113
  // --- SQLite Database ---
113
- const IS_BUILD_BOOTSTRAP = process.env.SWARMCLAW_BUILD_MODE === '1'
114
114
  const DB_PATH = IS_BUILD_BOOTSTRAP ? ':memory:' : path.join(DATA_DIR, 'swarmclaw.db')
115
115
  const db = new Database(DB_PATH)
116
116
  if (!IS_BUILD_BOOTSTRAP) {
@@ -120,6 +120,12 @@ if (!IS_BUILD_BOOTSTRAP) {
120
120
  db.pragma('foreign_keys = ON')
121
121
 
122
122
  const collectionCacheKey = '__swarmclaw_storage_collection_cache__' as const
123
+ type StoredObject = Record<string, unknown>
124
+ type ActiveProcess = ChildProcess | {
125
+ runId?: string | null
126
+ source?: string
127
+ kill: (signal?: NodeJS.Signals | number) => boolean | void
128
+ }
123
129
  type StorageGlobals = typeof globalThis & {
124
130
  [collectionCacheKey]?: Map<string, LRUMap<string, string>>
125
131
  }
@@ -194,11 +200,11 @@ function getCollectionRawCache(table: string): LRUMap<string, string> {
194
200
  return loaded
195
201
  }
196
202
 
197
- function normalizeStoredRecord(table: string, value: any): any {
203
+ function normalizeStoredRecord(table: string, value: unknown): unknown {
198
204
  if (table !== 'sessions') return value
199
205
  if (!value || typeof value !== 'object' || Array.isArray(value)) return value
200
206
 
201
- const session = value as Record<string, any>
207
+ const session = value as StoredObject
202
208
  if (session.sessionType !== 'human') session.sessionType = 'human'
203
209
  const isLegacyShortcut = (
204
210
  (typeof session.id === 'string' && session.id.startsWith('agent-thread-'))
@@ -282,7 +288,7 @@ function deleteCollectionItem(table: string, id: string) {
282
288
  * loading/saving the entire collection. Prevents race conditions when
283
289
  * concurrent processes are modifying different items.
284
290
  */
285
- function upsertCollectionItem(table: string, id: string, value: any) {
291
+ function upsertCollectionItem(table: string, id: string, value: unknown) {
286
292
  const serialized = JSON.stringify(normalizeStoredRecord(table, value))
287
293
  db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`).run(id, serialized)
288
294
  // Update the in-memory cache
@@ -292,7 +298,7 @@ function upsertCollectionItem(table: string, id: string, value: any) {
292
298
  }
293
299
  }
294
300
 
295
- function loadCollectionItem(table: string, id: string): any | null {
301
+ function loadCollectionItem(table: string, id: string): unknown | null {
296
302
  const row = db.prepare(`SELECT data FROM ${table} WHERE id = ?`).get(id) as { data: string } | undefined
297
303
  if (!row) return null
298
304
  try {
@@ -302,7 +308,7 @@ function loadCollectionItem(table: string, id: string): any | null {
302
308
  }
303
309
  }
304
310
 
305
- function upsertCollectionItems(table: string, entries: Array<[string, any]>): void {
311
+ function upsertCollectionItems(table: string, entries: Array<[string, unknown]>): void {
306
312
  if (!entries.length) return
307
313
  const prepared = entries
308
314
  .map(([id, value]) => [id, JSON.stringify(normalizeStoredRecord(table, value))] as const)
@@ -325,24 +331,24 @@ function upsertCollectionItems(table: string, entries: Array<[string, any]>): vo
325
331
  }
326
332
  }
327
333
 
328
- export function loadStoredItem(table: StorageCollection, id: string): any | null {
334
+ export function loadStoredItem(table: StorageCollection, id: string): unknown | null {
329
335
  return loadCollectionItem(table, id)
330
336
  }
331
337
 
332
- export function upsertStoredItem(table: StorageCollection, id: string, value: any): void {
338
+ export function upsertStoredItem(table: StorageCollection, id: string, value: unknown): void {
333
339
  upsertCollectionItem(table, id, value)
334
340
  }
335
341
 
336
- export function upsertStoredItems(table: StorageCollection, entries: Array<[string, any]>): void {
342
+ export function upsertStoredItems(table: StorageCollection, entries: Array<[string, unknown]>): void {
337
343
  upsertCollectionItems(table, entries)
338
344
  }
339
345
 
340
- function loadSingleton(table: string, fallback: any): any {
346
+ function loadSingleton<T>(table: string, fallback: T): T {
341
347
  const row = db.prepare(`SELECT data FROM ${table} WHERE id = 1`).get() as { data: string } | undefined
342
- return row ? JSON.parse(row.data) : fallback
348
+ return row ? JSON.parse(row.data) as T : fallback
343
349
  }
344
350
 
345
- function saveSingleton(table: string, data: any) {
351
+ function saveSingleton(table: string, data: unknown) {
346
352
  db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (1, ?)`).run(JSON.stringify(data))
347
353
  }
348
354
 
@@ -594,37 +600,43 @@ export function markSetupComplete(): void {
594
600
  export function loadSessions(): Record<string, any> {
595
601
  const sessions = loadCollection('sessions')
596
602
  const agents = loadCollection('agents')
597
- let changed = false
603
+ const changedEntries: Array<[string, any]> = []
598
604
 
599
605
  for (const [id, session] of Object.entries(sessions)) {
600
606
  if (!session || typeof session !== 'object') continue
607
+ let touched = false
601
608
 
602
609
  if (typeof session.id !== 'string' || !session.id.trim()) {
603
610
  session.id = id
604
- changed = true
611
+ touched = true
605
612
  }
606
613
 
607
-
608
614
  const agentId = typeof session.agentId === 'string' ? session.agentId.trim() : ''
609
615
  if (agentId && !Object.prototype.hasOwnProperty.call(agents, agentId)) {
610
616
  session.agentId = null
611
- changed = true
617
+ touched = true
612
618
  }
613
619
 
614
620
  // Migrate tools → plugins
615
621
  if (Array.isArray(session.tools) && !Array.isArray(session.plugins)) {
616
622
  session.plugins = session.tools
617
623
  delete session.tools
618
- changed = true
624
+ touched = true
619
625
  }
626
+
627
+ if (touched) changedEntries.push([id, session])
620
628
  }
621
629
 
622
- if (changed) saveCollection('sessions', sessions)
630
+ // Upsert only changed entries — never full-replace, which deletes concurrent sessions
631
+ if (changedEntries.length > 0) upsertCollectionItems('sessions', changedEntries)
623
632
  return sessions
624
633
  }
625
634
 
626
635
  export function saveSessions(s: Record<string, any>) {
627
- saveCollection('sessions', s)
636
+ // Upsert-only — never delete sessions that aren't in the map.
637
+ // Explicit deletion goes through deleteSession(id).
638
+ const entries = Object.entries(s)
639
+ if (entries.length > 0) upsertCollectionItems('sessions', entries)
628
640
  }
629
641
 
630
642
  export function disableAllSessionHeartbeats(): number {
@@ -636,9 +648,9 @@ export function disableAllSessionHeartbeats(): number {
636
648
 
637
649
  const tx = db.transaction(() => {
638
650
  for (const row of rows) {
639
- let parsed: any
651
+ let parsed: StoredObject | null = null
640
652
  try {
641
- parsed = JSON.parse(row.data)
653
+ parsed = JSON.parse(row.data) as StoredObject
642
654
  } catch {
643
655
  continue
644
656
  }
@@ -754,7 +766,7 @@ export function loadTasks(): Record<string, any> {
754
766
  export function saveTasks(t: Record<string, any>) {
755
767
  saveCollection('tasks', t)
756
768
  }
757
- export function upsertTask(id: string, task: any) {
769
+ export function upsertTask(id: string, task: unknown) {
758
770
  upsertCollectionItem('tasks', id, task)
759
771
  }
760
772
  export function deleteTask(id: string) { deleteCollectionItem('tasks', id) }
@@ -809,6 +821,9 @@ function isProvidedSecretValue(value: unknown): value is string {
809
821
 
810
822
  function buildPersistedSettings(input: Record<string, any>, existing?: PersistedSettingsRecord): PersistedSettingsRecord {
811
823
  const next = cloneRecord(input) as PersistedSettingsRecord
824
+ if (typeof next.approvalsEnabled !== 'boolean' && typeof existing?.approvalsEnabled !== 'boolean') {
825
+ next.approvalsEnabled = false
826
+ }
812
827
  Object.assign(next, normalizeRuntimeSettingFields(next))
813
828
  Object.assign(next, normalizeHeartbeatSettingFields(next))
814
829
  const encrypted = {
@@ -900,15 +915,13 @@ export async function getSecret(key: string): Promise<{
900
915
  if (!needle) return null
901
916
 
902
917
  const secrets = loadSecrets()
903
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
904
- const matches = Object.values(secrets).find((secret: any) => {
918
+ const matches = Object.values(secrets).find((secret): secret is StoredObject => {
905
919
  if (!secret || typeof secret !== 'object') return false
906
920
  const id = typeof secret.id === 'string' ? secret.id.toLowerCase() : ''
907
921
  const name = typeof secret.name === 'string' ? secret.name.toLowerCase() : ''
908
922
  const service = typeof secret.service === 'string' ? secret.service.toLowerCase() : ''
909
923
  return id === needle || name === needle || service === needle
910
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
911
- }) as any | undefined
924
+ })
912
925
 
913
926
  if (!matches) return null
914
927
 
@@ -919,15 +932,23 @@ export async function getSecret(key: string): Promise<{
919
932
  : (typeof matches.value === 'string' ? matches.value : '')
920
933
  if (!decryptedValue) return null
921
934
 
935
+ const id = typeof matches.id === 'string' ? matches.id : ''
936
+ const name = typeof matches.name === 'string' ? matches.name : ''
937
+ const service = typeof matches.service === 'string' ? matches.service : ''
938
+ const scope = typeof matches.scope === 'string' ? matches.scope : ''
939
+ const createdAt = typeof matches.createdAt === 'number' ? matches.createdAt : 0
940
+ const updatedAt = typeof matches.updatedAt === 'number' ? matches.updatedAt : 0
941
+ if (!id || !name || !service || !scope) return null
942
+
922
943
  return {
923
- id: matches.id,
924
- name: matches.name,
925
- service: matches.service,
944
+ id,
945
+ name,
946
+ service,
926
947
  value: decryptedValue,
927
- scope: matches.scope,
948
+ scope,
928
949
  agentIds: Array.isArray(matches.agentIds) ? matches.agentIds : [],
929
- createdAt: matches.createdAt,
930
- updatedAt: matches.updatedAt,
950
+ createdAt,
951
+ updatedAt,
931
952
  }
932
953
  } catch {
933
954
  return null
@@ -1016,7 +1037,7 @@ export function saveUsage(u: Record<string, any[]>) {
1016
1037
  transaction()
1017
1038
  }
1018
1039
 
1019
- export function appendUsage(sessionId: string, record: any) {
1040
+ export function appendUsage(sessionId: string, record: unknown) {
1020
1041
  const ins = db.prepare('INSERT INTO usage (session_id, data) VALUES (?, ?)')
1021
1042
  ins.run(sessionId, JSON.stringify(record))
1022
1043
  }
@@ -1058,8 +1079,8 @@ export function saveWebhooks(w: Record<string, any>) {
1058
1079
  }
1059
1080
 
1060
1081
  // --- Active processes ---
1061
- export const active = new Map<string, any>()
1062
- export const devServers = new Map<string, { proc: any; url: string }>()
1082
+ export const active = new Map<string, ActiveProcess>()
1083
+ export const devServers = new Map<string, { proc: ChildProcess; url: string }>()
1063
1084
 
1064
1085
  // --- Utilities ---
1065
1086
  export function localIP(): string {
@@ -1097,6 +1118,10 @@ export function loadWebhookLogs(): Record<string, unknown> {
1097
1118
  return loadCollection('webhook_logs')
1098
1119
  }
1099
1120
 
1121
+ export function saveWebhookLogs(entries: Record<string, unknown>) {
1122
+ saveCollection('webhook_logs', entries)
1123
+ }
1124
+
1100
1125
  export function appendWebhookLog(id: string, entry: unknown) {
1101
1126
  upsertCollectionItem('webhook_logs', id, entry)
1102
1127
  }
@@ -1125,6 +1150,10 @@ export function loadWebhookRetryQueue(): Record<string, unknown> {
1125
1150
  return loadCollection('webhook_retry_queue')
1126
1151
  }
1127
1152
 
1153
+ export function saveWebhookRetryQueue(entries: Record<string, unknown>) {
1154
+ saveCollection('webhook_retry_queue', entries)
1155
+ }
1156
+
1128
1157
  export function upsertWebhookRetry(id: string, entry: unknown) {
1129
1158
  upsertCollectionItem('webhook_retry_queue', id, entry)
1130
1159
  }