simple-memory-mcp 1.1.3
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/LICENSE +21 -0
- package/README.md +742 -0
- package/dist/cli/arg-parser.d.ts +9 -0
- package/dist/cli/arg-parser.d.ts.map +1 -0
- package/dist/cli/arg-parser.js +68 -0
- package/dist/cli/arg-parser.js.map +1 -0
- package/dist/cli/help-generator.d.ts +8 -0
- package/dist/cli/help-generator.d.ts.map +1 -0
- package/dist/cli/help-generator.js +89 -0
- package/dist/cli/help-generator.js.map +1 -0
- package/dist/cli/index.d.ts +27 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +56 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/query-builder.d.ts +8 -0
- package/dist/cli/query-builder.d.ts.map +1 -0
- package/dist/cli/query-builder.js +63 -0
- package/dist/cli/query-builder.js.map +1 -0
- package/dist/cli/shortcuts-config.d.ts +26 -0
- package/dist/cli/shortcuts-config.d.ts.map +1 -0
- package/dist/cli/shortcuts-config.js +94 -0
- package/dist/cli/shortcuts-config.js.map +1 -0
- package/dist/graphql/resolvers.d.ts +101 -0
- package/dist/graphql/resolvers.d.ts.map +1 -0
- package/dist/graphql/resolvers.js +98 -0
- package/dist/graphql/resolvers.js.map +1 -0
- package/dist/graphql/schema.d.ts +13 -0
- package/dist/graphql/schema.d.ts.map +1 -0
- package/dist/graphql/schema.js +314 -0
- package/dist/graphql/schema.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +573 -0
- package/dist/index.js.map +1 -0
- package/dist/services/backup-service.d.ts +57 -0
- package/dist/services/backup-service.d.ts.map +1 -0
- package/dist/services/backup-service.js +191 -0
- package/dist/services/backup-service.js.map +1 -0
- package/dist/services/database-optimizer.d.ts +15 -0
- package/dist/services/database-optimizer.d.ts.map +1 -0
- package/dist/services/database-optimizer.js +45 -0
- package/dist/services/database-optimizer.js.map +1 -0
- package/dist/services/memory-service.d.ts +126 -0
- package/dist/services/memory-service.d.ts.map +1 -0
- package/dist/services/memory-service.js +862 -0
- package/dist/services/memory-service.js.map +1 -0
- package/dist/services/migrations.d.ts +17 -0
- package/dist/services/migrations.d.ts.map +1 -0
- package/dist/services/migrations.js +273 -0
- package/dist/services/migrations.js.map +1 -0
- package/dist/tests/backup-export-test.d.ts +2 -0
- package/dist/tests/backup-export-test.d.ts.map +1 -0
- package/dist/tests/backup-export-test.js +162 -0
- package/dist/tests/backup-export-test.js.map +1 -0
- package/dist/tests/backup-test.d.ts +14 -0
- package/dist/tests/backup-test.d.ts.map +1 -0
- package/dist/tests/backup-test.js +209 -0
- package/dist/tests/backup-test.js.map +1 -0
- package/dist/tests/export-import-test.d.ts +15 -0
- package/dist/tests/export-import-test.d.ts.map +1 -0
- package/dist/tests/export-import-test.js +227 -0
- package/dist/tests/export-import-test.js.map +1 -0
- package/dist/tests/graphql-comprehensive-test.d.ts +6 -0
- package/dist/tests/graphql-comprehensive-test.d.ts.map +1 -0
- package/dist/tests/graphql-comprehensive-test.js +342 -0
- package/dist/tests/graphql-comprehensive-test.js.map +1 -0
- package/dist/tests/graphql-performance-test.d.ts +6 -0
- package/dist/tests/graphql-performance-test.d.ts.map +1 -0
- package/dist/tests/graphql-performance-test.js +141 -0
- package/dist/tests/graphql-performance-test.js.map +1 -0
- package/dist/tests/graphql-test.d.ts +5 -0
- package/dist/tests/graphql-test.d.ts.map +1 -0
- package/dist/tests/graphql-test.js +47 -0
- package/dist/tests/graphql-test.js.map +1 -0
- package/dist/tests/memory-server-tests.d.ts +7 -0
- package/dist/tests/memory-server-tests.d.ts.map +1 -0
- package/dist/tests/memory-server-tests.js +466 -0
- package/dist/tests/memory-server-tests.js.map +1 -0
- package/dist/tests/migration-test.d.ts +2 -0
- package/dist/tests/migration-test.d.ts.map +1 -0
- package/dist/tests/migration-test.js +270 -0
- package/dist/tests/migration-test.js.map +1 -0
- package/dist/tests/performance-benchmark.d.ts +7 -0
- package/dist/tests/performance-benchmark.d.ts.map +1 -0
- package/dist/tests/performance-benchmark.js +234 -0
- package/dist/tests/performance-benchmark.js.map +1 -0
- package/dist/tests/performance-test.d.ts +3 -0
- package/dist/tests/performance-test.d.ts.map +1 -0
- package/dist/tests/performance-test.js +61 -0
- package/dist/tests/performance-test.js.map +1 -0
- package/dist/tests/time-range-test.d.ts +7 -0
- package/dist/tests/time-range-test.d.ts.map +1 -0
- package/dist/tests/time-range-test.js +169 -0
- package/dist/tests/time-range-test.js.map +1 -0
- package/dist/tools/delete-memory/cli-parser.d.ts +2 -0
- package/dist/tools/delete-memory/cli-parser.d.ts.map +1 -0
- package/dist/tools/delete-memory/cli-parser.js +13 -0
- package/dist/tools/delete-memory/cli-parser.js.map +1 -0
- package/dist/tools/delete-memory/executor.d.ts +13 -0
- package/dist/tools/delete-memory/executor.d.ts.map +1 -0
- package/dist/tools/delete-memory/executor.js +40 -0
- package/dist/tools/delete-memory/executor.js.map +1 -0
- package/dist/tools/delete-memory/index.d.ts +3 -0
- package/dist/tools/delete-memory/index.d.ts.map +1 -0
- package/dist/tools/delete-memory/index.js +24 -0
- package/dist/tools/delete-memory/index.js.map +1 -0
- package/dist/tools/export-memory/cli-parser.d.ts +2 -0
- package/dist/tools/export-memory/cli-parser.d.ts.map +1 -0
- package/dist/tools/export-memory/cli-parser.js +34 -0
- package/dist/tools/export-memory/cli-parser.js.map +1 -0
- package/dist/tools/export-memory/executor.d.ts +10 -0
- package/dist/tools/export-memory/executor.d.ts.map +1 -0
- package/dist/tools/export-memory/executor.js +41 -0
- package/dist/tools/export-memory/executor.js.map +1 -0
- package/dist/tools/export-memory/index.d.ts +4 -0
- package/dist/tools/export-memory/index.d.ts.map +1 -0
- package/dist/tools/export-memory/index.js +99 -0
- package/dist/tools/export-memory/index.js.map +1 -0
- package/dist/tools/import-memory/cli-parser.d.ts +2 -0
- package/dist/tools/import-memory/cli-parser.d.ts.map +1 -0
- package/dist/tools/import-memory/cli-parser.js +25 -0
- package/dist/tools/import-memory/cli-parser.js.map +1 -0
- package/dist/tools/import-memory/executor.d.ts +8 -0
- package/dist/tools/import-memory/executor.d.ts.map +1 -0
- package/dist/tools/import-memory/executor.js +31 -0
- package/dist/tools/import-memory/executor.js.map +1 -0
- package/dist/tools/import-memory/index.d.ts +4 -0
- package/dist/tools/import-memory/index.d.ts.map +1 -0
- package/dist/tools/import-memory/index.js +70 -0
- package/dist/tools/import-memory/index.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +48 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory-graphql/cli-parser.d.ts +6 -0
- package/dist/tools/memory-graphql/cli-parser.d.ts.map +1 -0
- package/dist/tools/memory-graphql/cli-parser.js +24 -0
- package/dist/tools/memory-graphql/cli-parser.js.map +1 -0
- package/dist/tools/memory-graphql/executor.d.ts +18 -0
- package/dist/tools/memory-graphql/executor.d.ts.map +1 -0
- package/dist/tools/memory-graphql/executor.js +53 -0
- package/dist/tools/memory-graphql/executor.js.map +1 -0
- package/dist/tools/memory-graphql/index.d.ts +3 -0
- package/dist/tools/memory-graphql/index.d.ts.map +1 -0
- package/dist/tools/memory-graphql/index.js +73 -0
- package/dist/tools/memory-graphql/index.js.map +1 -0
- package/dist/tools/memory-stats/cli-parser.d.ts +2 -0
- package/dist/tools/memory-stats/cli-parser.d.ts.map +1 -0
- package/dist/tools/memory-stats/cli-parser.js +8 -0
- package/dist/tools/memory-stats/cli-parser.js.map +1 -0
- package/dist/tools/memory-stats/executor.d.ts +4 -0
- package/dist/tools/memory-stats/executor.d.ts.map +1 -0
- package/dist/tools/memory-stats/executor.js +4 -0
- package/dist/tools/memory-stats/executor.js.map +1 -0
- package/dist/tools/memory-stats/index.d.ts +3 -0
- package/dist/tools/memory-stats/index.d.ts.map +1 -0
- package/dist/tools/memory-stats/index.js +15 -0
- package/dist/tools/memory-stats/index.js.map +1 -0
- package/dist/tools/search-memory/cli-parser.d.ts +2 -0
- package/dist/tools/search-memory/cli-parser.d.ts.map +1 -0
- package/dist/tools/search-memory/cli-parser.js +56 -0
- package/dist/tools/search-memory/cli-parser.js.map +1 -0
- package/dist/tools/search-memory/executor.d.ts +36 -0
- package/dist/tools/search-memory/executor.d.ts.map +1 -0
- package/dist/tools/search-memory/executor.js +83 -0
- package/dist/tools/search-memory/executor.js.map +1 -0
- package/dist/tools/search-memory/index.d.ts +3 -0
- package/dist/tools/search-memory/index.d.ts.map +1 -0
- package/dist/tools/search-memory/index.js +89 -0
- package/dist/tools/search-memory/index.js.map +1 -0
- package/dist/tools/store-memory/cli-parser.d.ts +2 -0
- package/dist/tools/store-memory/cli-parser.d.ts.map +1 -0
- package/dist/tools/store-memory/cli-parser.js +21 -0
- package/dist/tools/store-memory/cli-parser.js.map +1 -0
- package/dist/tools/store-memory/executor.d.ts +16 -0
- package/dist/tools/store-memory/executor.d.ts.map +1 -0
- package/dist/tools/store-memory/executor.js +100 -0
- package/dist/tools/store-memory/executor.js.map +1 -0
- package/dist/tools/store-memory/index.d.ts +3 -0
- package/dist/tools/store-memory/index.d.ts.map +1 -0
- package/dist/tools/store-memory/index.js +68 -0
- package/dist/tools/store-memory/index.js.map +1 -0
- package/dist/tools/sync-memory/cli-parser.d.ts +1 -0
- package/dist/tools/sync-memory/cli-parser.d.ts.map +1 -0
- package/dist/tools/sync-memory/cli-parser.js +2 -0
- package/dist/tools/sync-memory/cli-parser.js.map +1 -0
- package/dist/tools/sync-memory/executor.d.ts +1 -0
- package/dist/tools/sync-memory/executor.d.ts.map +1 -0
- package/dist/tools/sync-memory/executor.js +2 -0
- package/dist/tools/sync-memory/executor.js.map +1 -0
- package/dist/tools/sync-memory/index.d.ts +1 -0
- package/dist/tools/sync-memory/index.d.ts.map +1 -0
- package/dist/tools/sync-memory/index.js +2 -0
- package/dist/tools/sync-memory/index.js.map +1 -0
- package/dist/tools/update-memory/cli-parser.d.ts +2 -0
- package/dist/tools/update-memory/cli-parser.d.ts.map +1 -0
- package/dist/tools/update-memory/cli-parser.js +17 -0
- package/dist/tools/update-memory/cli-parser.js.map +1 -0
- package/dist/tools/update-memory/executor.d.ts +16 -0
- package/dist/tools/update-memory/executor.d.ts.map +1 -0
- package/dist/tools/update-memory/executor.js +59 -0
- package/dist/tools/update-memory/executor.js.map +1 -0
- package/dist/tools/update-memory/index.d.ts +3 -0
- package/dist/tools/update-memory/index.d.ts.map +1 -0
- package/dist/tools/update-memory/index.js +30 -0
- package/dist/tools/update-memory/index.js.map +1 -0
- package/dist/transports/streamable-http.d.ts +38 -0
- package/dist/transports/streamable-http.d.ts.map +1 -0
- package/dist/transports/streamable-http.js +209 -0
- package/dist/transports/streamable-http.js.map +1 -0
- package/dist/types/tools.d.ts +79 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +3 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/utils/cli-parser.d.ts +12 -0
- package/dist/utils/cli-parser.d.ts.map +1 -0
- package/dist/utils/cli-parser.js +43 -0
- package/dist/utils/cli-parser.js.map +1 -0
- package/dist/utils/config.d.ts +95 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +176 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/db-integrity-check.d.ts +22 -0
- package/dist/utils/db-integrity-check.d.ts.map +1 -0
- package/dist/utils/db-integrity-check.js +69 -0
- package/dist/utils/db-integrity-check.js.map +1 -0
- package/dist/utils/debug.d.ts +25 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +67 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/help-generator.d.ts +18 -0
- package/dist/utils/help-generator.d.ts.map +1 -0
- package/dist/utils/help-generator.js +81 -0
- package/dist/utils/help-generator.js.map +1 -0
- package/dist/utils/json-parser.d.ts +12 -0
- package/dist/utils/json-parser.d.ts.map +1 -0
- package/dist/utils/json-parser.js +52 -0
- package/dist/utils/json-parser.js.map +1 -0
- package/dist/utils/mcp-config.d.ts +12 -0
- package/dist/utils/mcp-config.d.ts.map +1 -0
- package/dist/utils/mcp-config.js +64 -0
- package/dist/utils/mcp-config.js.map +1 -0
- package/dist/web-server.d.ts +3 -0
- package/dist/web-server.d.ts.map +1 -0
- package/dist/web-server.js +265 -0
- package/dist/web-server.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { hostname } from 'os';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import { debugLog, debugLogHash } from '../utils/debug.js';
|
|
9
|
+
import { runMigrations } from './migrations.js';
|
|
10
|
+
import { DatabaseOptimizer } from './database-optimizer.js';
|
|
11
|
+
import { BackupService } from './backup-service.js';
|
|
12
|
+
import { getMCPConfigPaths } from '../utils/mcp-config.js';
|
|
13
|
+
import { getBackupConfig, getConfigPath } from '../utils/config.js';
|
|
14
|
+
// Get package version for export metadata
|
|
15
|
+
function getPackageVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
const packagePath = join(__dirname, '..', '..', 'package.json');
|
|
20
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
21
|
+
return packageJson.version || '1.0.0';
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return '1.0.0'; // Fallback version
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Memory Service for persistent storage and retrieval of memories with tagging and relationships.
|
|
29
|
+
* Based on SQLite with FTS (Full Text Search) for efficient querying.
|
|
30
|
+
*/
|
|
31
|
+
export class MemoryService {
|
|
32
|
+
db = null;
|
|
33
|
+
dbPath;
|
|
34
|
+
resolvedDbPath;
|
|
35
|
+
stmts;
|
|
36
|
+
maxContentSize = 1024 * 1024; // 1MB default
|
|
37
|
+
backup;
|
|
38
|
+
constructor(dbPath = 'memory.db', maxContentSize) {
|
|
39
|
+
this.dbPath = dbPath;
|
|
40
|
+
if (maxContentSize)
|
|
41
|
+
this.maxContentSize = maxContentSize;
|
|
42
|
+
// Cache resolved path once
|
|
43
|
+
this.resolvedDbPath = resolve(dbPath);
|
|
44
|
+
}
|
|
45
|
+
initialize() {
|
|
46
|
+
try {
|
|
47
|
+
this.db = new Database(this.dbPath);
|
|
48
|
+
this.initDb();
|
|
49
|
+
// Configure backup service after db is ready
|
|
50
|
+
const backupConfig = getBackupConfig();
|
|
51
|
+
if (backupConfig.path) {
|
|
52
|
+
this.backup = new BackupService({
|
|
53
|
+
backupPath: backupConfig.path,
|
|
54
|
+
autoBackupInterval: backupConfig.interval,
|
|
55
|
+
maxBackups: backupConfig.keep,
|
|
56
|
+
source: backupConfig.source,
|
|
57
|
+
getBackupData: () => this.exportMemories()
|
|
58
|
+
});
|
|
59
|
+
this.backup.initialize();
|
|
60
|
+
}
|
|
61
|
+
debugLog('MemoryService initialized with database:', this.dbPath);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw new Error(`Failed to initialize database: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
initDb() {
|
|
68
|
+
if (!this.db) {
|
|
69
|
+
throw new Error('Database not initialized');
|
|
70
|
+
}
|
|
71
|
+
// Apply SQLite optimizations first
|
|
72
|
+
DatabaseOptimizer.applyOptimizations(this.db);
|
|
73
|
+
// Create base tables (if they don't exist)
|
|
74
|
+
this.db.exec(`
|
|
75
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
content TEXT NOT NULL,
|
|
78
|
+
created_at TEXT,
|
|
79
|
+
hash TEXT UNIQUE
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
// Create relationships table for linking memories
|
|
83
|
+
this.db.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
85
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
86
|
+
from_memory_id INTEGER,
|
|
87
|
+
to_memory_id INTEGER,
|
|
88
|
+
relationship_type TEXT DEFAULT 'related',
|
|
89
|
+
created_at TEXT,
|
|
90
|
+
FOREIGN KEY (from_memory_id) REFERENCES memories (id) ON DELETE CASCADE,
|
|
91
|
+
FOREIGN KEY (to_memory_id) REFERENCES memories (id) ON DELETE CASCADE,
|
|
92
|
+
UNIQUE(from_memory_id, to_memory_id, relationship_type)
|
|
93
|
+
)
|
|
94
|
+
`);
|
|
95
|
+
// Create normalized tags table for efficient tag queries
|
|
96
|
+
this.db.exec(`
|
|
97
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
98
|
+
memory_id INTEGER NOT NULL,
|
|
99
|
+
tag TEXT NOT NULL,
|
|
100
|
+
FOREIGN KEY (memory_id) REFERENCES memories (id) ON DELETE CASCADE,
|
|
101
|
+
PRIMARY KEY (memory_id, tag)
|
|
102
|
+
)
|
|
103
|
+
`);
|
|
104
|
+
// Create indexes for performance
|
|
105
|
+
this.db.exec(`
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_tags_memory_id ON tags(memory_id);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at DESC);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_from ON relationships(from_memory_id);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_to ON relationships(to_memory_id);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_composite ON relationships(from_memory_id, to_memory_id);
|
|
113
|
+
`);
|
|
114
|
+
// Create FTS table for fast text search (content only, tags in separate table)
|
|
115
|
+
this.db.exec(`
|
|
116
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
|
|
117
|
+
USING fts5(content, content='memories', content_rowid='id')
|
|
118
|
+
`);
|
|
119
|
+
// Create trigger to automatically update FTS when memories are inserted
|
|
120
|
+
this.db.exec(`
|
|
121
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
122
|
+
INSERT INTO memories_fts (rowid, content)
|
|
123
|
+
VALUES (new.id, new.content);
|
|
124
|
+
END;
|
|
125
|
+
`);
|
|
126
|
+
// Create trigger to automatically update FTS when memories are updated
|
|
127
|
+
// Note: External content FTS5 tables don't support UPDATE, must DELETE+INSERT
|
|
128
|
+
this.db.exec(`
|
|
129
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
130
|
+
DELETE FROM memories_fts WHERE rowid = old.id;
|
|
131
|
+
INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content);
|
|
132
|
+
END;
|
|
133
|
+
`);
|
|
134
|
+
// Create trigger to automatically delete from FTS when memories are deleted
|
|
135
|
+
this.db.exec(`
|
|
136
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
137
|
+
DELETE FROM memories_fts WHERE rowid = old.id;
|
|
138
|
+
END;
|
|
139
|
+
`);
|
|
140
|
+
// Run migrations (creates tags table, indexes, etc.)
|
|
141
|
+
// This is where all the magic happens - automatic, tracked, safe
|
|
142
|
+
runMigrations(this.db, this.dbPath);
|
|
143
|
+
// Optimize FTS after migrations
|
|
144
|
+
DatabaseOptimizer.optimizeFTS(this.db);
|
|
145
|
+
// Prepare statements for better performance
|
|
146
|
+
this.prepareStatements();
|
|
147
|
+
debugLog('MemoryService: Database initialized successfully');
|
|
148
|
+
}
|
|
149
|
+
prepareStatements() {
|
|
150
|
+
this.stmts = {
|
|
151
|
+
// Memory operations
|
|
152
|
+
insert: this.db.prepare(`
|
|
153
|
+
INSERT INTO memories (content, created_at, hash)
|
|
154
|
+
VALUES (?, ?, ?)
|
|
155
|
+
`),
|
|
156
|
+
getMemoryById: this.db.prepare(`
|
|
157
|
+
SELECT * FROM memories WHERE id = ?
|
|
158
|
+
`),
|
|
159
|
+
getMemoryByHash: this.db.prepare(`
|
|
160
|
+
SELECT * FROM memories WHERE hash = ?
|
|
161
|
+
`),
|
|
162
|
+
getRecent: this.db.prepare(`
|
|
163
|
+
SELECT * FROM memories
|
|
164
|
+
ORDER BY created_at DESC
|
|
165
|
+
LIMIT ?
|
|
166
|
+
`),
|
|
167
|
+
deleteByHash: this.db.prepare(`
|
|
168
|
+
DELETE FROM memories WHERE hash = ?
|
|
169
|
+
`),
|
|
170
|
+
updateMemory: this.db.prepare(`
|
|
171
|
+
UPDATE memories
|
|
172
|
+
SET content = ?, hash = ?
|
|
173
|
+
WHERE id = ?
|
|
174
|
+
`),
|
|
175
|
+
// Tag operations (NEW)
|
|
176
|
+
insertTag: this.db.prepare(`
|
|
177
|
+
INSERT OR IGNORE INTO tags (memory_id, tag) VALUES (?, ?)
|
|
178
|
+
`),
|
|
179
|
+
getTagsForMemory: this.db.prepare(`
|
|
180
|
+
SELECT tag FROM tags WHERE memory_id = ? ORDER BY tag
|
|
181
|
+
`),
|
|
182
|
+
deleteTagsForMemory: this.db.prepare(`
|
|
183
|
+
DELETE FROM tags WHERE memory_id = ?
|
|
184
|
+
`),
|
|
185
|
+
searchByTag: this.db.prepare(`
|
|
186
|
+
SELECT DISTINCT m.*
|
|
187
|
+
FROM memories m
|
|
188
|
+
INNER JOIN tags t ON m.id = t.memory_id
|
|
189
|
+
WHERE t.tag = ?
|
|
190
|
+
ORDER BY m.created_at DESC
|
|
191
|
+
LIMIT ?
|
|
192
|
+
`),
|
|
193
|
+
deleteByTag: this.db.prepare(`
|
|
194
|
+
DELETE FROM memories
|
|
195
|
+
WHERE id IN (SELECT memory_id FROM tags WHERE tag = ?)
|
|
196
|
+
`),
|
|
197
|
+
// FTS search with BM25 ranking for relevance scoring
|
|
198
|
+
searchText: this.db.prepare(`
|
|
199
|
+
SELECT m.*, bm25(memories_fts) as rank
|
|
200
|
+
FROM memories m
|
|
201
|
+
JOIN memories_fts fts ON m.id = fts.rowid
|
|
202
|
+
WHERE memories_fts MATCH ?
|
|
203
|
+
ORDER BY rank, m.created_at DESC
|
|
204
|
+
LIMIT ?
|
|
205
|
+
`),
|
|
206
|
+
// Legacy tag search (no longer used after migration 2)
|
|
207
|
+
searchTagsLegacy: this.db.prepare(`
|
|
208
|
+
SELECT * FROM memories
|
|
209
|
+
WHERE 1=0
|
|
210
|
+
ORDER BY created_at DESC
|
|
211
|
+
LIMIT ?
|
|
212
|
+
`),
|
|
213
|
+
// Relationship operations
|
|
214
|
+
insertRelationship: this.db.prepare(`
|
|
215
|
+
INSERT INTO relationships (from_memory_id, to_memory_id, relationship_type, created_at)
|
|
216
|
+
VALUES (?, ?, ?, ?)
|
|
217
|
+
`),
|
|
218
|
+
getRelated: this.db.prepare(`
|
|
219
|
+
SELECT m.*, r.relationship_type
|
|
220
|
+
FROM memories m
|
|
221
|
+
JOIN relationships r ON (m.id = r.to_memory_id OR m.id = r.from_memory_id)
|
|
222
|
+
WHERE (r.from_memory_id = ? OR r.to_memory_id = ?) AND m.id != ?
|
|
223
|
+
ORDER BY r.created_at DESC
|
|
224
|
+
LIMIT ?
|
|
225
|
+
`),
|
|
226
|
+
// Stats
|
|
227
|
+
getStats: this.db.prepare(`
|
|
228
|
+
SELECT COUNT(*) as count FROM memories
|
|
229
|
+
`),
|
|
230
|
+
getRelationshipStats: this.db.prepare(`
|
|
231
|
+
SELECT COUNT(*) as count FROM relationships
|
|
232
|
+
`)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Store a memory with optional tags
|
|
237
|
+
*/
|
|
238
|
+
store(content, tags = []) {
|
|
239
|
+
// Validate content size
|
|
240
|
+
if (content.length > this.maxContentSize) {
|
|
241
|
+
throw new Error(`Content exceeds maximum size of ${this.maxContentSize} characters`);
|
|
242
|
+
}
|
|
243
|
+
const hash = createHash('md5').update(content).digest('hex');
|
|
244
|
+
const createdAt = new Date().toISOString();
|
|
245
|
+
try {
|
|
246
|
+
if (!this.db) {
|
|
247
|
+
throw new Error('Database not initialized');
|
|
248
|
+
}
|
|
249
|
+
// Use transaction for atomicity and performance
|
|
250
|
+
const insertMemory = this.db.transaction(() => {
|
|
251
|
+
// Insert memory (tags stored in normalized 'tags' table since v2.0)
|
|
252
|
+
const result = this.stmts.insert.run(content, createdAt, hash);
|
|
253
|
+
const memoryId = result.lastInsertRowid;
|
|
254
|
+
// Insert tags into normalized tags table
|
|
255
|
+
for (const tag of tags) {
|
|
256
|
+
const normalizedTag = tag.trim().toLowerCase();
|
|
257
|
+
if (normalizedTag) {
|
|
258
|
+
this.stmts.insertTag.run(memoryId, normalizedTag);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return hash;
|
|
262
|
+
});
|
|
263
|
+
const resultHash = insertMemory();
|
|
264
|
+
debugLogHash('MemoryService: Stored memory with hash:', hash);
|
|
265
|
+
// Backup if needed (lazy, throttled)
|
|
266
|
+
this.backup?.backupIfNeeded();
|
|
267
|
+
return resultHash;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
|
271
|
+
debugLogHash('MemoryService: Memory already exists with hash:', hash);
|
|
272
|
+
return hash; // Already exists
|
|
273
|
+
}
|
|
274
|
+
debugLog('MemoryService: Error storing memory:', error);
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Search memories by content or tags
|
|
280
|
+
*/
|
|
281
|
+
search(query, tags, limit = 10, daysAgo, startDate, endDate, minRelevance) {
|
|
282
|
+
let results;
|
|
283
|
+
// Calculate date boundaries for filtering
|
|
284
|
+
let minDate;
|
|
285
|
+
let maxDate;
|
|
286
|
+
if (daysAgo !== undefined && daysAgo >= 0) {
|
|
287
|
+
// Convert daysAgo to a start date (N days ago from now)
|
|
288
|
+
// Use UTC to match how SQLite stores timestamps
|
|
289
|
+
const now = new Date();
|
|
290
|
+
minDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - daysAgo, 0, 0, 0, 0 // Start of day in UTC
|
|
291
|
+
));
|
|
292
|
+
}
|
|
293
|
+
if (startDate) {
|
|
294
|
+
// Parse start date (supports both YYYY-MM-DD and full ISO strings)
|
|
295
|
+
const parsed = new Date(startDate);
|
|
296
|
+
if (!isNaN(parsed.getTime())) {
|
|
297
|
+
minDate = parsed;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (endDate) {
|
|
301
|
+
// Parse end date (supports both YYYY-MM-DD and full ISO strings)
|
|
302
|
+
const parsed = new Date(endDate);
|
|
303
|
+
if (!isNaN(parsed.getTime())) {
|
|
304
|
+
maxDate = parsed;
|
|
305
|
+
// Set to end of day if only date provided (no time component)
|
|
306
|
+
if (endDate.length === 10) { // YYYY-MM-DD format
|
|
307
|
+
maxDate.setHours(23, 59, 59, 999);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (query) {
|
|
312
|
+
// Use FTS for text search
|
|
313
|
+
let ftsResults;
|
|
314
|
+
// Tokenize query into words and join with OR for flexible matching
|
|
315
|
+
// This allows: "git reset" to match memories with either "git" OR "reset"
|
|
316
|
+
const words = query
|
|
317
|
+
.split(/\s+/)
|
|
318
|
+
.map(word => word.trim())
|
|
319
|
+
.filter(word => word.length > 0)
|
|
320
|
+
.map(word => `"${word.replace(/"/g, '""')}"`); // Quote each word for exact word matching
|
|
321
|
+
const escapedQuery = words.length > 0 ? words.join(' OR ') : query.replace(/"/g, '""');
|
|
322
|
+
// Fetch more results initially if we need to filter by tags or relevance
|
|
323
|
+
const fetchLimit = (tags && tags.length > 0) || minRelevance !== undefined ? limit * 5 : limit * 2;
|
|
324
|
+
ftsResults = this.stmts.searchText.all(escapedQuery, fetchLimit);
|
|
325
|
+
// Filter by relevance score if threshold provided
|
|
326
|
+
if (minRelevance !== undefined && ftsResults.length > 0) {
|
|
327
|
+
// BM25 returns negative scores (more negative = better match)
|
|
328
|
+
// Normalize to 0-1 range where 1 is best match, 0 is worst
|
|
329
|
+
const scores = ftsResults.map(r => Math.abs(r.rank));
|
|
330
|
+
const maxScore = Math.max(...scores);
|
|
331
|
+
const minScore = Math.min(...scores);
|
|
332
|
+
const range = maxScore - minScore || 1;
|
|
333
|
+
ftsResults = ftsResults.filter((row) => {
|
|
334
|
+
const normalizedScore = 1 - ((Math.abs(row.rank) - minScore) / range);
|
|
335
|
+
row.relevance = normalizedScore; // Store for debugging/display
|
|
336
|
+
return normalizedScore >= minRelevance;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Hydrate with tags from tags table
|
|
340
|
+
results = ftsResults.map((row) => {
|
|
341
|
+
const tagRows = this.stmts.getTagsForMemory.all(row.id);
|
|
342
|
+
return {
|
|
343
|
+
...row,
|
|
344
|
+
tags: tagRows.map(t => t.tag)
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else if (tags && tags.length > 0) {
|
|
349
|
+
// Fast indexed tag search using normalized tags table
|
|
350
|
+
const normalizedTag = tags[0].trim().toLowerCase();
|
|
351
|
+
const tagResults = this.stmts.searchByTag.all(normalizedTag, limit * 2); // Fetch more to allow for filtering
|
|
352
|
+
// Hydrate with all tags for each memory
|
|
353
|
+
results = tagResults.map((row) => {
|
|
354
|
+
const tagRows = this.stmts.getTagsForMemory.all(row.id);
|
|
355
|
+
return {
|
|
356
|
+
...row,
|
|
357
|
+
tags: tagRows.map(t => t.tag)
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// Get recent memories
|
|
363
|
+
const recentResults = this.stmts.getRecent.all(limit * 2); // Fetch more to allow for filtering
|
|
364
|
+
// Hydrate with tags
|
|
365
|
+
results = recentResults.map((row) => {
|
|
366
|
+
const tagRows = this.stmts.getTagsForMemory.all(row.id);
|
|
367
|
+
return {
|
|
368
|
+
...row,
|
|
369
|
+
tags: tagRows.map(t => t.tag)
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
// Apply date filtering if needed
|
|
374
|
+
if (minDate || maxDate) {
|
|
375
|
+
results = results.filter((row) => {
|
|
376
|
+
const createdAt = new Date(row.created_at);
|
|
377
|
+
if (minDate && createdAt < minDate)
|
|
378
|
+
return false;
|
|
379
|
+
if (maxDate && createdAt > maxDate)
|
|
380
|
+
return false;
|
|
381
|
+
return true;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
// Apply limit after filtering
|
|
385
|
+
results = results.slice(0, limit);
|
|
386
|
+
// Convert to MemoryEntry format
|
|
387
|
+
const memories = results.map(row => ({
|
|
388
|
+
id: row.id,
|
|
389
|
+
content: row.content,
|
|
390
|
+
tags: row.tags || [],
|
|
391
|
+
createdAt: row.created_at,
|
|
392
|
+
hash: row.hash,
|
|
393
|
+
...(row.relevance !== undefined && { relevance: row.relevance })
|
|
394
|
+
}));
|
|
395
|
+
debugLog('MemoryService: Search returned', memories.length, 'results');
|
|
396
|
+
return memories;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Delete a memory by hash
|
|
400
|
+
*/
|
|
401
|
+
delete(hash) {
|
|
402
|
+
const result = this.stmts.deleteByHash.run(hash);
|
|
403
|
+
let deleted = result.changes > 0;
|
|
404
|
+
// Fallback: If hash lookup failed, force full table scan (bypasses corrupted index)
|
|
405
|
+
// The + prefix tells SQLite to not use the index on hash column
|
|
406
|
+
if (!deleted && this.db) {
|
|
407
|
+
debugLogHash('MemoryService: Hash lookup failed, trying fallback full table scan for:', hash);
|
|
408
|
+
const orphaned = this.db.prepare('SELECT id FROM memories WHERE +hash = ?').get(hash);
|
|
409
|
+
if (orphaned) {
|
|
410
|
+
// DIAGNOSTIC: This indicates hash index corruption - log details for investigation
|
|
411
|
+
console.error('⚠️ HASH INDEX CORRUPTION DETECTED ⚠️');
|
|
412
|
+
console.error('Hash:', hash);
|
|
413
|
+
console.error('Memory ID:', orphaned.id);
|
|
414
|
+
console.error('This suggests index corruption occurred during a previous operation.');
|
|
415
|
+
console.error('Please report this with the hash and operation that preceded it.');
|
|
416
|
+
debugLog('MemoryService: Found orphaned memory with corrupted hash index, deleting by ID:', orphaned.id);
|
|
417
|
+
const fallbackResult = this.db.prepare('DELETE FROM memories WHERE id = ?').run(orphaned.id);
|
|
418
|
+
deleted = fallbackResult.changes > 0;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
debugLogHash('MemoryService: Delete by hash', hash, deleted ? 'success' : 'not found');
|
|
422
|
+
// Backup if needed (lazy, throttled)
|
|
423
|
+
if (deleted)
|
|
424
|
+
this.backup?.backupIfNeeded();
|
|
425
|
+
return deleted;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Delete memories by tag
|
|
429
|
+
*/
|
|
430
|
+
deleteByTag(tag) {
|
|
431
|
+
if (!this.db) {
|
|
432
|
+
throw new Error('Database not initialized');
|
|
433
|
+
}
|
|
434
|
+
const normalizedTag = tag.trim().toLowerCase();
|
|
435
|
+
const result = this.stmts.deleteByTag.run(normalizedTag);
|
|
436
|
+
debugLog('MemoryService: Deleted', result.changes, 'memories with tag:', normalizedTag);
|
|
437
|
+
// Backup if needed (lazy, throttled)
|
|
438
|
+
if (result.changes > 0)
|
|
439
|
+
this.backup?.backupIfNeeded();
|
|
440
|
+
return result.changes;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Bulk link memories in a single transaction for performance
|
|
444
|
+
* Returns the number of relationships successfully created
|
|
445
|
+
*/
|
|
446
|
+
linkMemoriesBulk(relationships) {
|
|
447
|
+
if (!this.db) {
|
|
448
|
+
throw new Error('Database not initialized');
|
|
449
|
+
}
|
|
450
|
+
if (relationships.length === 0) {
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
const insertBulk = this.db.transaction(() => {
|
|
454
|
+
let count = 0;
|
|
455
|
+
const createdAt = new Date().toISOString();
|
|
456
|
+
for (const rel of relationships) {
|
|
457
|
+
const fromMemory = this.stmts.getMemoryByHash.get(rel.fromHash);
|
|
458
|
+
const toMemory = this.stmts.getMemoryByHash.get(rel.toHash);
|
|
459
|
+
if (!fromMemory || !toMemory)
|
|
460
|
+
continue;
|
|
461
|
+
try {
|
|
462
|
+
this.stmts.insertRelationship.run(fromMemory.id, toMemory.id, rel.relationshipType || 'related', createdAt);
|
|
463
|
+
count++;
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
|
467
|
+
// Skip duplicates silently
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return count;
|
|
474
|
+
});
|
|
475
|
+
const created = insertBulk();
|
|
476
|
+
debugLog('MemoryService: Bulk linked', created, 'relationships');
|
|
477
|
+
return created;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Link two memories with a relationship
|
|
481
|
+
*/
|
|
482
|
+
linkMemories(fromHash, toHash, relationshipType = 'related') {
|
|
483
|
+
const fromMemory = this.stmts.getMemoryByHash.get(fromHash);
|
|
484
|
+
const toMemory = this.stmts.getMemoryByHash.get(toHash);
|
|
485
|
+
if (!fromMemory || !toMemory) {
|
|
486
|
+
throw new Error('One or both memories not found');
|
|
487
|
+
}
|
|
488
|
+
const createdAt = new Date().toISOString();
|
|
489
|
+
try {
|
|
490
|
+
this.stmts.insertRelationship.run(fromMemory.id, toMemory.id, relationshipType, createdAt);
|
|
491
|
+
debugLogHash('MemoryService: Linked memories:', fromHash, 'to', toHash);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
|
496
|
+
debugLog('MemoryService: Relationship already exists');
|
|
497
|
+
return false; // Relationship already exists
|
|
498
|
+
}
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get memories related to a specific memory
|
|
504
|
+
*/
|
|
505
|
+
getRelated(hash, limit = 10) {
|
|
506
|
+
const memory = this.stmts.getMemoryByHash.get(hash);
|
|
507
|
+
if (!memory) {
|
|
508
|
+
debugLogHash('MemoryService: Memory not found for getRelated:', hash);
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
const results = this.stmts.getRelated.all(memory.id, memory.id, memory.id, limit);
|
|
512
|
+
const related = results.map((row) => {
|
|
513
|
+
// Hydrate tags from tags table
|
|
514
|
+
const tagRows = this.stmts.getTagsForMemory.all(row.id);
|
|
515
|
+
return {
|
|
516
|
+
id: row.id,
|
|
517
|
+
content: row.content,
|
|
518
|
+
tags: tagRows.map(t => t.tag),
|
|
519
|
+
createdAt: row.created_at,
|
|
520
|
+
hash: row.hash,
|
|
521
|
+
relationshipType: row.relationship_type
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
debugLog('MemoryService: Found', related.length, 'related memories');
|
|
525
|
+
return related;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Get statistics about the memory database
|
|
529
|
+
*/
|
|
530
|
+
stats() {
|
|
531
|
+
if (!this.db) {
|
|
532
|
+
throw new Error('Database not initialized');
|
|
533
|
+
}
|
|
534
|
+
const memoryCount = this.stmts.getStats.get();
|
|
535
|
+
const relationshipCount = this.stmts.getRelationshipStats.get();
|
|
536
|
+
// Get current schema version from migrations table
|
|
537
|
+
const versionResult = this.db.prepare('SELECT MAX(version) as version FROM schema_migrations').get();
|
|
538
|
+
const schemaVersion = versionResult?.version || 0;
|
|
539
|
+
// Get backup config from unified config system
|
|
540
|
+
const backupConfig = getBackupConfig();
|
|
541
|
+
const stats = {
|
|
542
|
+
version: getPackageVersion(),
|
|
543
|
+
totalMemories: memoryCount.count,
|
|
544
|
+
totalRelationships: relationshipCount.count,
|
|
545
|
+
dbSize: this.db.pragma('page_size', { simple: true }) *
|
|
546
|
+
this.db.pragma('page_count', { simple: true }),
|
|
547
|
+
dbPath: this.dbPath,
|
|
548
|
+
resolvedPath: this.resolvedDbPath.replace(/\\/g, '/'), // Normalize to forward slashes
|
|
549
|
+
schemaVersion,
|
|
550
|
+
configPath: getConfigPath().replace(/\\/g, '/'), // Path to config.json
|
|
551
|
+
};
|
|
552
|
+
// Add backup information if backup service is configured
|
|
553
|
+
if (this.backup) {
|
|
554
|
+
const lastBackupAge = this.backup.getTimeSinceLastBackup();
|
|
555
|
+
const backupInterval = backupConfig.interval;
|
|
556
|
+
stats.backupEnabled = true;
|
|
557
|
+
stats.backupPath = backupConfig.path;
|
|
558
|
+
stats.backupCount = this.backup.getBackupCount();
|
|
559
|
+
stats.lastBackupAge = lastBackupAge >= 0 ? lastBackupAge : undefined;
|
|
560
|
+
// Calculate next backup time
|
|
561
|
+
if (backupInterval > 0 && lastBackupAge >= 0) {
|
|
562
|
+
const nextBackup = backupInterval - lastBackupAge;
|
|
563
|
+
stats.nextBackupIn = nextBackup > 0 ? nextBackup : -1; // -1 means will backup on next write
|
|
564
|
+
}
|
|
565
|
+
else if (backupInterval === 0) {
|
|
566
|
+
stats.nextBackupIn = -1; // Will backup on every write
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Add MCP configuration file paths (only existing ones)
|
|
570
|
+
stats.mcpConfigPaths = getMCPConfigPaths().filter(p => p.exists);
|
|
571
|
+
debugLog('MemoryService: Stats:', stats);
|
|
572
|
+
return stats;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Update an existing memory by hash
|
|
576
|
+
* @param hash The hash of the memory to update
|
|
577
|
+
* @param newContent The new content for the memory
|
|
578
|
+
* @param newTags Optional new tags (if provided, replaces all existing tags)
|
|
579
|
+
* @returns The new hash if successful, null if memory not found
|
|
580
|
+
*/
|
|
581
|
+
update(hash, newContent, newTags) {
|
|
582
|
+
// Validate content size
|
|
583
|
+
if (newContent.length > this.maxContentSize) {
|
|
584
|
+
throw new Error(`Content exceeds maximum size of ${this.maxContentSize} characters`);
|
|
585
|
+
}
|
|
586
|
+
if (!this.db) {
|
|
587
|
+
throw new Error('Database not initialized');
|
|
588
|
+
}
|
|
589
|
+
// Find the existing memory
|
|
590
|
+
let existing = this.stmts.getMemoryByHash.get(hash);
|
|
591
|
+
// Fallback: If hash lookup failed, force full table scan (bypasses corrupted index)
|
|
592
|
+
// The + prefix tells SQLite to not use the index on hash column
|
|
593
|
+
if (!existing) {
|
|
594
|
+
debugLogHash('MemoryService: Hash lookup failed, trying fallback full table scan for:', hash);
|
|
595
|
+
existing = this.db.prepare('SELECT * FROM memories WHERE +hash = ?').get(hash);
|
|
596
|
+
if (existing) {
|
|
597
|
+
// DIAGNOSTIC: This indicates hash index corruption - log details for investigation
|
|
598
|
+
console.error('⚠️ HASH INDEX CORRUPTION DETECTED ⚠️');
|
|
599
|
+
console.error('Hash:', hash);
|
|
600
|
+
console.error('Memory ID:', existing.id);
|
|
601
|
+
console.error('Operation: UPDATE');
|
|
602
|
+
console.error('This suggests index corruption occurred during a previous operation.');
|
|
603
|
+
console.error('Please report this with the hash and operation that preceded it.');
|
|
604
|
+
debugLog('MemoryService: Found orphaned memory with corrupted hash index, ID:', existing.id);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (!existing) {
|
|
608
|
+
debugLogHash('MemoryService: Memory not found for update:', hash);
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
// Calculate new hash
|
|
612
|
+
const newHash = createHash('md5').update(newContent).digest('hex');
|
|
613
|
+
try {
|
|
614
|
+
// Use transaction for atomicity
|
|
615
|
+
const updateMemory = this.db.transaction(() => {
|
|
616
|
+
// Update memory content (FTS will be updated automatically by trigger)
|
|
617
|
+
this.stmts.updateMemory.run(newContent, newHash, existing.id);
|
|
618
|
+
// Update tags if provided
|
|
619
|
+
if (newTags !== undefined) {
|
|
620
|
+
// Delete old tags
|
|
621
|
+
this.stmts.deleteTagsForMemory.run(existing.id);
|
|
622
|
+
// Insert new tags
|
|
623
|
+
for (const tag of newTags) {
|
|
624
|
+
const normalizedTag = tag.trim().toLowerCase();
|
|
625
|
+
if (normalizedTag) {
|
|
626
|
+
this.stmts.insertTag.run(existing.id, normalizedTag);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return newHash;
|
|
631
|
+
});
|
|
632
|
+
const resultHash = updateMemory();
|
|
633
|
+
debugLogHash('MemoryService: Updated memory from hash:', hash, 'to new hash:', resultHash);
|
|
634
|
+
// Backup if needed (lazy, throttled)
|
|
635
|
+
this.backup?.backupIfNeeded();
|
|
636
|
+
return resultHash;
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
|
640
|
+
throw new Error(`Cannot update: a memory with the new content already exists (hash: ${newHash})`);
|
|
641
|
+
}
|
|
642
|
+
debugLog('MemoryService: Error updating memory:', error);
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get memory by hash
|
|
648
|
+
*/
|
|
649
|
+
getByHash(hash) {
|
|
650
|
+
const result = this.stmts.getMemoryByHash.get(hash);
|
|
651
|
+
if (!result) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
// Hydrate with tags from tags table
|
|
655
|
+
const tagRows = this.stmts.getTagsForMemory.all(result.id);
|
|
656
|
+
return {
|
|
657
|
+
id: result.id,
|
|
658
|
+
content: result.content,
|
|
659
|
+
tags: tagRows.map(t => t.tag),
|
|
660
|
+
createdAt: result.created_at,
|
|
661
|
+
hash: result.hash
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Close database connection
|
|
666
|
+
*/
|
|
667
|
+
close() {
|
|
668
|
+
if (this.db) {
|
|
669
|
+
this.db.close();
|
|
670
|
+
this.db = null;
|
|
671
|
+
debugLog('MemoryService: Database connection closed');
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Create a manual backup
|
|
676
|
+
*/
|
|
677
|
+
createBackup(label = 'manual') {
|
|
678
|
+
return this.backup?.backup(label) || null;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Export memories to JSON format with optional filtering
|
|
682
|
+
*/
|
|
683
|
+
exportMemories(filters) {
|
|
684
|
+
if (!this.db) {
|
|
685
|
+
throw new Error('Database not initialized');
|
|
686
|
+
}
|
|
687
|
+
// Use existing search method to get memories
|
|
688
|
+
// Pass undefined for query to use tag search (if tags provided) or recent search (if no filters)
|
|
689
|
+
const memories = this.search(undefined, // query - let search decide based on tags
|
|
690
|
+
filters?.tags, filters?.limit || 1000, // default high limit for export
|
|
691
|
+
undefined, // daysAgo
|
|
692
|
+
filters?.startDate?.toISOString(), filters?.endDate?.toISOString());
|
|
693
|
+
// Get relationships for each memory
|
|
694
|
+
const exportedMemories = memories.map((memory) => {
|
|
695
|
+
const relationships = this.getMemoryRelationships(memory.id);
|
|
696
|
+
return {
|
|
697
|
+
id: memory.id,
|
|
698
|
+
content: memory.content,
|
|
699
|
+
tags: memory.tags,
|
|
700
|
+
createdAt: memory.createdAt,
|
|
701
|
+
hash: memory.hash,
|
|
702
|
+
relationships: relationships.length > 0 ? relationships.map(rel => ({
|
|
703
|
+
relatedMemoryHash: rel.relatedMemoryHash,
|
|
704
|
+
relatedMemoryId: rel.relatedMemoryId,
|
|
705
|
+
relationshipType: rel.relationshipType
|
|
706
|
+
})) : undefined
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
return {
|
|
710
|
+
exportedAt: new Date().toISOString(),
|
|
711
|
+
exportVersion: getPackageVersion(),
|
|
712
|
+
source: hostname(),
|
|
713
|
+
totalMemories: exportedMemories.length,
|
|
714
|
+
memories: exportedMemories
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Import memories from JSON format
|
|
719
|
+
*/
|
|
720
|
+
importMemories(jsonData, options) {
|
|
721
|
+
if (!this.db) {
|
|
722
|
+
throw new Error('Database not initialized');
|
|
723
|
+
}
|
|
724
|
+
const result = {
|
|
725
|
+
imported: 0,
|
|
726
|
+
skipped: 0,
|
|
727
|
+
errors: []
|
|
728
|
+
};
|
|
729
|
+
let exportData;
|
|
730
|
+
try {
|
|
731
|
+
exportData = JSON.parse(jsonData);
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
throw new Error(`Invalid JSON format: ${error.message}`);
|
|
735
|
+
}
|
|
736
|
+
// Validate export format
|
|
737
|
+
if (!exportData.memories || !Array.isArray(exportData.memories)) {
|
|
738
|
+
throw new Error('Invalid export format: missing memories array');
|
|
739
|
+
}
|
|
740
|
+
debugLog(`Importing ${exportData.totalMemories} memories from export version ${exportData.exportVersion}`);
|
|
741
|
+
for (const memory of exportData.memories) {
|
|
742
|
+
try {
|
|
743
|
+
// Check for duplicates by hash
|
|
744
|
+
if (options?.skipDuplicates) {
|
|
745
|
+
const existing = this.getMemoryByHash(memory.hash);
|
|
746
|
+
if (existing) {
|
|
747
|
+
result.skipped++;
|
|
748
|
+
debugLog(`Skipped duplicate memory: ${memory.hash}`);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Store memory (without relationships for now)
|
|
753
|
+
const stored = this.store(memory.content, memory.tags);
|
|
754
|
+
if (stored) {
|
|
755
|
+
result.imported++;
|
|
756
|
+
debugLog(`Imported memory: ${stored}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
result.errors.push({
|
|
761
|
+
memory: memory,
|
|
762
|
+
error: error.message
|
|
763
|
+
});
|
|
764
|
+
debugLog(`Error importing memory ${memory.hash}: ${error.message}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Second pass: restore relationships
|
|
768
|
+
if (result.imported > 0) {
|
|
769
|
+
this.restoreRelationships(exportData.memories);
|
|
770
|
+
}
|
|
771
|
+
debugLog(`Import complete: ${result.imported} imported, ${result.skipped} skipped, ${result.errors.length} errors`);
|
|
772
|
+
// Trigger backup after import if enabled
|
|
773
|
+
if (this.backup && result.imported > 0) {
|
|
774
|
+
this.backup.backupIfNeeded();
|
|
775
|
+
}
|
|
776
|
+
return result;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Get relationships for a memory
|
|
780
|
+
*/
|
|
781
|
+
getMemoryRelationships(memoryId) {
|
|
782
|
+
if (!this.db)
|
|
783
|
+
return [];
|
|
784
|
+
const stmt = this.db.prepare(`
|
|
785
|
+
SELECT
|
|
786
|
+
r.to_memory_id as relatedMemoryId,
|
|
787
|
+
r.relationship_type as relationshipType,
|
|
788
|
+
m.hash as relatedMemoryHash
|
|
789
|
+
FROM relationships r
|
|
790
|
+
JOIN memories m ON m.id = r.to_memory_id
|
|
791
|
+
WHERE r.from_memory_id = ?
|
|
792
|
+
`);
|
|
793
|
+
const relationships = stmt.all(memoryId);
|
|
794
|
+
return relationships.map(rel => ({
|
|
795
|
+
relatedMemoryHash: rel.relatedMemoryHash,
|
|
796
|
+
relatedMemoryId: rel.relatedMemoryId,
|
|
797
|
+
relationshipType: rel.relationshipType
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Restore relationships after importing memories
|
|
802
|
+
*/
|
|
803
|
+
restoreRelationships(memories) {
|
|
804
|
+
if (!this.db)
|
|
805
|
+
return;
|
|
806
|
+
let restoredCount = 0;
|
|
807
|
+
for (const memory of memories) {
|
|
808
|
+
if (!memory.relationships || memory.relationships.length === 0)
|
|
809
|
+
continue;
|
|
810
|
+
// Find the imported memory by hash
|
|
811
|
+
const fromMemory = this.getMemoryByHash(memory.hash);
|
|
812
|
+
if (!fromMemory)
|
|
813
|
+
continue;
|
|
814
|
+
for (const rel of memory.relationships) {
|
|
815
|
+
// Find the related memory by hash
|
|
816
|
+
const toMemory = this.getMemoryByHash(rel.relatedMemoryHash);
|
|
817
|
+
if (!toMemory)
|
|
818
|
+
continue;
|
|
819
|
+
try {
|
|
820
|
+
// Create relationship
|
|
821
|
+
const stmt = this.db.prepare(`
|
|
822
|
+
INSERT OR IGNORE INTO relationships (from_memory_id, to_memory_id, relationship_type, created_at)
|
|
823
|
+
VALUES (?, ?, ?, ?)
|
|
824
|
+
`);
|
|
825
|
+
const info = stmt.run(fromMemory.id, toMemory.id, rel.relationshipType, new Date().toISOString());
|
|
826
|
+
if (info.changes > 0) {
|
|
827
|
+
restoredCount++;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
debugLog(`Warning: Could not restore relationship: ${error.message}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (restoredCount > 0) {
|
|
836
|
+
debugLog(`Restored ${restoredCount} relationships`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Get a memory by its hash
|
|
841
|
+
*/
|
|
842
|
+
getMemoryByHash(hash) {
|
|
843
|
+
if (!this.db)
|
|
844
|
+
return null;
|
|
845
|
+
const stmt = this.db.prepare(`
|
|
846
|
+
SELECT * FROM memories WHERE hash = ?
|
|
847
|
+
`);
|
|
848
|
+
const result = stmt.get(hash);
|
|
849
|
+
if (!result)
|
|
850
|
+
return null;
|
|
851
|
+
// Hydrate with tags from tags table
|
|
852
|
+
const tagRows = this.stmts.getTagsForMemory.all(result.id);
|
|
853
|
+
return {
|
|
854
|
+
id: result.id,
|
|
855
|
+
content: result.content,
|
|
856
|
+
tags: tagRows.map(t => t.tag),
|
|
857
|
+
createdAt: result.created_at,
|
|
858
|
+
hash: result.hash
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
//# sourceMappingURL=memory-service.js.map
|