@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,441 @@
1
+ import { describe, it, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import os from 'os'
4
+ import path from 'path'
5
+ import fs from 'fs'
6
+ import crypto from 'crypto'
7
+ import Database from 'better-sqlite3'
8
+ import type { MemoryEntry } from '@/types'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Portable test harness:
12
+ // We replicate the minimal schema from memory-db.ts and build thin wrappers
13
+ // equivalent to addKnowledge / searchKnowledge / listKnowledge so we can test
14
+ // the knowledge helpers' logic against an isolated temp SQLite file without
15
+ // pulling in the full module singleton (which depends on storage.ts, embeddings,
16
+ // and a hardcoded DB_PATH).
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const tmpDbPath = path.join(
20
+ os.tmpdir(),
21
+ `knowledge-test-${crypto.randomBytes(4).toString('hex')}.db`,
22
+ )
23
+
24
+ let db: ReturnType<typeof Database>
25
+
26
+ // ---- Schema (mirrors initDb in memory-db.ts) ----
27
+ function createSchema(d: ReturnType<typeof Database>) {
28
+ d.pragma('journal_mode = WAL')
29
+ d.exec(`
30
+ CREATE TABLE IF NOT EXISTS memories (
31
+ id TEXT PRIMARY KEY,
32
+ agentId TEXT,
33
+ sessionId TEXT,
34
+ category TEXT NOT NULL DEFAULT 'note',
35
+ title TEXT NOT NULL,
36
+ content TEXT NOT NULL DEFAULT '',
37
+ metadata TEXT,
38
+ embedding BLOB,
39
+ "references" TEXT,
40
+ filePaths TEXT,
41
+ image TEXT,
42
+ imagePath TEXT,
43
+ linkedMemoryIds TEXT,
44
+ createdAt INTEGER NOT NULL,
45
+ updatedAt INTEGER NOT NULL
46
+ )
47
+ `)
48
+ d.exec(`
49
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
50
+ title, content, category,
51
+ content='memories',
52
+ content_rowid='rowid'
53
+ )
54
+ `)
55
+ d.exec(`
56
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
57
+ INSERT INTO memories_fts(rowid, title, content, category)
58
+ VALUES (new.rowid, new.title, new.content, new.category);
59
+ END
60
+ `)
61
+ d.exec(`
62
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
63
+ INSERT INTO memories_fts(memories_fts, rowid, title, content, category)
64
+ VALUES ('delete', old.rowid, old.title, old.content, old.category);
65
+ END
66
+ `)
67
+ d.exec(`
68
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
69
+ INSERT INTO memories_fts(memories_fts, rowid, title, content, category)
70
+ VALUES ('delete', old.rowid, old.title, old.content, old.category);
71
+ INSERT INTO memories_fts(rowid, title, content, category)
72
+ VALUES (new.rowid, new.title, new.content, new.category);
73
+ END
74
+ `)
75
+ d.exec(`CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updatedAt DESC)`)
76
+ d.exec(`CREATE INDEX IF NOT EXISTS idx_memories_agent_updated_at ON memories(agentId, updatedAt DESC)`)
77
+ }
78
+
79
+ // ---- Prepared statements ----
80
+ let stmts: {
81
+ insert: ReturnType<ReturnType<typeof Database>['prepare']>
82
+ listAll: ReturnType<ReturnType<typeof Database>['prepare']>
83
+ listByAgent: ReturnType<ReturnType<typeof Database>['prepare']>
84
+ search: ReturnType<ReturnType<typeof Database>['prepare']>
85
+ searchByAgent: ReturnType<ReturnType<typeof Database>['prepare']>
86
+ }
87
+
88
+ function parseJsonSafe<T>(value: unknown, fallback: T): T {
89
+ if (typeof value !== 'string' || !value.trim()) return fallback
90
+ try { return JSON.parse(value) as T } catch { return fallback }
91
+ }
92
+
93
+ function rowToEntry(row: Record<string, unknown>): MemoryEntry {
94
+ return {
95
+ id: String(row.id || ''),
96
+ agentId: typeof row.agentId === 'string' ? row.agentId : null,
97
+ sessionId: typeof row.sessionId === 'string' ? row.sessionId : null,
98
+ category: typeof row.category === 'string' ? row.category : 'note',
99
+ title: typeof row.title === 'string' ? row.title : 'Untitled',
100
+ content: typeof row.content === 'string' ? row.content : '',
101
+ metadata: parseJsonSafe<Record<string, unknown> | undefined>(row.metadata, undefined),
102
+ createdAt: typeof row.createdAt === 'number' ? row.createdAt : Date.now(),
103
+ updatedAt: typeof row.updatedAt === 'number' ? row.updatedAt : Date.now(),
104
+ }
105
+ }
106
+
107
+ // ---- Knowledge helpers (mirrors memory-db.ts exported functions) ----
108
+
109
+ const MEMORY_FTS_STOP_WORDS = new Set([
110
+ 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
111
+ 'i', 'if', 'in', 'is', 'it', 'of', 'on', 'or', 'that', 'the', 'this',
112
+ 'to', 'was', 'we', 'were', 'what', 'when', 'where', 'which', 'who', 'with',
113
+ 'you', 'your',
114
+ ])
115
+ const MAX_FTS_QUERY_TERMS = 6
116
+ const MAX_FTS_TERM_LENGTH = 48
117
+
118
+ function buildFtsQuery(input: string): string {
119
+ const tokens = String(input || '')
120
+ .toLowerCase()
121
+ .match(/[a-z0-9][a-z0-9._:/-]*/g) || []
122
+ if (!tokens.length) return ''
123
+ const unique: string[] = []
124
+ const seen = new Set<string>()
125
+ for (const token of tokens) {
126
+ const term = token.slice(0, MAX_FTS_TERM_LENGTH)
127
+ if (term.length < 3) continue
128
+ if (MEMORY_FTS_STOP_WORDS.has(term)) continue
129
+ if (seen.has(term)) continue
130
+ seen.add(term)
131
+ unique.push(term)
132
+ if (unique.length >= MAX_FTS_QUERY_TERMS) break
133
+ }
134
+ if (unique.length === 1) {
135
+ return unique[0].length >= 5 ? `"${unique[0].replace(/"/g, '')}"` : ''
136
+ }
137
+ const selected = unique.slice(0, Math.min(4, MAX_FTS_QUERY_TERMS))
138
+ return selected.map((term) => `"${term.replace(/"/g, '')}"`).join(' AND ')
139
+ }
140
+
141
+ function addRawMemory(data: {
142
+ agentId?: string | null
143
+ sessionId?: string | null
144
+ category: string
145
+ title: string
146
+ content: string
147
+ metadata?: Record<string, unknown>
148
+ }): MemoryEntry {
149
+ const id = crypto.randomBytes(6).toString('hex')
150
+ const now = Date.now()
151
+ stmts.insert.run(
152
+ id,
153
+ data.agentId || null,
154
+ data.sessionId || null,
155
+ data.category,
156
+ data.title,
157
+ data.content,
158
+ data.metadata ? JSON.stringify(data.metadata) : null,
159
+ now,
160
+ now,
161
+ )
162
+ return {
163
+ id,
164
+ agentId: data.agentId || null,
165
+ sessionId: data.sessionId || null,
166
+ category: data.category,
167
+ title: data.title,
168
+ content: data.content,
169
+ metadata: data.metadata,
170
+ createdAt: now,
171
+ updatedAt: now,
172
+ }
173
+ }
174
+
175
+ function addKnowledge(params: {
176
+ title: string
177
+ content: string
178
+ tags?: string[]
179
+ createdByAgentId?: string | null
180
+ createdBySessionId?: string | null
181
+ }): MemoryEntry {
182
+ return addRawMemory({
183
+ agentId: null,
184
+ sessionId: null,
185
+ category: 'knowledge',
186
+ title: params.title,
187
+ content: params.content,
188
+ metadata: {
189
+ tags: params.tags || [],
190
+ createdByAgentId: params.createdByAgentId || null,
191
+ createdBySessionId: params.createdBySessionId || null,
192
+ },
193
+ })
194
+ }
195
+
196
+ function searchKnowledge(query: string, tags?: string[], limit?: number): MemoryEntry[] {
197
+ const ftsQuery = buildFtsQuery(query)
198
+ if (!ftsQuery) return []
199
+ const rows = (stmts.search.all(ftsQuery) as Record<string, unknown>[]).map(rowToEntry)
200
+ let filtered = rows.filter((e) => e.category === 'knowledge')
201
+ if (tags && tags.length > 0) {
202
+ const tagSet = new Set(tags.map((t) => t.toLowerCase()))
203
+ filtered = filtered.filter((e) => {
204
+ const entryTags: string[] = (e.metadata as Record<string, unknown>)?.tags as string[] || []
205
+ return entryTags.some((t) => tagSet.has(t.toLowerCase()))
206
+ })
207
+ }
208
+ if (limit && limit > 0) {
209
+ filtered = filtered.slice(0, limit)
210
+ }
211
+ return filtered
212
+ }
213
+
214
+ function listKnowledge(tags?: string[], limit?: number): MemoryEntry[] {
215
+ const rows = (stmts.listAll.all(500) as Record<string, unknown>[]).map(rowToEntry)
216
+ let filtered = rows.filter((e) => e.category === 'knowledge')
217
+ if (tags && tags.length > 0) {
218
+ const tagSet = new Set(tags.map((t) => t.toLowerCase()))
219
+ filtered = filtered.filter((e) => {
220
+ const entryTags: string[] = (e.metadata as Record<string, unknown>)?.tags as string[] || []
221
+ return entryTags.some((t) => tagSet.has(t.toLowerCase()))
222
+ })
223
+ }
224
+ if (limit && limit > 0) {
225
+ filtered = filtered.slice(0, limit)
226
+ }
227
+ return filtered
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Setup / teardown
232
+ // ---------------------------------------------------------------------------
233
+ before(() => {
234
+ db = new Database(tmpDbPath)
235
+ createSchema(db)
236
+ stmts = {
237
+ insert: db.prepare(`
238
+ INSERT INTO memories (id, agentId, sessionId, category, title, content, metadata, createdAt, updatedAt)
239
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
240
+ `),
241
+ listAll: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC LIMIT ?`),
242
+ listByAgent: db.prepare(`SELECT * FROM memories WHERE agentId=? ORDER BY updatedAt DESC LIMIT ?`),
243
+ search: db.prepare(`
244
+ SELECT m.* FROM memories m
245
+ INNER JOIN memories_fts f ON m.rowid = f.rowid
246
+ WHERE memories_fts MATCH ?
247
+ LIMIT 30
248
+ `),
249
+ searchByAgent: db.prepare(`
250
+ SELECT m.* FROM memories m
251
+ INNER JOIN memories_fts f ON m.rowid = f.rowid
252
+ WHERE memories_fts MATCH ? AND m.agentId = ?
253
+ LIMIT 30
254
+ `),
255
+ }
256
+ })
257
+
258
+ after(() => {
259
+ try { db?.close() } catch { /* ok */ }
260
+ try { fs.unlinkSync(tmpDbPath) } catch { /* ok */ }
261
+ // WAL / SHM files
262
+ try { fs.unlinkSync(tmpDbPath + '-wal') } catch { /* ok */ }
263
+ try { fs.unlinkSync(tmpDbPath + '-shm') } catch { /* ok */ }
264
+ })
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // 1. addKnowledge
268
+ // ---------------------------------------------------------------------------
269
+ describe('addKnowledge', () => {
270
+ it('creates entry with category=knowledge and agentId=null', () => {
271
+ const entry = addKnowledge({ title: 'CatK', content: 'body' })
272
+ assert.equal(entry.category, 'knowledge')
273
+ assert.equal(entry.agentId, null)
274
+ })
275
+
276
+ it('stores title and content correctly', () => {
277
+ const entry = addKnowledge({ title: 'My Title', content: 'My Content' })
278
+ assert.equal(entry.title, 'My Title')
279
+ assert.equal(entry.content, 'My Content')
280
+ })
281
+
282
+ it('stores tags in metadata', () => {
283
+ const entry = addKnowledge({ title: 'Tagged', content: 'c', tags: ['alpha', 'beta'] })
284
+ const meta = entry.metadata as Record<string, unknown>
285
+ assert.deepEqual(meta.tags, ['alpha', 'beta'])
286
+ })
287
+
288
+ it('returns a valid hex ID', () => {
289
+ const entry = addKnowledge({ title: 'IDcheck', content: 'x' })
290
+ assert.ok(entry.id)
291
+ assert.match(entry.id, /^[0-9a-f]+$/)
292
+ })
293
+
294
+ it('stores createdByAgentId and createdBySessionId in metadata', () => {
295
+ const entry = addKnowledge({
296
+ title: 'MetaEntry',
297
+ content: 'body',
298
+ createdByAgentId: 'agent-1',
299
+ createdBySessionId: 'session-1',
300
+ })
301
+ const meta = entry.metadata as Record<string, unknown>
302
+ assert.equal(meta.createdByAgentId, 'agent-1')
303
+ assert.equal(meta.createdBySessionId, 'session-1')
304
+ })
305
+
306
+ it('defaults tags to empty array when omitted', () => {
307
+ const entry = addKnowledge({ title: 'NoTags', content: 'c' })
308
+ const meta = entry.metadata as Record<string, unknown>
309
+ assert.deepEqual(meta.tags, [])
310
+ })
311
+
312
+ it('defaults createdByAgentId/createdBySessionId to null when omitted', () => {
313
+ const entry = addKnowledge({ title: 'NullCreator', content: 'c' })
314
+ const meta = entry.metadata as Record<string, unknown>
315
+ assert.equal(meta.createdByAgentId, null)
316
+ assert.equal(meta.createdBySessionId, null)
317
+ })
318
+ })
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // 2. searchKnowledge
322
+ // ---------------------------------------------------------------------------
323
+ describe('searchKnowledge', () => {
324
+ before(() => {
325
+ addKnowledge({ title: 'Quantum physics overview', content: 'Entanglement is a quantum phenomenon', tags: ['science'] })
326
+ addKnowledge({ title: 'Cooking pasta recipe', content: 'Boil water and add pasta noodles', tags: ['cooking'] })
327
+ addKnowledge({ title: 'Quantum computing primer', content: 'Qubits leverage superposition for computing', tags: ['science', 'tech'] })
328
+ })
329
+
330
+ it('FTS search finds entries by content', () => {
331
+ const results = searchKnowledge('entanglement quantum phenomenon')
332
+ assert.ok(results.length > 0)
333
+ assert.ok(results.some(e => e.title === 'Quantum physics overview'))
334
+ })
335
+
336
+ it('FTS search finds entries by title', () => {
337
+ const results = searchKnowledge('quantum physics overview')
338
+ assert.ok(results.length > 0)
339
+ assert.ok(results.some(e => e.title.includes('Quantum')))
340
+ })
341
+
342
+ it('tag filter only returns entries with matching tag', () => {
343
+ const results = searchKnowledge('quantum', ['tech'])
344
+ for (const r of results) {
345
+ const tags: string[] = (r.metadata as Record<string, unknown>)?.tags as string[] || []
346
+ assert.ok(tags.some(t => t.toLowerCase() === 'tech'))
347
+ }
348
+ })
349
+
350
+ it('limit parameter works', () => {
351
+ const results = searchKnowledge('quantum computing', undefined, 1)
352
+ assert.ok(results.length <= 1)
353
+ })
354
+
355
+ it('no results for non-matching query', () => {
356
+ const results = searchKnowledge('xylophone orchestration symphony')
357
+ assert.equal(results.length, 0)
358
+ })
359
+ })
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // 3. listKnowledge
363
+ // ---------------------------------------------------------------------------
364
+ describe('listKnowledge', () => {
365
+ it('lists all knowledge entries', () => {
366
+ const all = listKnowledge()
367
+ assert.ok(all.length > 0)
368
+ for (const e of all) {
369
+ assert.equal(e.category, 'knowledge')
370
+ }
371
+ })
372
+
373
+ it('tag filter works', () => {
374
+ const filtered = listKnowledge(['cooking'])
375
+ assert.ok(filtered.length > 0)
376
+ for (const e of filtered) {
377
+ const tags: string[] = (e.metadata as Record<string, unknown>)?.tags as string[] || []
378
+ assert.ok(tags.some(t => t.toLowerCase() === 'cooking'))
379
+ }
380
+ })
381
+
382
+ it('limit parameter works', () => {
383
+ const limited = listKnowledge(undefined, 2)
384
+ assert.ok(limited.length <= 2)
385
+ })
386
+
387
+ it('returns entries sorted by updatedAt desc', () => {
388
+ const all = listKnowledge()
389
+ for (let i = 1; i < all.length; i++) {
390
+ assert.ok(all[i - 1].updatedAt >= all[i].updatedAt)
391
+ }
392
+ })
393
+ })
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // 4. Isolation between knowledge and agent memory
397
+ // ---------------------------------------------------------------------------
398
+ describe('isolation between knowledge and agent memory', () => {
399
+ before(() => {
400
+ // Add a regular agent memory entry.
401
+ addRawMemory({
402
+ agentId: 'agent-xyz',
403
+ sessionId: null,
404
+ category: 'note',
405
+ title: 'Agent private quantum data',
406
+ content: 'Secret quantum agent information entanglement',
407
+ })
408
+ })
409
+
410
+ it('regular memory entries (with agentId) do not appear in knowledge list', () => {
411
+ const knowledgeList = listKnowledge()
412
+ for (const e of knowledgeList) {
413
+ assert.equal(e.agentId, null)
414
+ assert.equal(e.category, 'knowledge')
415
+ }
416
+ })
417
+
418
+ it('regular memory entries do not appear in knowledge search', () => {
419
+ const results = searchKnowledge('quantum entanglement')
420
+ for (const e of results) {
421
+ assert.equal(e.category, 'knowledge')
422
+ }
423
+ })
424
+
425
+ it('knowledge entries do not appear in agent-scoped memory list', () => {
426
+ const agentMemories = (stmts.listByAgent.all('agent-xyz', 500) as Record<string, unknown>[]).map(rowToEntry)
427
+ for (const e of agentMemories) {
428
+ assert.notEqual(e.category, 'knowledge')
429
+ assert.equal(e.agentId, 'agent-xyz')
430
+ }
431
+ })
432
+
433
+ it('knowledge entries do not appear in agent-scoped search', () => {
434
+ const ftsQuery = buildFtsQuery('quantum entanglement')
435
+ if (!ftsQuery) return
436
+ const agentResults = (stmts.searchByAgent.all(ftsQuery, 'agent-xyz') as Record<string, unknown>[]).map(rowToEntry)
437
+ for (const e of agentResults) {
438
+ assert.equal(e.agentId, 'agent-xyz')
439
+ }
440
+ })
441
+ })
@@ -0,0 +1,47 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ import { DATA_DIR } from './data-dir'
5
+
6
+ const LOG_FILE = path.join(DATA_DIR, 'app.log')
7
+ const MAX_SIZE = 5 * 1024 * 1024 // 5MB — rotate when exceeded
8
+
9
+ function rotate() {
10
+ try {
11
+ const stat = fs.statSync(LOG_FILE)
12
+ if (stat.size > MAX_SIZE) {
13
+ const old = LOG_FILE + '.old'
14
+ if (fs.existsSync(old)) fs.unlinkSync(old)
15
+ fs.renameSync(LOG_FILE, old)
16
+ }
17
+ } catch {
18
+ // file doesn't exist yet, fine
19
+ }
20
+ }
21
+
22
+ function write(level: string, tag: string, message: string, data?: unknown) {
23
+ const ts = new Date().toISOString()
24
+ let line = `[${ts}] [${level}] [${tag}] ${message}`
25
+ if (data !== undefined) {
26
+ try {
27
+ const s = typeof data === 'string' ? data : JSON.stringify(data, null, 0)
28
+ line += ' | ' + s.slice(0, 2000)
29
+ } catch {
30
+ line += ' | [unserializable]'
31
+ }
32
+ }
33
+ line += '\n'
34
+ try {
35
+ rotate()
36
+ fs.appendFileSync(LOG_FILE, line)
37
+ } catch (e) {
38
+ console.error('[logger] write failed:', e)
39
+ }
40
+ }
41
+
42
+ export const log = {
43
+ info: (tag: string, msg: string, data?: unknown) => write('INFO', tag, msg, data),
44
+ warn: (tag: string, msg: string, data?: unknown) => write('WARN', tag, msg, data),
45
+ error: (tag: string, msg: string, data?: unknown) => write('ERROR', tag, msg, data),
46
+ debug: (tag: string, msg: string, data?: unknown) => write('DEBUG', tag, msg, data),
47
+ }