memory-journal-mcp 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.dockerignore +88 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +76 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +89 -0
  5. package/.github/ISSUE_TEMPLATE/question.md +63 -0
  6. package/.github/dependabot.yml +110 -0
  7. package/.github/pull_request_template.md +110 -0
  8. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +346 -0
  9. package/.github/workflows/codeql.yml +45 -0
  10. package/.github/workflows/dependabot-auto-merge.yml +42 -0
  11. package/.github/workflows/docker-publish.yml +277 -0
  12. package/.github/workflows/lint-and-test.yml +58 -0
  13. package/.github/workflows/publish-npm.yml +75 -0
  14. package/.github/workflows/secrets-scanning.yml +32 -0
  15. package/.github/workflows/security-update.yml +99 -0
  16. package/.memory-journal-team.db +0 -0
  17. package/.trivyignore +18 -0
  18. package/CHANGELOG.md +19 -0
  19. package/CODE_OF_CONDUCT.md +128 -0
  20. package/CONTRIBUTING.md +209 -0
  21. package/DOCKER_README.md +377 -0
  22. package/Dockerfile +64 -0
  23. package/LICENSE +21 -0
  24. package/README.md +461 -0
  25. package/SECURITY.md +200 -0
  26. package/VERSION +1 -0
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +42 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/constants/ServerInstructions.d.ts +8 -0
  32. package/dist/constants/ServerInstructions.d.ts.map +1 -0
  33. package/dist/constants/ServerInstructions.js +26 -0
  34. package/dist/constants/ServerInstructions.js.map +1 -0
  35. package/dist/database/SqliteAdapter.d.ts +198 -0
  36. package/dist/database/SqliteAdapter.d.ts.map +1 -0
  37. package/dist/database/SqliteAdapter.js +736 -0
  38. package/dist/database/SqliteAdapter.js.map +1 -0
  39. package/dist/filtering/ToolFilter.d.ts +63 -0
  40. package/dist/filtering/ToolFilter.d.ts.map +1 -0
  41. package/dist/filtering/ToolFilter.js +242 -0
  42. package/dist/filtering/ToolFilter.js.map +1 -0
  43. package/dist/github/GitHubIntegration.d.ts +91 -0
  44. package/dist/github/GitHubIntegration.d.ts.map +1 -0
  45. package/dist/github/GitHubIntegration.js +317 -0
  46. package/dist/github/GitHubIntegration.js.map +1 -0
  47. package/dist/handlers/prompts/index.d.ts +28 -0
  48. package/dist/handlers/prompts/index.d.ts.map +1 -0
  49. package/dist/handlers/prompts/index.js +366 -0
  50. package/dist/handlers/prompts/index.js.map +1 -0
  51. package/dist/handlers/resources/index.d.ts +27 -0
  52. package/dist/handlers/resources/index.d.ts.map +1 -0
  53. package/dist/handlers/resources/index.js +453 -0
  54. package/dist/handlers/resources/index.js.map +1 -0
  55. package/dist/handlers/tools/index.d.ts +26 -0
  56. package/dist/handlers/tools/index.d.ts.map +1 -0
  57. package/dist/handlers/tools/index.js +982 -0
  58. package/dist/handlers/tools/index.js.map +1 -0
  59. package/dist/index.d.ts +11 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +13 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/server/McpServer.d.ts +18 -0
  64. package/dist/server/McpServer.d.ts.map +1 -0
  65. package/dist/server/McpServer.js +171 -0
  66. package/dist/server/McpServer.js.map +1 -0
  67. package/dist/types/index.d.ts +300 -0
  68. package/dist/types/index.d.ts.map +1 -0
  69. package/dist/types/index.js +15 -0
  70. package/dist/types/index.js.map +1 -0
  71. package/dist/utils/McpLogger.d.ts +61 -0
  72. package/dist/utils/McpLogger.d.ts.map +1 -0
  73. package/dist/utils/McpLogger.js +113 -0
  74. package/dist/utils/McpLogger.js.map +1 -0
  75. package/dist/utils/logger.d.ts +30 -0
  76. package/dist/utils/logger.d.ts.map +1 -0
  77. package/dist/utils/logger.js +70 -0
  78. package/dist/utils/logger.js.map +1 -0
  79. package/dist/vector/VectorSearchManager.d.ts +63 -0
  80. package/dist/vector/VectorSearchManager.d.ts.map +1 -0
  81. package/dist/vector/VectorSearchManager.js +235 -0
  82. package/dist/vector/VectorSearchManager.js.map +1 -0
  83. package/docker-compose.yml +37 -0
  84. package/eslint.config.js +86 -0
  85. package/mcp-config-example.json +21 -0
  86. package/package.json +71 -0
  87. package/releases/release-notes-v2.2.0.md +165 -0
  88. package/releases/release-notes.md +214 -0
  89. package/releases/v3.0.0.md +236 -0
  90. package/server.json +42 -0
  91. package/src/cli.ts +52 -0
  92. package/src/constants/ServerInstructions.ts +25 -0
  93. package/src/database/SqliteAdapter.ts +952 -0
  94. package/src/filtering/ToolFilter.ts +271 -0
  95. package/src/github/GitHubIntegration.ts +409 -0
  96. package/src/handlers/prompts/index.ts +420 -0
  97. package/src/handlers/resources/index.ts +529 -0
  98. package/src/handlers/tools/index.ts +1081 -0
  99. package/src/index.ts +53 -0
  100. package/src/server/McpServer.ts +230 -0
  101. package/src/types/index.ts +435 -0
  102. package/src/types/sql.js.d.ts +34 -0
  103. package/src/utils/McpLogger.ts +155 -0
  104. package/src/utils/logger.ts +98 -0
  105. package/src/vector/VectorSearchManager.ts +277 -0
  106. package/tools.json +300 -0
  107. package/tsconfig.json +51 -0
@@ -0,0 +1,736 @@
1
+ /**
2
+ * Memory Journal MCP Server - SQLite Database Adapter
3
+ *
4
+ * Manages SQLite database with FTS5 full-text search using sql.js.
5
+ * Note: sql.js is pure JavaScript, no native compilation required.
6
+ */
7
+ import initSqlJs from 'sql.js';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { logger } from '../utils/logger.js';
11
+ // Schema SQL for initialization
12
+ const SCHEMA_SQL = `
13
+ -- Main journal entries table
14
+ CREATE TABLE IF NOT EXISTS memory_journal (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ entry_type TEXT NOT NULL,
17
+ content TEXT NOT NULL,
18
+ timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
19
+ is_personal INTEGER DEFAULT 1,
20
+ significance_type TEXT,
21
+ auto_context TEXT,
22
+ deleted_at TEXT,
23
+ -- GitHub integration fields
24
+ project_number INTEGER,
25
+ project_owner TEXT,
26
+ issue_number INTEGER,
27
+ issue_url TEXT,
28
+ pr_number INTEGER,
29
+ pr_url TEXT,
30
+ pr_status TEXT,
31
+ workflow_run_id INTEGER,
32
+ workflow_name TEXT,
33
+ workflow_status TEXT
34
+ );
35
+
36
+ -- Tags table
37
+ CREATE TABLE IF NOT EXISTS tags (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ name TEXT UNIQUE NOT NULL,
40
+ usage_count INTEGER DEFAULT 0
41
+ );
42
+
43
+ -- Junction table for entry-tag relationships
44
+ CREATE TABLE IF NOT EXISTS entry_tags (
45
+ entry_id INTEGER NOT NULL,
46
+ tag_id INTEGER NOT NULL,
47
+ PRIMARY KEY (entry_id, tag_id),
48
+ FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
49
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
50
+ );
51
+
52
+ -- Relationships between entries
53
+ CREATE TABLE IF NOT EXISTS relationships (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ from_entry_id INTEGER NOT NULL,
56
+ to_entry_id INTEGER NOT NULL,
57
+ relationship_type TEXT NOT NULL,
58
+ description TEXT,
59
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
60
+ FOREIGN KEY (from_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
61
+ FOREIGN KEY (to_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
62
+ );
63
+
64
+ -- Embeddings for vector search (stored as JSON for sql.js compatibility)
65
+ CREATE TABLE IF NOT EXISTS embeddings (
66
+ entry_id INTEGER PRIMARY KEY,
67
+ embedding TEXT NOT NULL,
68
+ model_name TEXT NOT NULL,
69
+ FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
70
+ );
71
+
72
+ -- Indexes for performance
73
+ CREATE INDEX IF NOT EXISTS idx_memory_journal_timestamp ON memory_journal(timestamp);
74
+ CREATE INDEX IF NOT EXISTS idx_memory_journal_type ON memory_journal(entry_type);
75
+ CREATE INDEX IF NOT EXISTS idx_memory_journal_personal ON memory_journal(is_personal);
76
+ CREATE INDEX IF NOT EXISTS idx_memory_journal_deleted ON memory_journal(deleted_at);
77
+ CREATE INDEX IF NOT EXISTS idx_memory_journal_project ON memory_journal(project_number);
78
+ CREATE INDEX IF NOT EXISTS idx_memory_journal_issue ON memory_journal(issue_number);
79
+ CREATE INDEX IF NOT EXISTS idx_memory_journal_pr ON memory_journal(pr_number);
80
+ CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);
81
+ CREATE INDEX IF NOT EXISTS idx_entry_tags_entry ON entry_tags(entry_id);
82
+ CREATE INDEX IF NOT EXISTS idx_entry_tags_tag ON entry_tags(tag_id);
83
+ CREATE INDEX IF NOT EXISTS idx_relationships_from ON relationships(from_entry_id);
84
+ CREATE INDEX IF NOT EXISTS idx_relationships_to ON relationships(to_entry_id);
85
+ `;
86
+ /**
87
+ * SQLite Database Adapter for Memory Journal using sql.js
88
+ */
89
+ export class SqliteAdapter {
90
+ db = null;
91
+ dbPath;
92
+ initialized = false;
93
+ constructor(dbPath) {
94
+ this.dbPath = dbPath;
95
+ }
96
+ /**
97
+ * Initialize the database (must be called before using)
98
+ */
99
+ async initialize() {
100
+ if (this.initialized)
101
+ return;
102
+ const SQL = await initSqlJs();
103
+ // Try to load existing database
104
+ let dbBuffer = null;
105
+ if (fs.existsSync(this.dbPath)) {
106
+ try {
107
+ dbBuffer = fs.readFileSync(this.dbPath);
108
+ }
109
+ catch {
110
+ // File doesn't exist or can't be read, create new
111
+ }
112
+ }
113
+ if (dbBuffer) {
114
+ this.db = new SQL.Database(dbBuffer);
115
+ }
116
+ else {
117
+ this.db = new SQL.Database();
118
+ // Ensure directory exists
119
+ const dir = path.dirname(this.dbPath);
120
+ if (dir && !fs.existsSync(dir)) {
121
+ fs.mkdirSync(dir, { recursive: true });
122
+ }
123
+ }
124
+ // Initialize schema
125
+ this.db.run(SCHEMA_SQL);
126
+ this.initialized = true;
127
+ logger.info('Database opened', { module: 'SqliteAdapter', dbPath: this.dbPath });
128
+ // Save after initialization
129
+ this.save();
130
+ }
131
+ /**
132
+ * Save database to disk
133
+ */
134
+ save() {
135
+ if (!this.db)
136
+ return;
137
+ const data = this.db.export();
138
+ const buffer = Buffer.from(data);
139
+ fs.writeFileSync(this.dbPath, buffer);
140
+ }
141
+ /**
142
+ * Close database connection
143
+ */
144
+ close() {
145
+ if (this.db) {
146
+ this.save();
147
+ this.db.close();
148
+ this.db = null;
149
+ }
150
+ logger.info('Database closed', { module: 'SqliteAdapter' });
151
+ }
152
+ /**
153
+ * Ensure database is initialized
154
+ */
155
+ ensureDb() {
156
+ if (!this.db) {
157
+ throw new Error('Database not initialized. Call initialize() first.');
158
+ }
159
+ return this.db;
160
+ }
161
+ // =========================================================================
162
+ // Entry Operations
163
+ // =========================================================================
164
+ /**
165
+ * Create a new journal entry
166
+ */
167
+ createEntry(input) {
168
+ const db = this.ensureDb();
169
+ const { content, entryType = 'personal_reflection', tags = [], isPersonal = true, significanceType = null, autoContext = null, projectNumber, projectOwner, issueNumber, issueUrl, prNumber, prUrl, prStatus, workflowRunId, workflowName, workflowStatus, } = input;
170
+ db.run(`
171
+ INSERT INTO memory_journal (
172
+ entry_type, content, is_personal, significance_type, auto_context,
173
+ project_number, project_owner, issue_number, issue_url,
174
+ pr_number, pr_url, pr_status, workflow_run_id, workflow_name, workflow_status
175
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
176
+ `, [
177
+ entryType,
178
+ content,
179
+ isPersonal ? 1 : 0,
180
+ significanceType,
181
+ autoContext,
182
+ projectNumber ?? null,
183
+ projectOwner ?? null,
184
+ issueNumber ?? null,
185
+ issueUrl ?? null,
186
+ prNumber ?? null,
187
+ prUrl ?? null,
188
+ prStatus ?? null,
189
+ workflowRunId ?? null,
190
+ workflowName ?? null,
191
+ workflowStatus ?? null,
192
+ ]);
193
+ // Get the inserted ID
194
+ const result = db.exec('SELECT last_insert_rowid() as id');
195
+ const entryId = result[0]?.values[0]?.[0];
196
+ // Create tags and link them
197
+ if (tags.length > 0) {
198
+ this.linkTagsToEntry(entryId, tags);
199
+ }
200
+ this.save();
201
+ logger.info('Entry created', {
202
+ module: 'SqliteAdapter',
203
+ operation: 'createEntry',
204
+ entityId: entryId
205
+ });
206
+ const entry = this.getEntryById(entryId);
207
+ if (!entry) {
208
+ throw new Error(`Failed to retrieve created entry with ID ${entryId}`);
209
+ }
210
+ return entry;
211
+ }
212
+ /**
213
+ * Get entry by ID
214
+ */
215
+ getEntryById(id) {
216
+ const db = this.ensureDb();
217
+ const result = db.exec(`SELECT * FROM memory_journal WHERE id = ? AND deleted_at IS NULL`, [id]);
218
+ if (result.length === 0 || result[0]?.values.length === 0)
219
+ return null;
220
+ const columns = result[0]?.columns ?? [];
221
+ const values = result[0]?.values[0] ?? [];
222
+ const row = this.rowToObject(columns, values);
223
+ return this.rowToEntry(row);
224
+ }
225
+ /**
226
+ * Get recent entries
227
+ */
228
+ getRecentEntries(limit = 10, isPersonal) {
229
+ const db = this.ensureDb();
230
+ let sql = `SELECT * FROM memory_journal WHERE deleted_at IS NULL`;
231
+ const params = [];
232
+ if (isPersonal !== undefined) {
233
+ sql += ` AND is_personal = ?`;
234
+ params.push(isPersonal ? 1 : 0);
235
+ }
236
+ sql += ` ORDER BY timestamp DESC LIMIT ?`;
237
+ params.push(limit);
238
+ const result = db.exec(sql, params);
239
+ if (result.length === 0)
240
+ return [];
241
+ const columns = result[0]?.columns ?? [];
242
+ return (result[0]?.values ?? []).map(values => this.rowToEntry(this.rowToObject(columns, values)));
243
+ }
244
+ /**
245
+ * Update an entry
246
+ */
247
+ updateEntry(id, updates) {
248
+ const db = this.ensureDb();
249
+ const entry = this.getEntryById(id);
250
+ if (!entry)
251
+ return null;
252
+ const setClause = [];
253
+ const params = [];
254
+ if (updates.content !== undefined) {
255
+ setClause.push('content = ?');
256
+ params.push(updates.content);
257
+ }
258
+ if (updates.entryType !== undefined) {
259
+ setClause.push('entry_type = ?');
260
+ params.push(updates.entryType);
261
+ }
262
+ if (updates.isPersonal !== undefined) {
263
+ setClause.push('is_personal = ?');
264
+ params.push(updates.isPersonal ? 1 : 0);
265
+ }
266
+ if (setClause.length > 0) {
267
+ params.push(id);
268
+ db.run(`UPDATE memory_journal SET ${setClause.join(', ')} WHERE id = ?`, params);
269
+ }
270
+ // Update tags if provided
271
+ if (updates.tags !== undefined) {
272
+ db.run('DELETE FROM entry_tags WHERE entry_id = ?', [id]);
273
+ this.linkTagsToEntry(id, updates.tags);
274
+ }
275
+ this.save();
276
+ logger.info('Entry updated', {
277
+ module: 'SqliteAdapter',
278
+ operation: 'updateEntry',
279
+ entityId: id
280
+ });
281
+ return this.getEntryById(id);
282
+ }
283
+ /**
284
+ * Soft delete an entry
285
+ */
286
+ deleteEntry(id, permanent = false) {
287
+ const db = this.ensureDb();
288
+ if (permanent) {
289
+ db.run('DELETE FROM memory_journal WHERE id = ?', [id]);
290
+ }
291
+ else {
292
+ db.run(`UPDATE memory_journal SET deleted_at = datetime('now') WHERE id = ?`, [id]);
293
+ }
294
+ this.save();
295
+ return true;
296
+ }
297
+ // =========================================================================
298
+ // Search Operations
299
+ // =========================================================================
300
+ /**
301
+ * Full-text search entries (using LIKE for sql.js - FTS5 not supported)
302
+ */
303
+ searchEntries(query, options = {}) {
304
+ const db = this.ensureDb();
305
+ const { limit = 10, isPersonal, projectNumber, issueNumber, prNumber } = options;
306
+ let sql = `
307
+ SELECT * FROM memory_journal
308
+ WHERE deleted_at IS NULL AND content LIKE ?
309
+ `;
310
+ const params = [`%${query}%`];
311
+ if (isPersonal !== undefined) {
312
+ sql += ` AND is_personal = ?`;
313
+ params.push(isPersonal ? 1 : 0);
314
+ }
315
+ if (projectNumber !== undefined) {
316
+ sql += ` AND project_number = ?`;
317
+ params.push(projectNumber);
318
+ }
319
+ if (issueNumber !== undefined) {
320
+ sql += ` AND issue_number = ?`;
321
+ params.push(issueNumber);
322
+ }
323
+ if (prNumber !== undefined) {
324
+ sql += ` AND pr_number = ?`;
325
+ params.push(prNumber);
326
+ }
327
+ sql += ` ORDER BY timestamp DESC LIMIT ?`;
328
+ params.push(limit);
329
+ const result = db.exec(sql, params);
330
+ if (result.length === 0)
331
+ return [];
332
+ const columns = result[0]?.columns ?? [];
333
+ return (result[0]?.values ?? []).map(values => this.rowToEntry(this.rowToObject(columns, values)));
334
+ }
335
+ /**
336
+ * Search by date range
337
+ */
338
+ searchByDateRange(startDate, endDate, options = {}) {
339
+ const db = this.ensureDb();
340
+ const { entryType, tags, isPersonal, projectNumber } = options;
341
+ let sql = `
342
+ SELECT DISTINCT m.* FROM memory_journal m
343
+ LEFT JOIN entry_tags et ON m.id = et.entry_id
344
+ LEFT JOIN tags t ON et.tag_id = t.id
345
+ WHERE m.deleted_at IS NULL
346
+ AND m.timestamp >= ? AND m.timestamp <= ?
347
+ `;
348
+ const params = [startDate, endDate + ' 23:59:59'];
349
+ if (entryType) {
350
+ sql += ` AND m.entry_type = ?`;
351
+ params.push(entryType);
352
+ }
353
+ if (isPersonal !== undefined) {
354
+ sql += ` AND m.is_personal = ?`;
355
+ params.push(isPersonal ? 1 : 0);
356
+ }
357
+ if (projectNumber !== undefined) {
358
+ sql += ` AND m.project_number = ?`;
359
+ params.push(projectNumber);
360
+ }
361
+ if (tags && tags.length > 0) {
362
+ const placeholders = tags.map(() => '?').join(',');
363
+ sql += ` AND t.name IN (${placeholders})`;
364
+ params.push(...tags);
365
+ }
366
+ sql += ` ORDER BY m.timestamp DESC`;
367
+ const result = db.exec(sql, params);
368
+ if (result.length === 0)
369
+ return [];
370
+ const columns = result[0]?.columns ?? [];
371
+ return (result[0]?.values ?? []).map(values => this.rowToEntry(this.rowToObject(columns, values)));
372
+ }
373
+ // =========================================================================
374
+ // Tag Operations
375
+ // =========================================================================
376
+ /**
377
+ * Get or create tags and link to entry
378
+ */
379
+ linkTagsToEntry(entryId, tagNames) {
380
+ const db = this.ensureDb();
381
+ for (const tagName of tagNames) {
382
+ // Insert or ignore tag
383
+ db.run('INSERT OR IGNORE INTO tags (name, usage_count) VALUES (?, 0)', [tagName]);
384
+ // Get tag ID
385
+ const result = db.exec('SELECT id FROM tags WHERE name = ?', [tagName]);
386
+ const tagId = result[0]?.values[0]?.[0];
387
+ if (tagId !== undefined) {
388
+ // Link tag to entry
389
+ db.run('INSERT OR IGNORE INTO entry_tags (entry_id, tag_id) VALUES (?, ?)', [entryId, tagId]);
390
+ // Increment usage
391
+ db.run('UPDATE tags SET usage_count = usage_count + 1 WHERE id = ?', [tagId]);
392
+ }
393
+ }
394
+ }
395
+ /**
396
+ * Get tags for an entry
397
+ */
398
+ getTagsForEntry(entryId) {
399
+ const db = this.ensureDb();
400
+ const result = db.exec(`
401
+ SELECT t.name FROM tags t
402
+ JOIN entry_tags et ON t.id = et.tag_id
403
+ WHERE et.entry_id = ?
404
+ `, [entryId]);
405
+ if (result.length === 0)
406
+ return [];
407
+ return (result[0]?.values ?? []).map((v) => v[0]);
408
+ }
409
+ /**
410
+ * List all tags
411
+ */
412
+ listTags() {
413
+ const db = this.ensureDb();
414
+ const result = db.exec('SELECT * FROM tags ORDER BY usage_count DESC');
415
+ if (result.length === 0)
416
+ return [];
417
+ return (result[0]?.values ?? []).map(v => ({
418
+ id: v[0],
419
+ name: v[1],
420
+ usageCount: v[2],
421
+ }));
422
+ }
423
+ // =========================================================================
424
+ // Relationship Operations
425
+ // =========================================================================
426
+ /**
427
+ * Link two entries
428
+ */
429
+ linkEntries(fromEntryId, toEntryId, relationshipType, description) {
430
+ const db = this.ensureDb();
431
+ db.run(`
432
+ INSERT INTO relationships (from_entry_id, to_entry_id, relationship_type, description)
433
+ VALUES (?, ?, ?, ?)
434
+ `, [fromEntryId, toEntryId, relationshipType, description ?? null]);
435
+ const result = db.exec('SELECT last_insert_rowid() as id');
436
+ const id = result[0]?.values[0]?.[0];
437
+ this.save();
438
+ return {
439
+ id,
440
+ fromEntryId,
441
+ toEntryId,
442
+ relationshipType,
443
+ description: description ?? null,
444
+ createdAt: new Date().toISOString(),
445
+ };
446
+ }
447
+ /**
448
+ * Get relationships for an entry
449
+ */
450
+ getRelationships(entryId) {
451
+ const db = this.ensureDb();
452
+ const result = db.exec(`
453
+ SELECT * FROM relationships
454
+ WHERE from_entry_id = ? OR to_entry_id = ?
455
+ `, [entryId, entryId]);
456
+ if (result.length === 0)
457
+ return [];
458
+ const columns = result[0]?.columns ?? [];
459
+ return (result[0]?.values ?? []).map((values) => {
460
+ const row = this.rowToObject(columns, values);
461
+ return {
462
+ id: row['id'],
463
+ fromEntryId: row['from_entry_id'],
464
+ toEntryId: row['to_entry_id'],
465
+ relationshipType: row['relationship_type'],
466
+ description: row['description'],
467
+ createdAt: row['created_at'],
468
+ };
469
+ });
470
+ }
471
+ // =========================================================================
472
+ // Statistics
473
+ // =========================================================================
474
+ /**
475
+ * Get entry statistics
476
+ */
477
+ getStatistics(groupBy = 'week') {
478
+ const db = this.ensureDb();
479
+ // Total entries
480
+ const totalResult = db.exec(`
481
+ SELECT COUNT(*) as count FROM memory_journal WHERE deleted_at IS NULL
482
+ `);
483
+ const totalEntries = totalResult[0]?.values[0]?.[0] ?? 0;
484
+ // By type
485
+ const byTypeResult = db.exec(`
486
+ SELECT entry_type, COUNT(*) as count
487
+ FROM memory_journal
488
+ WHERE deleted_at IS NULL
489
+ GROUP BY entry_type
490
+ `);
491
+ const entriesByType = {};
492
+ for (const row of (byTypeResult[0]?.values ?? [])) {
493
+ entriesByType[row[0]] = row[1];
494
+ }
495
+ // By period
496
+ let dateFormat;
497
+ switch (groupBy) {
498
+ case 'day':
499
+ dateFormat = '%Y-%m-%d';
500
+ break;
501
+ case 'month':
502
+ dateFormat = '%Y-%m';
503
+ break;
504
+ default:
505
+ dateFormat = '%Y-W%W';
506
+ }
507
+ const byPeriodResult = db.exec(`
508
+ SELECT strftime('${dateFormat}', timestamp) as period, COUNT(*) as count
509
+ FROM memory_journal
510
+ WHERE deleted_at IS NULL
511
+ GROUP BY period
512
+ ORDER BY period DESC
513
+ LIMIT 52
514
+ `);
515
+ const entriesByPeriod = (byPeriodResult[0]?.values ?? []).map((v) => ({
516
+ period: v[0],
517
+ count: v[1],
518
+ }));
519
+ return {
520
+ totalEntries,
521
+ entriesByType,
522
+ entriesByPeriod,
523
+ };
524
+ }
525
+ // =========================================================================
526
+ // Backup Operations
527
+ // =========================================================================
528
+ /**
529
+ * Get the backups directory path (relative to database location)
530
+ */
531
+ getBackupsDir() {
532
+ return path.join(path.dirname(this.dbPath), 'backups');
533
+ }
534
+ /**
535
+ * Export database to a backup file
536
+ * @param backupName Optional custom name (default: timestamp-based)
537
+ * @returns Backup file info
538
+ */
539
+ exportToFile(backupName) {
540
+ const db = this.ensureDb();
541
+ const backupsDir = this.getBackupsDir();
542
+ // Ensure backups directory exists
543
+ if (!fs.existsSync(backupsDir)) {
544
+ fs.mkdirSync(backupsDir, { recursive: true });
545
+ }
546
+ // Generate filename with timestamp
547
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
548
+ const sanitizedName = backupName
549
+ ? backupName.replace(/[/\\:*?"<>|]/g, '_').slice(0, 50)
550
+ : `backup_${timestamp}`;
551
+ const filename = `${sanitizedName}.db`;
552
+ const backupPath = path.join(backupsDir, filename);
553
+ // Export database
554
+ const data = db.export();
555
+ const buffer = Buffer.from(data);
556
+ fs.writeFileSync(backupPath, buffer);
557
+ const stats = fs.statSync(backupPath);
558
+ logger.info('Backup created', {
559
+ module: 'SqliteAdapter',
560
+ operation: 'exportToFile',
561
+ context: { backupPath, sizeBytes: stats.size }
562
+ });
563
+ return {
564
+ filename,
565
+ path: backupPath,
566
+ sizeBytes: stats.size,
567
+ };
568
+ }
569
+ /**
570
+ * List all available backup files
571
+ * @returns Array of backup file information
572
+ */
573
+ listBackups() {
574
+ const backupsDir = this.getBackupsDir();
575
+ if (!fs.existsSync(backupsDir)) {
576
+ return [];
577
+ }
578
+ const files = fs.readdirSync(backupsDir);
579
+ const backups = [];
580
+ for (const filename of files) {
581
+ if (!filename.endsWith('.db'))
582
+ continue;
583
+ const filePath = path.join(backupsDir, filename);
584
+ try {
585
+ const stats = fs.statSync(filePath);
586
+ if (stats.isFile()) {
587
+ backups.push({
588
+ filename,
589
+ path: filePath,
590
+ sizeBytes: stats.size,
591
+ createdAt: stats.birthtime.toISOString(),
592
+ });
593
+ }
594
+ }
595
+ catch {
596
+ // Skip files that can't be read
597
+ }
598
+ }
599
+ // Sort by creation time, newest first
600
+ backups.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
601
+ return backups;
602
+ }
603
+ /**
604
+ * Restore database from a backup file
605
+ * @param filename Backup filename to restore from
606
+ * @returns Statistics about the restore operation
607
+ */
608
+ async restoreFromFile(filename) {
609
+ // Validate filename (prevent path traversal)
610
+ if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
611
+ throw new Error('Invalid backup filename: path separators not allowed');
612
+ }
613
+ const backupsDir = this.getBackupsDir();
614
+ const backupPath = path.join(backupsDir, filename);
615
+ if (!fs.existsSync(backupPath)) {
616
+ throw new Error(`Backup file not found: ${filename}`);
617
+ }
618
+ // Get current entry count for comparison
619
+ const db = this.ensureDb();
620
+ const currentCountResult = db.exec('SELECT COUNT(*) FROM memory_journal WHERE deleted_at IS NULL');
621
+ const previousEntryCount = currentCountResult[0]?.values[0]?.[0] ?? 0;
622
+ // Create auto-backup before restore
623
+ this.exportToFile(`pre_restore_${new Date().toISOString().replace(/[:.]/g, '-')}`);
624
+ // Close current database
625
+ this.db?.close();
626
+ this.db = null;
627
+ this.initialized = false;
628
+ // Read backup file
629
+ const backupBuffer = fs.readFileSync(backupPath);
630
+ // Initialize new database from backup
631
+ const SQL = await import('sql.js').then(m => m.default());
632
+ this.db = new SQL.Database(backupBuffer);
633
+ this.initialized = true;
634
+ // Get new entry count
635
+ const newCountResult = this.db.exec('SELECT COUNT(*) FROM memory_journal WHERE deleted_at IS NULL');
636
+ const newEntryCount = newCountResult[0]?.values[0]?.[0] ?? 0;
637
+ // Save to main database path
638
+ this.save();
639
+ logger.info('Database restored from backup', {
640
+ module: 'SqliteAdapter',
641
+ operation: 'restoreFromFile',
642
+ context: { backupPath, previousEntryCount, newEntryCount }
643
+ });
644
+ return {
645
+ restoredFrom: filename,
646
+ previousEntryCount,
647
+ newEntryCount,
648
+ };
649
+ }
650
+ // =========================================================================
651
+ // Health Status
652
+ // =========================================================================
653
+ /**
654
+ * Get database health status for diagnostics
655
+ */
656
+ getHealthStatus() {
657
+ const db = this.ensureDb();
658
+ // Get file size
659
+ let sizeBytes = 0;
660
+ try {
661
+ const stats = fs.statSync(this.dbPath);
662
+ sizeBytes = stats.size;
663
+ }
664
+ catch {
665
+ // File may not exist on disk yet
666
+ }
667
+ // Entry counts
668
+ const entryResult = db.exec('SELECT COUNT(*) FROM memory_journal WHERE deleted_at IS NULL');
669
+ const deletedResult = db.exec('SELECT COUNT(*) FROM memory_journal WHERE deleted_at IS NOT NULL');
670
+ const relResult = db.exec('SELECT COUNT(*) FROM relationships');
671
+ const tagResult = db.exec('SELECT COUNT(*) FROM tags');
672
+ const entryCount = entryResult[0]?.values[0]?.[0] ?? 0;
673
+ const deletedEntryCount = deletedResult[0]?.values[0]?.[0] ?? 0;
674
+ const relationshipCount = relResult[0]?.values[0]?.[0] ?? 0;
675
+ const tagCount = tagResult[0]?.values[0]?.[0] ?? 0;
676
+ // Backup info
677
+ const backups = this.listBackups();
678
+ const lastBackup = backups[0] ?? null;
679
+ return {
680
+ database: {
681
+ path: this.dbPath,
682
+ sizeBytes,
683
+ entryCount,
684
+ deletedEntryCount,
685
+ relationshipCount,
686
+ tagCount,
687
+ },
688
+ backups: {
689
+ directory: this.getBackupsDir(),
690
+ count: backups.length,
691
+ lastBackup: lastBackup ? {
692
+ filename: lastBackup.filename,
693
+ createdAt: lastBackup.createdAt,
694
+ sizeBytes: lastBackup.sizeBytes,
695
+ } : null,
696
+ },
697
+ };
698
+ }
699
+ // =========================================================================
700
+ // Helpers
701
+ // =========================================================================
702
+ /**
703
+ * Convert columns and values to object
704
+ */
705
+ rowToObject(columns, values) {
706
+ const obj = {};
707
+ columns.forEach((col, i) => {
708
+ obj[col] = values[i];
709
+ });
710
+ return obj;
711
+ }
712
+ /**
713
+ * Convert database row to JournalEntry
714
+ */
715
+ rowToEntry(row) {
716
+ const id = row['id'];
717
+ return {
718
+ id,
719
+ entryType: row['entry_type'],
720
+ content: row['content'],
721
+ timestamp: row['timestamp'],
722
+ isPersonal: row['is_personal'] === 1,
723
+ significanceType: row['significance_type'],
724
+ autoContext: row['auto_context'],
725
+ deletedAt: row['deleted_at'],
726
+ tags: this.getTagsForEntry(id),
727
+ };
728
+ }
729
+ /**
730
+ * Get raw database for advanced operations
731
+ */
732
+ getRawDb() {
733
+ return this.ensureDb();
734
+ }
735
+ }
736
+ //# sourceMappingURL=SqliteAdapter.js.map