synabun 2026.3.307 → 2026.3.310
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/CLAUDE.md +150 -0
- package/hooks/claude-code/post-plan.mjs +1 -1
- package/hooks/claude-code/session-start.mjs +2 -0
- package/mcp-server/dist/config.d.ts +13 -0
- package/mcp-server/dist/config.d.ts.map +1 -0
- package/mcp-server/dist/config.js +59 -0
- package/mcp-server/dist/config.js.map +1 -0
- package/mcp-server/dist/http.d.ts +13 -0
- package/mcp-server/dist/http.d.ts.map +1 -0
- package/mcp-server/dist/http.js +86 -0
- package/mcp-server/dist/http.js.map +1 -0
- package/mcp-server/dist/index.d.ts +10 -0
- package/mcp-server/dist/index.d.ts.map +1 -0
- package/mcp-server/dist/index.js +189 -0
- package/mcp-server/dist/index.js.map +1 -0
- package/mcp-server/dist/preload-http.d.ts +3 -0
- package/mcp-server/dist/preload-http.d.ts.map +1 -0
- package/mcp-server/dist/preload-http.js +26 -0
- package/mcp-server/dist/preload-http.js.map +1 -0
- package/mcp-server/dist/preload.d.ts +3 -0
- package/mcp-server/dist/preload.d.ts.map +1 -0
- package/mcp-server/dist/preload.js +30 -0
- package/mcp-server/dist/preload.js.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
- package/mcp-server/dist/services/categories.d.ts +61 -0
- package/mcp-server/dist/services/categories.d.ts.map +1 -0
- package/mcp-server/dist/services/categories.js +356 -0
- package/mcp-server/dist/services/categories.js.map +1 -0
- package/mcp-server/dist/services/discord.d.ts +112 -0
- package/mcp-server/dist/services/discord.d.ts.map +1 -0
- package/mcp-server/dist/services/discord.js +477 -0
- package/mcp-server/dist/services/discord.js.map +1 -0
- package/mcp-server/dist/services/file-checksums.d.ts +14 -0
- package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
- package/mcp-server/dist/services/file-checksums.js +34 -0
- package/mcp-server/dist/services/file-checksums.js.map +1 -0
- package/mcp-server/dist/services/local-embeddings.d.ts +23 -0
- package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/services/local-embeddings.js +71 -0
- package/mcp-server/dist/services/local-embeddings.js.map +1 -0
- package/mcp-server/dist/services/neural-interface.d.ts +92 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
- package/mcp-server/dist/services/neural-interface.js +238 -0
- package/mcp-server/dist/services/neural-interface.js.map +1 -0
- package/mcp-server/dist/services/sqlite.d.ts +85 -0
- package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
- package/mcp-server/dist/services/sqlite.js +660 -0
- package/mcp-server/dist/services/sqlite.js.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts +54 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.js +97 -0
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
- package/mcp-server/dist/tools/browser-interact.d.ts +138 -0
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-interact.js +216 -0
- package/mcp-server/dist/tools/browser-interact.js.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts +52 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.js +61 -0
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
- package/mcp-server/dist/tools/browser-observe.d.ts +298 -0
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-observe.js +1154 -0
- package/mcp-server/dist/tools/browser-observe.js.map +1 -0
- package/mcp-server/dist/tools/browser.d.ts +7 -0
- package/mcp-server/dist/tools/browser.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser.js +55 -0
- package/mcp-server/dist/tools/browser.js.map +1 -0
- package/mcp-server/dist/tools/card-tools.d.ts +82 -0
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/card-tools.js +137 -0
- package/mcp-server/dist/tools/card-tools.js.map +1 -0
- package/mcp-server/dist/tools/card.d.ts +8 -0
- package/mcp-server/dist/tools/card.d.ts.map +1 -0
- package/mcp-server/dist/tools/card.js +14 -0
- package/mcp-server/dist/tools/card.js.map +1 -0
- package/mcp-server/dist/tools/category.d.ts +32 -0
- package/mcp-server/dist/tools/category.d.ts.map +1 -0
- package/mcp-server/dist/tools/category.js +290 -0
- package/mcp-server/dist/tools/category.js.map +1 -0
- package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-channel.js +236 -0
- package/mcp-server/dist/tools/discord-channel.js.map +1 -0
- package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-guild.js +138 -0
- package/mcp-server/dist/tools/discord-guild.js.map +1 -0
- package/mcp-server/dist/tools/discord-member.d.ts +26 -0
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-member.js +122 -0
- package/mcp-server/dist/tools/discord-member.js.map +1 -0
- package/mcp-server/dist/tools/discord-message.d.ts +36 -0
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-message.js +184 -0
- package/mcp-server/dist/tools/discord-message.js.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.js +179 -0
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
- package/mcp-server/dist/tools/discord-role.d.ts +34 -0
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-role.js +178 -0
- package/mcp-server/dist/tools/discord-role.js.map +1 -0
- package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-thread.js +88 -0
- package/mcp-server/dist/tools/discord-thread.js.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.js +154 -0
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
- package/mcp-server/dist/tools/discord.d.ts +7 -0
- package/mcp-server/dist/tools/discord.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord.js +31 -0
- package/mcp-server/dist/tools/discord.js.map +1 -0
- package/mcp-server/dist/tools/forget.d.ts +14 -0
- package/mcp-server/dist/tools/forget.d.ts.map +1 -0
- package/mcp-server/dist/tools/forget.js +26 -0
- package/mcp-server/dist/tools/forget.js.map +1 -0
- package/mcp-server/dist/tools/git-tools.d.ts +24 -0
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/git-tools.js +107 -0
- package/mcp-server/dist/tools/git-tools.js.map +1 -0
- package/mcp-server/dist/tools/git.d.ts +7 -0
- package/mcp-server/dist/tools/git.d.ts.map +1 -0
- package/mcp-server/dist/tools/git.js +9 -0
- package/mcp-server/dist/tools/git.js.map +1 -0
- package/mcp-server/dist/tools/image-tools.d.ts +24 -0
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/image-tools.js +80 -0
- package/mcp-server/dist/tools/image-tools.js.map +1 -0
- package/mcp-server/dist/tools/image.d.ts +10 -0
- package/mcp-server/dist/tools/image.d.ts.map +1 -0
- package/mcp-server/dist/tools/image.js +12 -0
- package/mcp-server/dist/tools/image.js.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +77 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js +223 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
- package/mcp-server/dist/tools/leonardo.d.ts +13 -0
- package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo.js +19 -0
- package/mcp-server/dist/tools/leonardo.js.map +1 -0
- package/mcp-server/dist/tools/loop.d.ts +30 -0
- package/mcp-server/dist/tools/loop.d.ts.map +1 -0
- package/mcp-server/dist/tools/loop.js +302 -0
- package/mcp-server/dist/tools/loop.js.map +1 -0
- package/mcp-server/dist/tools/memories.d.ts +26 -0
- package/mcp-server/dist/tools/memories.d.ts.map +1 -0
- package/mcp-server/dist/tools/memories.js +96 -0
- package/mcp-server/dist/tools/memories.js.map +1 -0
- package/mcp-server/dist/tools/recall.d.ts +41 -0
- package/mcp-server/dist/tools/recall.d.ts.map +1 -0
- package/mcp-server/dist/tools/recall.js +275 -0
- package/mcp-server/dist/tools/recall.js.map +1 -0
- package/mcp-server/dist/tools/reflect.d.ts +44 -0
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
- package/mcp-server/dist/tools/reflect.js +118 -0
- package/mcp-server/dist/tools/reflect.js.map +1 -0
- package/mcp-server/dist/tools/remember.d.ts +38 -0
- package/mcp-server/dist/tools/remember.d.ts.map +1 -0
- package/mcp-server/dist/tools/remember.js +88 -0
- package/mcp-server/dist/tools/remember.js.map +1 -0
- package/mcp-server/dist/tools/response.d.ts +23 -0
- package/mcp-server/dist/tools/response.d.ts.map +1 -0
- package/mcp-server/dist/tools/response.js +24 -0
- package/mcp-server/dist/tools/response.js.map +1 -0
- package/mcp-server/dist/tools/restore.d.ts +14 -0
- package/mcp-server/dist/tools/restore.d.ts.map +1 -0
- package/mcp-server/dist/tools/restore.js +23 -0
- package/mcp-server/dist/tools/restore.js.map +1 -0
- package/mcp-server/dist/tools/sync.d.ts +18 -0
- package/mcp-server/dist/tools/sync.d.ts.map +1 -0
- package/mcp-server/dist/tools/sync.js +93 -0
- package/mcp-server/dist/tools/sync.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
- package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe.js +9 -0
- package/mcp-server/dist/tools/tictactoe.js.map +1 -0
- package/mcp-server/dist/tools/utils.d.ts +10 -0
- package/mcp-server/dist/tools/utils.d.ts.map +1 -0
- package/mcp-server/dist/tools/utils.js +30 -0
- package/mcp-server/dist/tools/utils.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +194 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.js +198 -0
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
- package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard.js +14 -0
- package/mcp-server/dist/tools/whiteboard.js.map +1 -0
- package/mcp-server/dist/tui.d.ts +3 -0
- package/mcp-server/dist/tui.d.ts.map +1 -0
- package/mcp-server/dist/tui.js +807 -0
- package/mcp-server/dist/tui.js.map +1 -0
- package/mcp-server/dist/types.d.ts +77 -0
- package/mcp-server/dist/types.d.ts.map +1 -0
- package/mcp-server/dist/types.js +2 -0
- package/mcp-server/dist/types.js.map +1 -0
- package/neural-interface/public/shared/styles.css +2 -2
- package/neural-interface/public/shared/ui-settings.js +2 -2
- package/neural-interface/server.js +82 -8
- package/neural-interface/templates/CLAUDE-template.md +9 -1
- package/package.json +4 -2
- package/preuninstall.js +28 -0
- package/setup.js +86 -1
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite storage layer.
|
|
3
|
+
* Uses Node.js built-in node:sqlite (available since Node 22.5.0).
|
|
4
|
+
* Vectors stored as Float32Array BLOBs, cosine similarity computed in JS.
|
|
5
|
+
*/
|
|
6
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import { config } from '../config.js';
|
|
10
|
+
import { getAllCategories } from './categories.js';
|
|
11
|
+
// Re-export for backward compat (was used by categories.ts)
|
|
12
|
+
export const CATEGORIES_POINT_ID = '00000000-0000-0000-0000-000000000000';
|
|
13
|
+
let db = null;
|
|
14
|
+
function getDbPath() {
|
|
15
|
+
const envPath = process.env.SQLITE_DB_PATH;
|
|
16
|
+
const defaultPath = path.join(config.dataDir, 'memory.db');
|
|
17
|
+
if (!envPath)
|
|
18
|
+
return defaultPath;
|
|
19
|
+
// If the env path's parent directory doesn't exist (e.g. Windows path on Mac),
|
|
20
|
+
// fall back to the local default so cross-OS restores work instantly
|
|
21
|
+
if (!fs.existsSync(path.dirname(envPath)))
|
|
22
|
+
return defaultPath;
|
|
23
|
+
return envPath;
|
|
24
|
+
}
|
|
25
|
+
function getDb() {
|
|
26
|
+
if (!db) {
|
|
27
|
+
db = new DatabaseSync(getDbPath());
|
|
28
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
29
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
30
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
31
|
+
}
|
|
32
|
+
return db;
|
|
33
|
+
}
|
|
34
|
+
// --- Vector encoding/decoding ---
|
|
35
|
+
function encodeVector(vector) {
|
|
36
|
+
const f32 = new Float32Array(vector);
|
|
37
|
+
return new Uint8Array(f32.buffer);
|
|
38
|
+
}
|
|
39
|
+
function decodeVector(blob) {
|
|
40
|
+
const f32 = new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
41
|
+
return Array.from(f32);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Cosine similarity for normalized vectors (equals dot product).
|
|
45
|
+
* Vectors from Transformers.js with normalize:true are unit-length.
|
|
46
|
+
*/
|
|
47
|
+
function cosineSimilarity(a, b) {
|
|
48
|
+
let dot = 0;
|
|
49
|
+
for (let i = 0; i < a.length; i++) {
|
|
50
|
+
dot += a[i] * b[i];
|
|
51
|
+
}
|
|
52
|
+
return dot;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Translates JSON filters to SQL WHERE clauses.
|
|
56
|
+
* Supports: exact match, range, tags (JSON array contains), is_empty.
|
|
57
|
+
*/
|
|
58
|
+
function translateFilter(filter) {
|
|
59
|
+
if (!filter)
|
|
60
|
+
return { where: '', params: [] };
|
|
61
|
+
const f = filter;
|
|
62
|
+
const clauses = [];
|
|
63
|
+
const params = [];
|
|
64
|
+
for (const cond of f.must || []) {
|
|
65
|
+
if (cond.match) {
|
|
66
|
+
if (cond.key === 'tags') {
|
|
67
|
+
clauses.push(`EXISTS (SELECT 1 FROM json_each(tags) WHERE json_each.value = ?)`);
|
|
68
|
+
params.push(cond.match.value);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
clauses.push(`${sanitizeColumn(cond.key)} = ?`);
|
|
72
|
+
params.push(cond.match.value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (cond.range) {
|
|
76
|
+
const col = sanitizeColumn(cond.key);
|
|
77
|
+
if (cond.range.gte !== undefined) {
|
|
78
|
+
clauses.push(`${col} >= ?`);
|
|
79
|
+
params.push(cond.range.gte);
|
|
80
|
+
}
|
|
81
|
+
if (cond.range.lte !== undefined) {
|
|
82
|
+
clauses.push(`${col} <= ?`);
|
|
83
|
+
params.push(cond.range.lte);
|
|
84
|
+
}
|
|
85
|
+
if (cond.range.gt !== undefined) {
|
|
86
|
+
clauses.push(`${col} > ?`);
|
|
87
|
+
params.push(cond.range.gt);
|
|
88
|
+
}
|
|
89
|
+
if (cond.range.lt !== undefined) {
|
|
90
|
+
clauses.push(`${col} < ?`);
|
|
91
|
+
params.push(cond.range.lt);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (cond.is_empty) {
|
|
95
|
+
clauses.push(`(${sanitizeColumn(cond.is_empty.key)} IS NULL OR ${sanitizeColumn(cond.is_empty.key)} = '')`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const cond of f.must_not || []) {
|
|
99
|
+
if (cond.match) {
|
|
100
|
+
clauses.push(`(${sanitizeColumn(cond.key)} IS NULL OR ${sanitizeColumn(cond.key)} != ?)`);
|
|
101
|
+
params.push(cond.match.value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
where: clauses.length > 0 ? ' AND ' + clauses.join(' AND ') : '',
|
|
106
|
+
params,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Allowlist of valid column names to prevent SQL injection */
|
|
110
|
+
const VALID_COLUMNS = new Set([
|
|
111
|
+
'id', 'content', 'category', 'subcategory', 'project', 'tags', 'importance',
|
|
112
|
+
'source', 'created_at', 'updated_at', 'accessed_at', 'access_count',
|
|
113
|
+
'related_files', 'related_memory_ids', 'file_checksums', 'trashed_at',
|
|
114
|
+
'source_session_chunks', '_type',
|
|
115
|
+
// session_chunks columns
|
|
116
|
+
'session_id', 'git_branch', 'cwd', 'chunk_index', 'start_timestamp',
|
|
117
|
+
'end_timestamp', 'tools_used', 'files_modified', 'files_read',
|
|
118
|
+
'user_messages', 'turn_count', 'dedup_memory_id', 'indexed_at', 'summary',
|
|
119
|
+
]);
|
|
120
|
+
function sanitizeColumn(name) {
|
|
121
|
+
if (!VALID_COLUMNS.has(name)) {
|
|
122
|
+
throw new Error(`Invalid column name: ${name}`);
|
|
123
|
+
}
|
|
124
|
+
return name;
|
|
125
|
+
}
|
|
126
|
+
// --- Schema initialization ---
|
|
127
|
+
const SCHEMA_SQL = `
|
|
128
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
vector BLOB NOT NULL,
|
|
131
|
+
content TEXT NOT NULL,
|
|
132
|
+
category TEXT NOT NULL,
|
|
133
|
+
subcategory TEXT,
|
|
134
|
+
project TEXT NOT NULL,
|
|
135
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
136
|
+
importance INTEGER NOT NULL DEFAULT 5,
|
|
137
|
+
source TEXT NOT NULL DEFAULT 'self-discovered',
|
|
138
|
+
created_at TEXT NOT NULL,
|
|
139
|
+
updated_at TEXT NOT NULL,
|
|
140
|
+
accessed_at TEXT NOT NULL,
|
|
141
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
142
|
+
related_files TEXT,
|
|
143
|
+
related_memory_ids TEXT,
|
|
144
|
+
file_checksums TEXT,
|
|
145
|
+
trashed_at TEXT,
|
|
146
|
+
source_session_chunks TEXT
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE IF NOT EXISTS session_chunks (
|
|
150
|
+
id TEXT PRIMARY KEY,
|
|
151
|
+
vector BLOB NOT NULL,
|
|
152
|
+
content TEXT NOT NULL,
|
|
153
|
+
summary TEXT,
|
|
154
|
+
session_id TEXT,
|
|
155
|
+
project TEXT,
|
|
156
|
+
git_branch TEXT,
|
|
157
|
+
cwd TEXT,
|
|
158
|
+
chunk_index INTEGER DEFAULT 0,
|
|
159
|
+
start_timestamp TEXT,
|
|
160
|
+
end_timestamp TEXT,
|
|
161
|
+
tools_used TEXT DEFAULT '[]',
|
|
162
|
+
files_modified TEXT DEFAULT '[]',
|
|
163
|
+
files_read TEXT DEFAULT '[]',
|
|
164
|
+
user_messages TEXT DEFAULT '[]',
|
|
165
|
+
turn_count INTEGER DEFAULT 0,
|
|
166
|
+
related_memory_ids TEXT DEFAULT '[]',
|
|
167
|
+
dedup_memory_id TEXT,
|
|
168
|
+
indexed_at TEXT
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
CREATE TABLE IF NOT EXISTS categories (
|
|
172
|
+
name TEXT PRIMARY KEY,
|
|
173
|
+
description TEXT NOT NULL,
|
|
174
|
+
created_at TEXT NOT NULL,
|
|
175
|
+
parent TEXT,
|
|
176
|
+
color TEXT,
|
|
177
|
+
is_parent INTEGER DEFAULT 0
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_mem_category ON memories(category);
|
|
181
|
+
CREATE INDEX IF NOT EXISTS idx_mem_project ON memories(project);
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_mem_importance ON memories(importance);
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_mem_trashed ON memories(trashed_at);
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_mem_created ON memories(created_at);
|
|
185
|
+
CREATE INDEX IF NOT EXISTS idx_mem_source ON memories(source);
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_sc_session ON session_chunks(session_id);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_sc_project ON session_chunks(project);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_sc_branch ON session_chunks(git_branch);
|
|
189
|
+
|
|
190
|
+
CREATE TABLE IF NOT EXISTS kv_config (
|
|
191
|
+
key TEXT PRIMARY KEY,
|
|
192
|
+
value TEXT NOT NULL
|
|
193
|
+
);
|
|
194
|
+
`;
|
|
195
|
+
// FTS5 created separately since CREATE VIRTUAL TABLE IF NOT EXISTS
|
|
196
|
+
// can fail silently on some SQLite versions
|
|
197
|
+
const FTS_SQL = `
|
|
198
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
199
|
+
content, category, project, tags,
|
|
200
|
+
content=memories, content_rowid=rowid,
|
|
201
|
+
tokenize='porter unicode61'
|
|
202
|
+
);
|
|
203
|
+
`;
|
|
204
|
+
// --- Public API: Collection initialization ---
|
|
205
|
+
export async function ensureCollection() {
|
|
206
|
+
const d = getDb();
|
|
207
|
+
d.exec(SCHEMA_SQL);
|
|
208
|
+
try {
|
|
209
|
+
d.exec(FTS_SQL);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// FTS5 may already exist or not be available
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export async function ensureSessionCollection() {
|
|
216
|
+
// No-op: session_chunks table is created in ensureCollection()
|
|
217
|
+
}
|
|
218
|
+
/** Combined init function for cleaner startup */
|
|
219
|
+
export async function ensureDatabase() {
|
|
220
|
+
await ensureCollection();
|
|
221
|
+
}
|
|
222
|
+
// --- Public API: Memory operations ---
|
|
223
|
+
export async function upsertMemory(id, vector, payload) {
|
|
224
|
+
const d = getDb();
|
|
225
|
+
const stmt = d.prepare(`
|
|
226
|
+
INSERT OR REPLACE INTO memories
|
|
227
|
+
(id, vector, content, category, subcategory, project, tags, importance, source,
|
|
228
|
+
created_at, updated_at, accessed_at, access_count, related_files,
|
|
229
|
+
related_memory_ids, file_checksums, trashed_at, source_session_chunks)
|
|
230
|
+
VALUES
|
|
231
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
232
|
+
`);
|
|
233
|
+
stmt.run(id, encodeVector(vector), payload.content, payload.category, payload.subcategory ?? null, payload.project, JSON.stringify(payload.tags || []), payload.importance ?? 5, payload.source || 'self-discovered', payload.created_at, payload.updated_at, payload.accessed_at, payload.access_count ?? 0, payload.related_files ? JSON.stringify(payload.related_files) : null, payload.related_memory_ids ? JSON.stringify(payload.related_memory_ids) : null, payload.file_checksums ? JSON.stringify(payload.file_checksums) : null, payload.trashed_at ?? null, payload.source_session_chunks ? JSON.stringify(payload.source_session_chunks) : null);
|
|
234
|
+
// Update FTS index
|
|
235
|
+
try {
|
|
236
|
+
d.prepare(`INSERT OR REPLACE INTO memories_fts(rowid, content, category, project, tags)
|
|
237
|
+
SELECT rowid, content, category, project, tags FROM memories WHERE id = ?`).run(id);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// FTS update failure is non-fatal
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export async function searchMemories(vector, limit, filter, scoreThreshold) {
|
|
244
|
+
const d = getDb();
|
|
245
|
+
const threshold = scoreThreshold ?? 0.3;
|
|
246
|
+
// Build WHERE clause from filter
|
|
247
|
+
const { where, params } = translateFilter(filter);
|
|
248
|
+
// Fetch all candidate rows (non-trashed) with their vectors
|
|
249
|
+
const rows = d.prepare(`
|
|
250
|
+
SELECT id, vector, content, category, subcategory, project, tags, importance, source,
|
|
251
|
+
created_at, updated_at, accessed_at, access_count, related_files,
|
|
252
|
+
related_memory_ids, file_checksums, trashed_at, source_session_chunks
|
|
253
|
+
FROM memories
|
|
254
|
+
WHERE trashed_at IS NULL${where}
|
|
255
|
+
`).all(...params);
|
|
256
|
+
// Compute cosine similarity and rank
|
|
257
|
+
const scored = rows.map((row) => {
|
|
258
|
+
const rowVector = decodeVector(row.vector);
|
|
259
|
+
const score = cosineSimilarity(vector, rowVector);
|
|
260
|
+
return { row, score };
|
|
261
|
+
}).filter((r) => r.score >= threshold);
|
|
262
|
+
scored.sort((a, b) => b.score - a.score);
|
|
263
|
+
return scored.slice(0, limit).map((r) => ({
|
|
264
|
+
id: r.row.id,
|
|
265
|
+
score: r.score,
|
|
266
|
+
payload: rowToPayload(r.row),
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
export async function getMemory(id) {
|
|
270
|
+
const d = getDb();
|
|
271
|
+
const row = d.prepare(`
|
|
272
|
+
SELECT id, content, category, subcategory, project, tags, importance, source,
|
|
273
|
+
created_at, updated_at, accessed_at, access_count, related_files,
|
|
274
|
+
related_memory_ids, file_checksums, trashed_at, source_session_chunks
|
|
275
|
+
FROM memories WHERE id = ?
|
|
276
|
+
`).get(id);
|
|
277
|
+
if (!row)
|
|
278
|
+
return null;
|
|
279
|
+
return { id: row.id, payload: rowToPayload(row) };
|
|
280
|
+
}
|
|
281
|
+
export async function updatePayload(id, payload) {
|
|
282
|
+
const d = getDb();
|
|
283
|
+
const sets = [];
|
|
284
|
+
const params = [];
|
|
285
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
286
|
+
if (key === 'id' || key === 'vector')
|
|
287
|
+
continue;
|
|
288
|
+
const col = sanitizeColumn(key);
|
|
289
|
+
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
|
|
290
|
+
sets.push(`${col} = ?`);
|
|
291
|
+
params.push(JSON.stringify(value));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
sets.push(`${col} = ?`);
|
|
295
|
+
params.push((value ?? null));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (sets.length === 0)
|
|
299
|
+
return;
|
|
300
|
+
params.push(id);
|
|
301
|
+
d.prepare(`UPDATE memories SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
302
|
+
// Update FTS if content changed
|
|
303
|
+
if (payload.content) {
|
|
304
|
+
try {
|
|
305
|
+
d.prepare(`INSERT OR REPLACE INTO memories_fts(rowid, content, category, project, tags)
|
|
306
|
+
SELECT rowid, content, category, project, tags FROM memories WHERE id = ?`).run(id);
|
|
307
|
+
}
|
|
308
|
+
catch { /* non-fatal */ }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
export async function updateVector(id, vector, payload) {
|
|
312
|
+
// Full upsert — replaces both vector and payload
|
|
313
|
+
await upsertMemory(id, vector, payload);
|
|
314
|
+
}
|
|
315
|
+
export async function updatePayloadByFilter(filter, payload) {
|
|
316
|
+
const d = getDb();
|
|
317
|
+
const { where, params: filterParams } = translateFilter(filter);
|
|
318
|
+
const sets = [];
|
|
319
|
+
const setParams = [];
|
|
320
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
321
|
+
if (key === 'id' || key === 'vector')
|
|
322
|
+
continue;
|
|
323
|
+
const col = sanitizeColumn(key);
|
|
324
|
+
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
|
|
325
|
+
sets.push(`${col} = ?`);
|
|
326
|
+
setParams.push(JSON.stringify(value));
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
sets.push(`${col} = ?`);
|
|
330
|
+
setParams.push((value ?? null));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (sets.length === 0)
|
|
334
|
+
return;
|
|
335
|
+
d.prepare(`UPDATE memories SET ${sets.join(', ')} WHERE trashed_at IS NULL${where}`)
|
|
336
|
+
.run(...setParams, ...filterParams);
|
|
337
|
+
}
|
|
338
|
+
export async function deleteMemory(id) {
|
|
339
|
+
const d = getDb();
|
|
340
|
+
d.prepare('DELETE FROM memories WHERE id = ?').run(id);
|
|
341
|
+
try {
|
|
342
|
+
d.prepare('DELETE FROM memories_fts WHERE rowid = (SELECT rowid FROM memories WHERE id = ?)').run(id);
|
|
343
|
+
}
|
|
344
|
+
catch { /* non-fatal */ }
|
|
345
|
+
}
|
|
346
|
+
export async function softDeleteMemory(id) {
|
|
347
|
+
await updatePayload(id, { trashed_at: new Date().toISOString() });
|
|
348
|
+
}
|
|
349
|
+
export async function restoreMemory(id) {
|
|
350
|
+
const d = getDb();
|
|
351
|
+
d.prepare('UPDATE memories SET trashed_at = NULL WHERE id = ?').run(id);
|
|
352
|
+
}
|
|
353
|
+
export async function scrollMemories(filter, limit = 20, offset) {
|
|
354
|
+
const d = getDb();
|
|
355
|
+
const { where, params } = translateFilter(filter);
|
|
356
|
+
const numericOffset = offset ? parseInt(offset, 10) : 0;
|
|
357
|
+
const rows = d.prepare(`
|
|
358
|
+
SELECT id, content, category, subcategory, project, tags, importance, source,
|
|
359
|
+
created_at, updated_at, accessed_at, access_count, related_files,
|
|
360
|
+
related_memory_ids, file_checksums, trashed_at, source_session_chunks
|
|
361
|
+
FROM memories
|
|
362
|
+
WHERE trashed_at IS NULL${where}
|
|
363
|
+
ORDER BY created_at DESC
|
|
364
|
+
LIMIT ? OFFSET ?
|
|
365
|
+
`).all(...params, limit, numericOffset);
|
|
366
|
+
const points = rows.map((row) => ({
|
|
367
|
+
id: row.id,
|
|
368
|
+
payload: rowToPayload(row),
|
|
369
|
+
}));
|
|
370
|
+
// Determine next offset
|
|
371
|
+
const nextOffset = rows.length === limit ? String(numericOffset + limit) : null;
|
|
372
|
+
return { points, next_page_offset: nextOffset };
|
|
373
|
+
}
|
|
374
|
+
export async function countMemories(filter) {
|
|
375
|
+
const d = getDb();
|
|
376
|
+
const { where, params } = translateFilter(filter);
|
|
377
|
+
const result = d.prepare(`
|
|
378
|
+
SELECT COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL${where}
|
|
379
|
+
`).get(...params);
|
|
380
|
+
return result.cnt;
|
|
381
|
+
}
|
|
382
|
+
export async function getMemoryStats() {
|
|
383
|
+
const d = getDb();
|
|
384
|
+
const totalRow = d.prepare('SELECT COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL').get();
|
|
385
|
+
const total = totalRow.cnt;
|
|
386
|
+
// Per-category counts using SQL aggregate
|
|
387
|
+
const catRows = d.prepare(`
|
|
388
|
+
SELECT category, COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL GROUP BY category
|
|
389
|
+
`).all();
|
|
390
|
+
const by_category = {};
|
|
391
|
+
// Initialize with all known categories at 0
|
|
392
|
+
for (const cat of getAllCategories()) {
|
|
393
|
+
by_category[cat] = 0;
|
|
394
|
+
}
|
|
395
|
+
for (const row of catRows) {
|
|
396
|
+
by_category[row.category] = row.cnt;
|
|
397
|
+
}
|
|
398
|
+
// Per-project counts
|
|
399
|
+
const projRows = d.prepare(`
|
|
400
|
+
SELECT project, COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL GROUP BY project
|
|
401
|
+
`).all();
|
|
402
|
+
const by_project = {};
|
|
403
|
+
for (const row of projRows) {
|
|
404
|
+
by_project[row.project || 'global'] = row.cnt;
|
|
405
|
+
}
|
|
406
|
+
// Oldest/newest
|
|
407
|
+
const dateRow = d.prepare(`
|
|
408
|
+
SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories WHERE trashed_at IS NULL
|
|
409
|
+
`).get();
|
|
410
|
+
return {
|
|
411
|
+
total,
|
|
412
|
+
by_category,
|
|
413
|
+
by_project,
|
|
414
|
+
oldest: dateRow.oldest ?? undefined,
|
|
415
|
+
newest: dateRow.newest ?? undefined,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// --- FTS5 full-text search ---
|
|
419
|
+
/**
|
|
420
|
+
* Search memories using FTS5 full-text search (keyword fallback).
|
|
421
|
+
* Returns results scored by BM25 relevance. Used when vector similarity
|
|
422
|
+
* scores are low (exact identifiers, error codes, proper nouns).
|
|
423
|
+
*/
|
|
424
|
+
export async function searchMemoriesFTS(query, limit, filter, excludeIds) {
|
|
425
|
+
const d = getDb();
|
|
426
|
+
// Sanitize query for FTS5: escape double quotes, wrap terms
|
|
427
|
+
const ftsQuery = query
|
|
428
|
+
.replace(/"/g, '""')
|
|
429
|
+
.split(/\s+/)
|
|
430
|
+
.filter(t => t.length > 1)
|
|
431
|
+
.map(t => `"${t}"`)
|
|
432
|
+
.join(' OR ');
|
|
433
|
+
if (!ftsQuery)
|
|
434
|
+
return [];
|
|
435
|
+
const { where, params } = translateFilter(filter);
|
|
436
|
+
try {
|
|
437
|
+
const rows = d.prepare(`
|
|
438
|
+
SELECT m.id, m.content, m.category, m.subcategory, m.project, m.tags,
|
|
439
|
+
m.importance, m.source, m.created_at, m.updated_at, m.accessed_at,
|
|
440
|
+
m.access_count, m.related_files, m.related_memory_ids,
|
|
441
|
+
m.file_checksums, m.trashed_at, m.source_session_chunks,
|
|
442
|
+
rank
|
|
443
|
+
FROM memories_fts fts
|
|
444
|
+
JOIN memories m ON m.rowid = fts.rowid
|
|
445
|
+
WHERE memories_fts MATCH ? AND m.trashed_at IS NULL${where}
|
|
446
|
+
ORDER BY rank
|
|
447
|
+
LIMIT ?
|
|
448
|
+
`).all(ftsQuery, ...params, limit);
|
|
449
|
+
return rows
|
|
450
|
+
.filter(row => !excludeIds || !excludeIds.has(row.id))
|
|
451
|
+
.map(row => {
|
|
452
|
+
// FTS5 rank is negative (lower = better match). Normalize to 0-1 range.
|
|
453
|
+
const rawRank = Math.abs(row.rank);
|
|
454
|
+
const normalizedScore = Math.min(1, rawRank / 10);
|
|
455
|
+
return {
|
|
456
|
+
id: row.id,
|
|
457
|
+
score: normalizedScore,
|
|
458
|
+
payload: rowToPayload(row),
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// FTS5 not available or query error — return empty
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// --- Session Chunks ---
|
|
468
|
+
export async function searchSessionChunks(vector, limit, filter, scoreThreshold = 0.3) {
|
|
469
|
+
const d = getDb();
|
|
470
|
+
const { where, params } = translateFilter(filter);
|
|
471
|
+
const rows = d.prepare(`
|
|
472
|
+
SELECT id, vector, content, summary, session_id, project, git_branch, cwd,
|
|
473
|
+
chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
|
|
474
|
+
files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at
|
|
475
|
+
FROM session_chunks
|
|
476
|
+
WHERE 1=1${where}
|
|
477
|
+
`).all(...params);
|
|
478
|
+
const scored = rows.map((row) => {
|
|
479
|
+
const rowVector = decodeVector(row.vector);
|
|
480
|
+
const score = cosineSimilarity(vector, rowVector);
|
|
481
|
+
return { row, score };
|
|
482
|
+
}).filter((r) => r.score >= scoreThreshold);
|
|
483
|
+
scored.sort((a, b) => b.score - a.score);
|
|
484
|
+
return scored.slice(0, limit).map((r) => ({
|
|
485
|
+
id: r.row.id,
|
|
486
|
+
score: r.score,
|
|
487
|
+
payload: rowToSessionChunkPayload(r.row),
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
490
|
+
export async function upsertSessionChunks(points) {
|
|
491
|
+
const d = getDb();
|
|
492
|
+
const stmt = d.prepare(`
|
|
493
|
+
INSERT OR REPLACE INTO session_chunks
|
|
494
|
+
(id, vector, content, summary, session_id, project, git_branch, cwd,
|
|
495
|
+
chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
|
|
496
|
+
files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at)
|
|
497
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
498
|
+
`);
|
|
499
|
+
// Manual transaction since node:sqlite doesn't have db.transaction()
|
|
500
|
+
d.exec('BEGIN');
|
|
501
|
+
try {
|
|
502
|
+
for (const p of points) {
|
|
503
|
+
stmt.run(p.id, encodeVector(p.vector), p.payload.content, p.payload.summary || null, p.payload.session_id || null, p.payload.project || null, p.payload.git_branch ?? null, p.payload.cwd ?? null, p.payload.chunk_index ?? 0, p.payload.start_timestamp || null, p.payload.end_timestamp || null, JSON.stringify(p.payload.tools_used || []), JSON.stringify(p.payload.files_modified || []), JSON.stringify(p.payload.files_read || []), JSON.stringify(p.payload.user_messages || []), p.payload.turn_count ?? 0, JSON.stringify(p.payload.related_memory_ids || []), p.payload.dedup_memory_id ?? null, p.payload.indexed_at || new Date().toISOString());
|
|
504
|
+
}
|
|
505
|
+
d.exec('COMMIT');
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
d.exec('ROLLBACK');
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
export async function scrollSessionChunks(filter, limit = 20, offset) {
|
|
513
|
+
const d = getDb();
|
|
514
|
+
const { where, params } = translateFilter(filter);
|
|
515
|
+
const numericOffset = offset ? parseInt(offset, 10) : 0;
|
|
516
|
+
const rows = d.prepare(`
|
|
517
|
+
SELECT id, content, summary, session_id, project, git_branch, cwd,
|
|
518
|
+
chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
|
|
519
|
+
files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at
|
|
520
|
+
FROM session_chunks
|
|
521
|
+
WHERE 1=1${where}
|
|
522
|
+
ORDER BY indexed_at DESC
|
|
523
|
+
LIMIT ? OFFSET ?
|
|
524
|
+
`).all(...params, limit, numericOffset);
|
|
525
|
+
const points = rows.map((row) => ({
|
|
526
|
+
id: row.id,
|
|
527
|
+
payload: rowToSessionChunkPayload(row),
|
|
528
|
+
}));
|
|
529
|
+
const nextOffset = rows.length === limit ? String(numericOffset + limit) : null;
|
|
530
|
+
return { points, next_page_offset: nextOffset };
|
|
531
|
+
}
|
|
532
|
+
export async function countSessionChunks(filter) {
|
|
533
|
+
const d = getDb();
|
|
534
|
+
const { where, params } = translateFilter(filter);
|
|
535
|
+
const result = d.prepare(`SELECT COUNT(*) as cnt FROM session_chunks WHERE 1=1${where}`)
|
|
536
|
+
.get(...params);
|
|
537
|
+
return result.cnt;
|
|
538
|
+
}
|
|
539
|
+
export async function getCategories() {
|
|
540
|
+
try {
|
|
541
|
+
const d = getDb();
|
|
542
|
+
const rows = d.prepare('SELECT name, description, created_at, parent, color, is_parent FROM categories ORDER BY name')
|
|
543
|
+
.all();
|
|
544
|
+
if (rows.length === 0)
|
|
545
|
+
return null;
|
|
546
|
+
return rows.map((row) => ({
|
|
547
|
+
name: row.name,
|
|
548
|
+
description: row.description,
|
|
549
|
+
created_at: row.created_at,
|
|
550
|
+
parent: row.parent,
|
|
551
|
+
color: row.color,
|
|
552
|
+
is_parent: row.is_parent === 1 ? true : undefined,
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
export async function saveCategories(categories) {
|
|
560
|
+
const d = getDb();
|
|
561
|
+
const insertStmt = d.prepare(`
|
|
562
|
+
INSERT OR REPLACE INTO categories (name, description, created_at, parent, color, is_parent)
|
|
563
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
564
|
+
`);
|
|
565
|
+
// Manual transaction
|
|
566
|
+
d.exec('BEGIN');
|
|
567
|
+
try {
|
|
568
|
+
// Get existing names to detect deletions
|
|
569
|
+
const existing = new Set(d.prepare('SELECT name FROM categories').all().map((r) => r.name));
|
|
570
|
+
const incoming = new Set(categories.map((c) => c.name));
|
|
571
|
+
// Delete removed categories
|
|
572
|
+
for (const name of existing) {
|
|
573
|
+
if (!incoming.has(name)) {
|
|
574
|
+
d.prepare('DELETE FROM categories WHERE name = ?').run(name);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// Upsert all incoming
|
|
578
|
+
for (const cat of categories) {
|
|
579
|
+
insertStmt.run(cat.name, cat.description, cat.created_at, cat.parent ?? null, cat.color ?? null, cat.is_parent ? 1 : 0);
|
|
580
|
+
}
|
|
581
|
+
d.exec('COMMIT');
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
d.exec('ROLLBACK');
|
|
585
|
+
throw err;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// --- Helpers ---
|
|
589
|
+
function parseJsonOrDefault(value, defaultValue) {
|
|
590
|
+
if (value == null || value === '')
|
|
591
|
+
return defaultValue;
|
|
592
|
+
if (typeof value === 'string') {
|
|
593
|
+
try {
|
|
594
|
+
return JSON.parse(value);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return defaultValue;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
function rowToPayload(row) {
|
|
603
|
+
return {
|
|
604
|
+
content: row.content,
|
|
605
|
+
category: row.category,
|
|
606
|
+
subcategory: row.subcategory,
|
|
607
|
+
project: row.project,
|
|
608
|
+
tags: parseJsonOrDefault(row.tags, []),
|
|
609
|
+
importance: row.importance ?? 5,
|
|
610
|
+
source: row.source || 'self-discovered',
|
|
611
|
+
created_at: row.created_at,
|
|
612
|
+
updated_at: row.updated_at,
|
|
613
|
+
accessed_at: row.accessed_at,
|
|
614
|
+
access_count: row.access_count ?? 0,
|
|
615
|
+
related_files: parseJsonOrDefault(row.related_files, undefined),
|
|
616
|
+
related_memory_ids: parseJsonOrDefault(row.related_memory_ids, undefined),
|
|
617
|
+
file_checksums: parseJsonOrDefault(row.file_checksums, undefined),
|
|
618
|
+
trashed_at: row.trashed_at,
|
|
619
|
+
source_session_chunks: parseJsonOrDefault(row.source_session_chunks, undefined),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function rowToSessionChunkPayload(row) {
|
|
623
|
+
return {
|
|
624
|
+
content: row.content,
|
|
625
|
+
summary: row.summary || '',
|
|
626
|
+
session_id: row.session_id || '',
|
|
627
|
+
project: row.project || '',
|
|
628
|
+
git_branch: row.git_branch,
|
|
629
|
+
cwd: row.cwd,
|
|
630
|
+
chunk_index: row.chunk_index ?? 0,
|
|
631
|
+
start_timestamp: row.start_timestamp || '',
|
|
632
|
+
end_timestamp: row.end_timestamp || '',
|
|
633
|
+
tools_used: parseJsonOrDefault(row.tools_used, []),
|
|
634
|
+
files_modified: parseJsonOrDefault(row.files_modified, []),
|
|
635
|
+
files_read: parseJsonOrDefault(row.files_read, []),
|
|
636
|
+
user_messages: parseJsonOrDefault(row.user_messages, []),
|
|
637
|
+
turn_count: row.turn_count ?? 0,
|
|
638
|
+
related_memory_ids: parseJsonOrDefault(row.related_memory_ids, []),
|
|
639
|
+
dedup_memory_id: row.dedup_memory_id,
|
|
640
|
+
indexed_at: row.indexed_at || '',
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Close the database connection. Call on process exit.
|
|
645
|
+
*/
|
|
646
|
+
export function closeDatabase() {
|
|
647
|
+
if (db) {
|
|
648
|
+
db.close();
|
|
649
|
+
db = null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Reopen the database at the current SQLITE_DB_PATH.
|
|
654
|
+
* Call after updating process.env.SQLITE_DB_PATH to switch databases.
|
|
655
|
+
*/
|
|
656
|
+
export async function reopenDatabase() {
|
|
657
|
+
closeDatabase();
|
|
658
|
+
await ensureDatabase();
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=sqlite.js.map
|