@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.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- 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
|
+
}
|