@swarmclawai/swarmclaw 0.2.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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,670 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import crypto from 'crypto'
4
+ import os from 'os'
5
+ import Database from 'better-sqlite3'
6
+
7
+ import { DATA_DIR } from './data-dir'
8
+ export const UPLOAD_DIR = path.join(os.tmpdir(), 'swarmclaw-uploads')
9
+
10
+ // Ensure directories exist
11
+ for (const dir of [DATA_DIR, UPLOAD_DIR]) {
12
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
13
+ }
14
+
15
+ // --- SQLite Database ---
16
+ const DB_PATH = path.join(DATA_DIR, 'swarmclaw.db')
17
+ const db = new Database(DB_PATH)
18
+ db.pragma('journal_mode = WAL')
19
+ db.pragma('foreign_keys = ON')
20
+
21
+ const collectionCacheKey = '__swarmclaw_storage_collection_cache__' as const
22
+ type StorageGlobals = typeof globalThis & {
23
+ [collectionCacheKey]?: Map<string, Map<string, string>>
24
+ }
25
+ const storageGlobals = globalThis as StorageGlobals
26
+ const collectionCache: Map<string, Map<string, string>> =
27
+ storageGlobals[collectionCacheKey]
28
+ ?? (storageGlobals[collectionCacheKey] = new Map<string, Map<string, string>>())
29
+
30
+ // Collection tables (id → JSON blob)
31
+ const COLLECTIONS = [
32
+ 'sessions',
33
+ 'credentials',
34
+ 'agents',
35
+ 'schedules',
36
+ 'tasks',
37
+ 'secrets',
38
+ 'provider_configs',
39
+ 'skills',
40
+ 'connectors',
41
+ 'documents',
42
+ 'webhooks',
43
+ 'model_overrides',
44
+ 'mcp_servers',
45
+ 'webhook_logs',
46
+ ] as const
47
+
48
+ for (const table of COLLECTIONS) {
49
+ db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
50
+ }
51
+
52
+ // Singleton tables (single row)
53
+ db.exec(`CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL)`)
54
+ db.exec(`CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL)`)
55
+ db.exec(`CREATE TABLE IF NOT EXISTS usage (session_id TEXT NOT NULL, data TEXT NOT NULL)`)
56
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_usage_session ON usage(session_id)`)
57
+
58
+ function readCollectionRaw(table: string): Map<string, string> {
59
+ const rows = db.prepare(`SELECT id, data FROM ${table}`).all() as { id: string; data: string }[]
60
+ const raw = new Map<string, string>()
61
+ for (const row of rows) {
62
+ raw.set(row.id, row.data)
63
+ }
64
+ return raw
65
+ }
66
+
67
+ function getCollectionRawCache(table: string): Map<string, string> {
68
+ const cached = collectionCache.get(table)
69
+ if (cached) return cached
70
+ const loaded = readCollectionRaw(table)
71
+ collectionCache.set(table, loaded)
72
+ return loaded
73
+ }
74
+
75
+ function loadCollection(table: string): Record<string, any> {
76
+ const raw = getCollectionRawCache(table)
77
+ const result: Record<string, any> = {}
78
+ for (const [id, data] of raw.entries()) {
79
+ try {
80
+ result[id] = JSON.parse(data)
81
+ } catch {
82
+ // Ignore malformed records instead of crashing list endpoints.
83
+ }
84
+ }
85
+ return result
86
+ }
87
+
88
+ function saveCollection(table: string, data: Record<string, any>) {
89
+ const current = getCollectionRawCache(table)
90
+ const toUpsert: Array<[string, string]> = []
91
+
92
+ for (const [id, val] of Object.entries(data)) {
93
+ const serialized = JSON.stringify(val)
94
+ if (typeof serialized !== 'string') continue
95
+ if (current.get(id) !== serialized) {
96
+ toUpsert.push([id, serialized])
97
+ }
98
+ current.set(id, serialized)
99
+ }
100
+
101
+ if (!toUpsert.length) return
102
+
103
+ const transaction = db.transaction(() => {
104
+ const upsert = db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`)
105
+ for (const [id, serialized] of toUpsert) {
106
+ upsert.run(id, serialized)
107
+ }
108
+ })
109
+ transaction()
110
+ }
111
+
112
+ function deleteCollectionItem(table: string, id: string) {
113
+ db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(id)
114
+ const cached = collectionCache.get(table)
115
+ if (cached) cached.delete(id)
116
+ }
117
+
118
+ /**
119
+ * Atomically insert or update a single item in a collection without
120
+ * loading/saving the entire collection. Prevents race conditions when
121
+ * concurrent processes are modifying different items.
122
+ */
123
+ function upsertCollectionItem(table: string, id: string, value: any) {
124
+ const serialized = JSON.stringify(value)
125
+ db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`).run(id, serialized)
126
+ // Update the in-memory cache
127
+ const cached = collectionCache.get(table)
128
+ if (cached) {
129
+ cached.set(id, serialized)
130
+ }
131
+ }
132
+
133
+ function loadSingleton(table: string, fallback: any): any {
134
+ const row = db.prepare(`SELECT data FROM ${table} WHERE id = 1`).get() as { data: string } | undefined
135
+ return row ? JSON.parse(row.data) : fallback
136
+ }
137
+
138
+ function saveSingleton(table: string, data: any) {
139
+ db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (1, ?)`).run(JSON.stringify(data))
140
+ }
141
+
142
+ // --- JSON Migration ---
143
+ // Auto-import from JSON files on first run, then leave them as backup
144
+ const JSON_FILES: Record<string, string> = {
145
+ sessions: path.join(DATA_DIR, 'sessions.json'),
146
+ credentials: path.join(DATA_DIR, 'credentials.json'),
147
+ agents: path.join(DATA_DIR, 'agents.json'),
148
+ schedules: path.join(DATA_DIR, 'schedules.json'),
149
+ tasks: path.join(DATA_DIR, 'tasks.json'),
150
+ secrets: path.join(DATA_DIR, 'secrets.json'),
151
+ provider_configs: path.join(DATA_DIR, 'providers.json'),
152
+ skills: path.join(DATA_DIR, 'skills.json'),
153
+ connectors: path.join(DATA_DIR, 'connectors.json'),
154
+ documents: path.join(DATA_DIR, 'documents.json'),
155
+ webhooks: path.join(DATA_DIR, 'webhooks.json'),
156
+ }
157
+
158
+ const MIGRATION_FLAG = path.join(DATA_DIR, '.sqlite_migrated')
159
+
160
+ function migrateFromJson() {
161
+ if (fs.existsSync(MIGRATION_FLAG)) return
162
+
163
+ console.log('[storage] Migrating from JSON files to SQLite...')
164
+
165
+ const transaction = db.transaction(() => {
166
+ for (const [table, jsonPath] of Object.entries(JSON_FILES)) {
167
+ if (fs.existsSync(jsonPath)) {
168
+ try {
169
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
170
+ if (data && typeof data === 'object' && Object.keys(data).length > 0) {
171
+ const ins = db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`)
172
+ for (const [id, val] of Object.entries(data)) {
173
+ ins.run(id, JSON.stringify(val))
174
+ }
175
+ console.log(`[storage] Migrated ${table}: ${Object.keys(data).length} records`)
176
+ }
177
+ } catch { /* skip malformed files */ }
178
+ }
179
+ }
180
+
181
+ // Settings (singleton)
182
+ const settingsPath = path.join(DATA_DIR, 'settings.json')
183
+ if (fs.existsSync(settingsPath)) {
184
+ try {
185
+ const data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
186
+ if (data && Object.keys(data).length > 0) {
187
+ saveSingleton('settings', data)
188
+ console.log('[storage] Migrated settings')
189
+ }
190
+ } catch { /* skip */ }
191
+ }
192
+
193
+ // Queue (singleton array)
194
+ const queuePath = path.join(DATA_DIR, 'queue.json')
195
+ if (fs.existsSync(queuePath)) {
196
+ try {
197
+ const data = JSON.parse(fs.readFileSync(queuePath, 'utf8'))
198
+ if (Array.isArray(data) && data.length > 0) {
199
+ saveSingleton('queue', data)
200
+ console.log(`[storage] Migrated queue: ${data.length} items`)
201
+ }
202
+ } catch { /* skip */ }
203
+ }
204
+
205
+ // Usage
206
+ const usagePath = path.join(DATA_DIR, 'usage.json')
207
+ if (fs.existsSync(usagePath)) {
208
+ try {
209
+ const data = JSON.parse(fs.readFileSync(usagePath, 'utf8'))
210
+ const ins = db.prepare(`INSERT INTO usage (session_id, data) VALUES (?, ?)`)
211
+ for (const [sessionId, records] of Object.entries(data)) {
212
+ if (Array.isArray(records)) {
213
+ for (const record of records) {
214
+ ins.run(sessionId, JSON.stringify(record))
215
+ }
216
+ }
217
+ }
218
+ console.log('[storage] Migrated usage records')
219
+ } catch { /* skip */ }
220
+ }
221
+ })
222
+
223
+ transaction()
224
+ fs.writeFileSync(MIGRATION_FLAG, new Date().toISOString())
225
+ console.log('[storage] Migration complete. JSON files preserved as backup.')
226
+ }
227
+
228
+ migrateFromJson()
229
+
230
+ // Seed default agent if agents table is empty
231
+ {
232
+ const defaultStarterTools = [
233
+ 'memory',
234
+ 'files',
235
+ 'web_search',
236
+ 'web_fetch',
237
+ 'browser',
238
+ 'manage_agents',
239
+ 'manage_tasks',
240
+ 'manage_schedules',
241
+ 'manage_skills',
242
+ 'manage_connectors',
243
+ 'manage_sessions',
244
+ 'manage_secrets',
245
+ 'manage_documents',
246
+ 'manage_webhooks',
247
+ 'claude_code',
248
+ 'codex_cli',
249
+ 'opencode_cli',
250
+ ]
251
+ const count = (db.prepare('SELECT COUNT(*) as c FROM agents').get() as { c: number }).c
252
+ if (count === 0) {
253
+ const defaultAgent = {
254
+ id: 'default',
255
+ name: 'Assistant',
256
+ description: 'A general-purpose AI assistant',
257
+ provider: 'claude-cli',
258
+ model: '',
259
+ systemPrompt: `You are the default SwarmClaw assistant. SwarmClaw is a self-hosted AI agent orchestration dashboard.
260
+
261
+ Help users get started with the platform:
262
+ - **Agents**: Create specialized AI agents (Agents tab → "+"). Each agent has a provider, model, system prompt, and optional tools (shell, files, web search, browser). Use "Generate with AI" to scaffold agents from a description.
263
+ - **Orchestrators**: Toggle "Orchestrator" when creating an agent to let it delegate tasks to other agents. Orchestrators coordinate multi-agent workflows automatically.
264
+ - **Providers**: Configure LLM backends in Settings → Providers. Built-in providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama (local or cloud), and OpenClaw. You can also add custom OpenAI-compatible endpoints.
265
+ - **Tasks**: Use the Task Board to create, assign, and track work items. Agents can be assigned to tasks and will execute them autonomously.
266
+ - **Schedules**: Set up cron-based schedules to run agents or tasks on a recurring basis (Schedules tab).
267
+ - **Skills**: Create reusable skill files (markdown instructions) in the Skills tab and attach them to agents to specialize their behavior.
268
+ - **Connectors**: Bridge agents to Discord, Slack, Telegram, or WhatsApp so they can respond in chat platforms.
269
+ - **Secrets**: Store API keys securely in the encrypted vault (Settings → Secrets).
270
+
271
+ ## Platform Tools
272
+
273
+ You have access to platform management tools. Here's how to use them:
274
+
275
+ - **manage_agents**: List, create, update, or delete agents. Use action "list" to see all agents, "create" with a JSON data payload to add new ones.
276
+ - **manage_tasks**: Create and manage task board items. Set "agentId" to assign a task to an agent, "status" to track progress (backlog → queued → running → completed/failed). Use action "create" with data like \`{"title": "...", "description": "...", "agentId": "...", "status": "backlog"}\`.
277
+ - **manage_schedules**: Create recurring or one-time scheduled jobs. Set "scheduleType" to "cron", "interval", or "once". Provide "taskPrompt" for what the agent should do and "agentId" for who runs it.
278
+ - **manage_skills**: List, create, or update reusable skill definitions that can be attached to agents.
279
+ - **manage_documents**: Upload/index/search long-lived docs (PDFs, markdown, notes) for retrieval.
280
+ - **manage_webhooks**: Register external webhook endpoints that trigger agent runs.
281
+ - **manage_connectors**: Manage chat platform bridges (Discord, Slack, Telegram, WhatsApp).
282
+ - **manage_sessions**: Session-level operations. Use \`sessions_tool\` to list sessions, send inter-session messages, spawn new agent sessions, and inspect status/history.
283
+ - **manage_secrets**: Store and retrieve encrypted service tokens/API credentials for durable reuse.
284
+ - **memory_tool**: Store and retrieve long-term memories. Use "store" to save knowledge, "search" to find relevant memories.
285
+
286
+ Be concise and helpful. When users ask how to do something, guide them to the specific UI location and explain the steps.`,
287
+ soul: '',
288
+ isOrchestrator: false,
289
+ tools: defaultStarterTools,
290
+ platformAssignScope: 'all',
291
+ skillIds: [],
292
+ subAgentIds: [],
293
+ createdAt: Date.now(),
294
+ updatedAt: Date.now(),
295
+ }
296
+ db.prepare(`INSERT OR REPLACE INTO agents (id, data) VALUES (?, ?)`).run('default', JSON.stringify(defaultAgent))
297
+ } else {
298
+ const row = db.prepare('SELECT data FROM agents WHERE id = ?').get('default') as { data: string } | undefined
299
+ if (row?.data) {
300
+ try {
301
+ const existing = JSON.parse(row.data) as Record<string, unknown>
302
+ const existingTools = Array.isArray(existing.tools) ? existing.tools : []
303
+ const mergedTools = Array.from(new Set([...existingTools, ...defaultStarterTools])).filter((t) => t !== 'delete_file')
304
+ if (JSON.stringify(existingTools) !== JSON.stringify(mergedTools)) {
305
+ existing.tools = mergedTools
306
+ existing.updatedAt = Date.now()
307
+ db.prepare('UPDATE agents SET data = ? WHERE id = ?').run(JSON.stringify(existing), 'default')
308
+ }
309
+ } catch {
310
+ // ignore malformed default agent payloads
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ // --- .env loading ---
317
+ function loadEnv() {
318
+ const envPath = path.join(process.cwd(), '.env.local')
319
+ if (fs.existsSync(envPath)) {
320
+ fs.readFileSync(envPath, 'utf8').split('\n').forEach(line => {
321
+ const [k, ...v] = line.split('=')
322
+ if (k && v.length) process.env[k.trim()] = v.join('=').trim()
323
+ })
324
+ }
325
+ }
326
+ loadEnv()
327
+
328
+ // Auto-generate CREDENTIAL_SECRET if missing
329
+ if (!process.env.CREDENTIAL_SECRET) {
330
+ const secret = crypto.randomBytes(32).toString('hex')
331
+ const envPath = path.join(process.cwd(), '.env.local')
332
+ fs.appendFileSync(envPath, `\nCREDENTIAL_SECRET=${secret}\n`)
333
+ process.env.CREDENTIAL_SECRET = secret
334
+ console.log('[credentials] Generated CREDENTIAL_SECRET in .env.local')
335
+ }
336
+
337
+ // Auto-generate ACCESS_KEY if missing (used for simple auth)
338
+ const SETUP_FLAG = path.join(DATA_DIR, '.setup_pending')
339
+ if (!process.env.ACCESS_KEY) {
340
+ const key = crypto.randomBytes(16).toString('hex')
341
+ const envPath = path.join(process.cwd(), '.env.local')
342
+ fs.appendFileSync(envPath, `\nACCESS_KEY=${key}\n`)
343
+ process.env.ACCESS_KEY = key
344
+ fs.writeFileSync(SETUP_FLAG, key)
345
+ console.log(`\n${'='.repeat(50)}`)
346
+ console.log(` ACCESS KEY: ${key}`)
347
+ console.log(` Use this key to connect from the browser.`)
348
+ console.log(`${'='.repeat(50)}\n`)
349
+ }
350
+
351
+ export function getAccessKey(): string {
352
+ return process.env.ACCESS_KEY || ''
353
+ }
354
+
355
+ export function validateAccessKey(key: string): boolean {
356
+ return key === process.env.ACCESS_KEY
357
+ }
358
+
359
+ export function isFirstTimeSetup(): boolean {
360
+ return fs.existsSync(SETUP_FLAG)
361
+ }
362
+
363
+ export function markSetupComplete(): void {
364
+ if (fs.existsSync(SETUP_FLAG)) fs.unlinkSync(SETUP_FLAG)
365
+ }
366
+
367
+ // --- Sessions ---
368
+ export function loadSessions(): Record<string, any> {
369
+ return loadCollection('sessions')
370
+ }
371
+
372
+ export function saveSessions(s: Record<string, any>) {
373
+ saveCollection('sessions', s)
374
+ }
375
+
376
+ export function disableAllSessionHeartbeats(): number {
377
+ const rows = db.prepare('SELECT id, data FROM sessions').all() as Array<{ id: string; data: string }>
378
+ if (!rows.length) return 0
379
+
380
+ const update = db.prepare('UPDATE sessions SET data = ? WHERE id = ?')
381
+ let changed = 0
382
+
383
+ const tx = db.transaction(() => {
384
+ for (const row of rows) {
385
+ let parsed: any
386
+ try {
387
+ parsed = JSON.parse(row.data)
388
+ } catch {
389
+ continue
390
+ }
391
+ if (!parsed || typeof parsed !== 'object') continue
392
+ if (parsed.heartbeatEnabled === false) continue
393
+
394
+ parsed.heartbeatEnabled = false
395
+ parsed.lastActiveAt = Date.now()
396
+ update.run(JSON.stringify(parsed), row.id)
397
+ changed += 1
398
+ }
399
+ })
400
+ tx()
401
+
402
+ return changed
403
+ }
404
+
405
+ // --- Credentials ---
406
+ export function loadCredentials(): Record<string, any> {
407
+ return loadCollection('credentials')
408
+ }
409
+
410
+ export function saveCredentials(c: Record<string, any>) {
411
+ saveCollection('credentials', c)
412
+ }
413
+
414
+ export function encryptKey(plaintext: string): string {
415
+ const key = Buffer.from(process.env.CREDENTIAL_SECRET!, 'hex')
416
+ const iv = crypto.randomBytes(12)
417
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
418
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex')
419
+ encrypted += cipher.final('hex')
420
+ const tag = cipher.getAuthTag().toString('hex')
421
+ return iv.toString('hex') + ':' + tag + ':' + encrypted
422
+ }
423
+
424
+ export function decryptKey(encrypted: string): string {
425
+ const key = Buffer.from(process.env.CREDENTIAL_SECRET!, 'hex')
426
+ const [ivHex, tagHex, data] = encrypted.split(':')
427
+ const iv = Buffer.from(ivHex, 'hex')
428
+ const tag = Buffer.from(tagHex, 'hex')
429
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
430
+ decipher.setAuthTag(tag)
431
+ let decrypted = decipher.update(data, 'hex', 'utf8')
432
+ decrypted += decipher.final('utf8')
433
+ return decrypted
434
+ }
435
+
436
+ // --- Agents ---
437
+ export function loadAgents(): Record<string, any> {
438
+ return loadCollection('agents')
439
+ }
440
+
441
+ export function saveAgents(p: Record<string, any>) {
442
+ saveCollection('agents', p)
443
+ }
444
+
445
+ // --- Schedules ---
446
+ export function loadSchedules(): Record<string, any> {
447
+ return loadCollection('schedules')
448
+ }
449
+
450
+ export function saveSchedules(s: Record<string, any>) {
451
+ saveCollection('schedules', s)
452
+ }
453
+
454
+ // --- Tasks ---
455
+ export function loadTasks(): Record<string, any> {
456
+ return loadCollection('tasks')
457
+ }
458
+
459
+ export function saveTasks(t: Record<string, any>) {
460
+ saveCollection('tasks', t)
461
+ }
462
+ export function upsertTask(id: string, task: any) {
463
+ upsertCollectionItem('tasks', id, task)
464
+ }
465
+ export function deleteTask(id: string) { deleteCollectionItem('tasks', id) }
466
+ export function deleteSession(id: string) { deleteCollectionItem('sessions', id) }
467
+ export function deleteAgent(id: string) { deleteCollectionItem('agents', id) }
468
+ export function deleteSchedule(id: string) { deleteCollectionItem('schedules', id) }
469
+ export function deleteSkill(id: string) { deleteCollectionItem('skills', id) }
470
+
471
+ // --- Queue ---
472
+ export function loadQueue(): string[] {
473
+ return loadSingleton('queue', [])
474
+ }
475
+
476
+ export function saveQueue(q: string[]) {
477
+ saveSingleton('queue', q)
478
+ }
479
+
480
+ // --- Settings ---
481
+ export function loadSettings(): Record<string, any> {
482
+ return loadSingleton('settings', {})
483
+ }
484
+
485
+ export function saveSettings(s: Record<string, any>) {
486
+ saveSingleton('settings', s)
487
+ }
488
+
489
+ // --- Secrets (service keys for orchestrators) ---
490
+ export function loadSecrets(): Record<string, any> {
491
+ return loadCollection('secrets')
492
+ }
493
+
494
+ export function saveSecrets(s: Record<string, any>) {
495
+ saveCollection('secrets', s)
496
+ }
497
+
498
+ export async function getSecret(key: string): Promise<{
499
+ id: string
500
+ name: string
501
+ service: string
502
+ value: string
503
+ scope: string
504
+ agentIds: string[]
505
+ createdAt: number
506
+ updatedAt: number
507
+ } | null> {
508
+ const needle = typeof key === 'string' ? key.trim().toLowerCase() : ''
509
+ if (!needle) return null
510
+
511
+ const secrets = loadSecrets()
512
+ const matches = Object.values(secrets).find((secret: any) => {
513
+ if (!secret || typeof secret !== 'object') return false
514
+ const id = typeof secret.id === 'string' ? secret.id.toLowerCase() : ''
515
+ const name = typeof secret.name === 'string' ? secret.name.toLowerCase() : ''
516
+ const service = typeof secret.service === 'string' ? secret.service.toLowerCase() : ''
517
+ return id === needle || name === needle || service === needle
518
+ }) as any | undefined
519
+
520
+ if (!matches) return null
521
+
522
+ try {
523
+ const decryptedValue =
524
+ typeof matches.encryptedValue === 'string'
525
+ ? decryptKey(matches.encryptedValue)
526
+ : (typeof matches.value === 'string' ? matches.value : '')
527
+ if (!decryptedValue) return null
528
+
529
+ return {
530
+ id: matches.id,
531
+ name: matches.name,
532
+ service: matches.service,
533
+ value: decryptedValue,
534
+ scope: matches.scope,
535
+ agentIds: Array.isArray(matches.agentIds) ? matches.agentIds : [],
536
+ createdAt: matches.createdAt,
537
+ updatedAt: matches.updatedAt,
538
+ }
539
+ } catch {
540
+ return null
541
+ }
542
+ }
543
+
544
+ // --- Provider Configs (custom providers) ---
545
+ export function loadProviderConfigs(): Record<string, any> {
546
+ return loadCollection('provider_configs')
547
+ }
548
+
549
+ export function saveProviderConfigs(p: Record<string, any>) {
550
+ saveCollection('provider_configs', p)
551
+ }
552
+
553
+ // --- Model Overrides (user-added models for built-in providers) ---
554
+ export function loadModelOverrides(): Record<string, string[]> {
555
+ return loadCollection('model_overrides') as Record<string, string[]>
556
+ }
557
+
558
+ export function saveModelOverrides(m: Record<string, string[]>) {
559
+ saveCollection('model_overrides', m)
560
+ }
561
+
562
+ // --- Skills ---
563
+ export function loadSkills(): Record<string, any> {
564
+ return loadCollection('skills')
565
+ }
566
+
567
+ export function saveSkills(s: Record<string, any>) {
568
+ saveCollection('skills', s)
569
+ }
570
+
571
+ // --- Usage ---
572
+ export function loadUsage(): Record<string, any[]> {
573
+ const stmt = db.prepare('SELECT session_id, data FROM usage')
574
+ const rows = stmt.all() as { session_id: string; data: string }[]
575
+ const result: Record<string, any[]> = {}
576
+ for (const row of rows) {
577
+ if (!result[row.session_id]) result[row.session_id] = []
578
+ result[row.session_id].push(JSON.parse(row.data))
579
+ }
580
+ return result
581
+ }
582
+
583
+ export function saveUsage(u: Record<string, any[]>) {
584
+ const del = db.prepare('DELETE FROM usage')
585
+ const ins = db.prepare('INSERT INTO usage (session_id, data) VALUES (?, ?)')
586
+ const transaction = db.transaction(() => {
587
+ del.run()
588
+ for (const [sessionId, records] of Object.entries(u)) {
589
+ for (const record of records) {
590
+ ins.run(sessionId, JSON.stringify(record))
591
+ }
592
+ }
593
+ })
594
+ transaction()
595
+ }
596
+
597
+ export function appendUsage(sessionId: string, record: any) {
598
+ const ins = db.prepare('INSERT INTO usage (session_id, data) VALUES (?, ?)')
599
+ ins.run(sessionId, JSON.stringify(record))
600
+ }
601
+
602
+ // --- Connectors ---
603
+ export function loadConnectors(): Record<string, any> {
604
+ return loadCollection('connectors')
605
+ }
606
+
607
+ export function saveConnectors(c: Record<string, any>) {
608
+ saveCollection('connectors', c)
609
+ }
610
+
611
+ // --- Documents ---
612
+ export function loadDocuments(): Record<string, any> {
613
+ return loadCollection('documents')
614
+ }
615
+
616
+ export function saveDocuments(d: Record<string, any>) {
617
+ saveCollection('documents', d)
618
+ }
619
+
620
+ // --- Webhooks ---
621
+ export function loadWebhooks(): Record<string, any> {
622
+ return loadCollection('webhooks')
623
+ }
624
+
625
+ export function saveWebhooks(w: Record<string, any>) {
626
+ saveCollection('webhooks', w)
627
+ }
628
+
629
+ // --- Active processes ---
630
+ export const active = new Map<string, any>()
631
+ export const devServers = new Map<string, { proc: any; url: string }>()
632
+
633
+ // --- Utilities ---
634
+ export function localIP(): string {
635
+ for (const ifaces of Object.values(os.networkInterfaces())) {
636
+ if (!ifaces) continue
637
+ for (const i of ifaces) {
638
+ if (i.family === 'IPv4' && !i.internal) return i.address
639
+ }
640
+ }
641
+ return 'localhost'
642
+ }
643
+
644
+ // --- MCP Servers ---
645
+ export function loadMcpServers(): Record<string, any> {
646
+ return loadCollection('mcp_servers')
647
+ }
648
+
649
+ export function saveMcpServers(m: Record<string, any>) {
650
+ saveCollection('mcp_servers', m)
651
+ }
652
+
653
+ export function deleteMcpServer(id: string) { deleteCollectionItem('mcp_servers', id) }
654
+
655
+ // --- Webhook Logs ---
656
+ export function loadWebhookLogs(): Record<string, any> {
657
+ return loadCollection('webhook_logs')
658
+ }
659
+
660
+ export function appendWebhookLog(id: string, entry: any) {
661
+ upsertCollectionItem('webhook_logs', id, entry)
662
+ }
663
+
664
+ export function getSessionMessages(sessionId: string): any[] {
665
+ const stmt = db.prepare('SELECT data FROM sessions WHERE id = ?')
666
+ const row = stmt.get(sessionId) as { data: string } | undefined
667
+ if (!row) return []
668
+ const session = JSON.parse(row.data)
669
+ return session?.messages || []
670
+ }