@esparkman/pensieve 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,12 +17,12 @@ describe('Database: Limits Configuration', () => {
17
17
  describe('Database: Field Truncation', () => {
18
18
  let db;
19
19
  let testDir;
20
- beforeEach(() => {
20
+ beforeEach(async () => {
21
21
  // Create a temporary directory for tests
22
22
  testDir = join(tmpdir(), `pensieve-test-${Date.now()}`);
23
23
  mkdirSync(testDir, { recursive: true });
24
24
  mkdirSync(join(testDir, '.pensieve'), { recursive: true });
25
- db = new MemoryDatabase(testDir);
25
+ db = await MemoryDatabase.create(testDir);
26
26
  });
27
27
  afterEach(() => {
28
28
  db.close();
@@ -139,11 +139,11 @@ describe('Database: Field Truncation', () => {
139
139
  describe('Database: Storage Limits and Pruning', () => {
140
140
  let db;
141
141
  let testDir;
142
- beforeEach(() => {
142
+ beforeEach(async () => {
143
143
  testDir = join(tmpdir(), `pensieve-test-${Date.now()}`);
144
144
  mkdirSync(testDir, { recursive: true });
145
145
  mkdirSync(join(testDir, '.pensieve'), { recursive: true });
146
- db = new MemoryDatabase(testDir);
146
+ db = await MemoryDatabase.create(testDir);
147
147
  });
148
148
  afterEach(() => {
149
149
  db.close();
@@ -190,13 +190,13 @@ describe('Database: Storage Limits and Pruning', () => {
190
190
  });
191
191
  });
192
192
  describe('Database: Path Resolution', () => {
193
- it('uses PENSIEVE_DB_PATH environment variable when set', () => {
193
+ it('uses PENSIEVE_DB_PATH environment variable when set', async () => {
194
194
  const testPath = join(tmpdir(), `pensieve-env-test-${Date.now()}`);
195
195
  mkdirSync(testPath, { recursive: true });
196
196
  const customDbPath = join(testPath, 'custom.sqlite');
197
197
  process.env.PENSIEVE_DB_PATH = customDbPath;
198
198
  try {
199
- const db = new MemoryDatabase();
199
+ const db = await MemoryDatabase.create();
200
200
  db.addDecision({ topic: 'test', decision: 'test' });
201
201
  // Verify the database was created at the custom path
202
202
  expect(existsSync(customDbPath)).toBe(true);
@@ -64,15 +64,17 @@ export interface OpenQuestion {
64
64
  }
65
65
  export declare class MemoryDatabase {
66
66
  private db;
67
- private projectPath;
68
67
  private dbPath;
69
- constructor(projectPath?: string);
70
- private openDatabase;
68
+ private constructor();
71
69
  /**
72
- * Check if the database is writable and reconnect if needed
70
+ * Create a new MemoryDatabase instance (async factory)
73
71
  */
74
- ensureWritable(): boolean;
75
- private getDbPath;
72
+ static create(projectPath?: string): Promise<MemoryDatabase>;
73
+ /**
74
+ * Save database to disk
75
+ */
76
+ private save;
77
+ private static getDbPath;
76
78
  /**
77
79
  * Truncate a string to the maximum field length
78
80
  */
@@ -82,6 +84,18 @@ export declare class MemoryDatabase {
82
84
  */
83
85
  private pruneIfNeeded;
84
86
  private initSchema;
87
+ /**
88
+ * Get last inserted row ID
89
+ */
90
+ private getLastInsertRowId;
91
+ /**
92
+ * Execute a query and return all rows as objects
93
+ */
94
+ private queryAll;
95
+ /**
96
+ * Execute a query and return the first row as an object
97
+ */
98
+ private queryOne;
85
99
  addDecision(decision: Decision): number;
86
100
  searchDecisions(query: string): Decision[];
87
101
  getRecentDecisions(limit?: number): Decision[];
package/dist/database.js CHANGED
@@ -1,5 +1,5 @@
1
- import Database from 'better-sqlite3';
2
- import { existsSync, mkdirSync } from 'fs';
1
+ import initSqlJs from 'sql.js';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { dirname, join } from 'path';
4
4
  import { homedir } from 'os';
5
5
  // Configuration limits
@@ -14,60 +14,59 @@ export const LIMITS = {
14
14
  };
15
15
  export class MemoryDatabase {
16
16
  db;
17
- projectPath;
18
17
  dbPath;
19
- constructor(projectPath) {
20
- // Use provided path, or detect from current directory, or use home
21
- this.projectPath = projectPath || process.cwd();
22
- this.dbPath = this.getDbPath();
18
+ constructor(db, dbPath) {
19
+ this.db = db;
20
+ this.dbPath = dbPath;
21
+ }
22
+ /**
23
+ * Create a new MemoryDatabase instance (async factory)
24
+ */
25
+ static async create(projectPath) {
26
+ const dbPath = MemoryDatabase.getDbPath(projectPath || process.cwd());
23
27
  // Ensure directory exists
24
- const dbDir = dirname(this.dbPath);
28
+ const dbDir = dirname(dbPath);
25
29
  if (!existsSync(dbDir)) {
26
30
  mkdirSync(dbDir, { recursive: true });
27
31
  }
28
- this.db = this.openDatabase();
29
- this.initSchema();
30
- }
31
- openDatabase() {
32
- // Always try to open in read-write mode
33
- // If file doesn't exist, it will be created
34
- try {
35
- return new Database(this.dbPath, { fileMustExist: false });
32
+ // Initialize sql.js
33
+ const SQL = await initSqlJs();
34
+ // Load existing database or create new one
35
+ let db;
36
+ if (existsSync(dbPath)) {
37
+ try {
38
+ const fileBuffer = readFileSync(dbPath);
39
+ db = new SQL.Database(fileBuffer);
40
+ console.error(`[Pensieve] Loaded existing database: ${dbPath}`);
41
+ }
42
+ catch (error) {
43
+ console.error(`[Pensieve] Failed to load database, creating new: ${error}`);
44
+ db = new SQL.Database();
45
+ }
36
46
  }
37
- catch (error) {
38
- console.error(`[Pensieve] Failed to open database: ${error}`);
39
- throw error;
47
+ else {
48
+ db = new SQL.Database();
49
+ console.error(`[Pensieve] Created new database: ${dbPath}`);
40
50
  }
51
+ const instance = new MemoryDatabase(db, dbPath);
52
+ instance.initSchema();
53
+ instance.save(); // Ensure schema is persisted
54
+ return instance;
41
55
  }
42
56
  /**
43
- * Check if the database is writable and reconnect if needed
57
+ * Save database to disk
44
58
  */
45
- ensureWritable() {
59
+ save() {
46
60
  try {
47
- // Test write capability
48
- this.db.exec('SELECT 1');
49
- this.db.prepare('CREATE TABLE IF NOT EXISTS _pensieve_health_check (id INTEGER)').run();
50
- return true;
61
+ const data = this.db.export();
62
+ const buffer = Buffer.from(data);
63
+ writeFileSync(this.dbPath, buffer);
51
64
  }
52
65
  catch (error) {
53
- const errorMessage = error instanceof Error ? error.message : String(error);
54
- if (errorMessage.includes('readonly')) {
55
- console.error('[Pensieve] Database is read-only, attempting reconnection...');
56
- try {
57
- this.db.close();
58
- this.db = this.openDatabase();
59
- console.error('[Pensieve] Reconnected successfully');
60
- return true;
61
- }
62
- catch (reconnectError) {
63
- console.error(`[Pensieve] Reconnection failed: ${reconnectError}`);
64
- return false;
65
- }
66
- }
67
- return false;
66
+ console.error(`[Pensieve] Failed to save database: ${error}`);
68
67
  }
69
68
  }
70
- getDbPath() {
69
+ static getDbPath(projectPath) {
71
70
  // Check for explicit database path override
72
71
  const envPath = process.env.PENSIEVE_DB_PATH;
73
72
  if (envPath) {
@@ -75,20 +74,18 @@ export class MemoryDatabase {
75
74
  return envPath;
76
75
  }
77
76
  // Check for explicit project path (recommended for MCP server usage)
78
- // This should be set in the MCP server config to ensure deterministic behavior
79
77
  const projectDir = process.env.PENSIEVE_PROJECT_DIR;
80
78
  if (projectDir) {
81
- const projectPath = join(projectDir, '.pensieve', 'memory.sqlite');
82
- console.error(`[Pensieve] Using project database from PENSIEVE_PROJECT_DIR: ${projectPath}`);
83
- return projectPath;
79
+ const projectDbPath = join(projectDir, '.pensieve', 'memory.sqlite');
80
+ console.error(`[Pensieve] Using project database from PENSIEVE_PROJECT_DIR: ${projectDbPath}`);
81
+ return projectDbPath;
84
82
  }
85
83
  // Fallback: Try project-local first, then fall back to home directory
86
- // WARNING: Using process.cwd() is unreliable in MCP server context
87
- const localPath = join(this.projectPath, '.pensieve', 'memory.sqlite');
84
+ const localPath = join(projectPath, '.pensieve', 'memory.sqlite');
88
85
  const globalPath = join(homedir(), '.claude-pensieve', 'memory.sqlite');
89
86
  // If local .pensieve directory exists or we're in a git repo, use local
90
- if (existsSync(join(this.projectPath, '.pensieve')) ||
91
- existsSync(join(this.projectPath, '.git'))) {
87
+ if (existsSync(join(projectPath, '.pensieve')) ||
88
+ existsSync(join(projectPath, '.git'))) {
92
89
  console.error(`[Pensieve] WARNING: Using cwd-based path (unreliable): ${localPath}`);
93
90
  console.error(`[Pensieve] Set PENSIEVE_PROJECT_DIR for deterministic behavior`);
94
91
  return localPath;
@@ -112,42 +109,44 @@ export class MemoryDatabase {
112
109
  */
113
110
  pruneIfNeeded() {
114
111
  // Prune old sessions beyond retention period
115
- this.db.prepare(`
112
+ this.db.run(`
116
113
  DELETE FROM sessions
117
114
  WHERE ended_at IS NOT NULL
118
115
  AND datetime(ended_at) < datetime('now', '-${LIMITS.SESSION_RETENTION_DAYS} days')
119
- `).run();
116
+ `);
120
117
  // Prune excess decisions (keep most recent)
121
- const decisionCount = this.db.prepare('SELECT COUNT(*) as count FROM decisions').get().count;
118
+ const decisionResult = this.db.exec('SELECT COUNT(*) as count FROM decisions');
119
+ const decisionCount = decisionResult.length > 0 ? decisionResult[0].values[0][0] : 0;
122
120
  if (decisionCount > LIMITS.MAX_DECISIONS) {
123
121
  const excess = decisionCount - LIMITS.MAX_DECISIONS;
124
- this.db.prepare(`
122
+ this.db.run(`
125
123
  DELETE FROM decisions WHERE id IN (
126
124
  SELECT id FROM decisions ORDER BY decided_at ASC LIMIT ?
127
125
  )
128
- `).run(excess);
126
+ `, [excess]);
129
127
  console.error(`[Pensieve] Pruned ${excess} old decisions`);
130
128
  }
131
129
  // Prune excess discoveries
132
- const discoveryCount = this.db.prepare('SELECT COUNT(*) as count FROM discoveries').get().count;
130
+ const discoveryResult = this.db.exec('SELECT COUNT(*) as count FROM discoveries');
131
+ const discoveryCount = discoveryResult.length > 0 ? discoveryResult[0].values[0][0] : 0;
133
132
  if (discoveryCount > LIMITS.MAX_DISCOVERIES) {
134
133
  const excess = discoveryCount - LIMITS.MAX_DISCOVERIES;
135
- this.db.prepare(`
134
+ this.db.run(`
136
135
  DELETE FROM discoveries WHERE id IN (
137
136
  SELECT id FROM discoveries ORDER BY discovered_at ASC LIMIT ?
138
137
  )
139
- `).run(excess);
138
+ `, [excess]);
140
139
  console.error(`[Pensieve] Pruned ${excess} old discoveries`);
141
140
  }
142
141
  // Prune resolved questions older than 30 days
143
- this.db.prepare(`
142
+ this.db.run(`
144
143
  DELETE FROM open_questions
145
144
  WHERE status = 'resolved'
146
145
  AND datetime(resolved_at) < datetime('now', '-30 days')
147
- `).run();
146
+ `);
148
147
  }
149
148
  initSchema() {
150
- this.db.exec(`
149
+ this.db.run(`
151
150
  -- Core discoveries about the codebase
152
151
  CREATE TABLE IF NOT EXISTS discoveries (
153
152
  id INTEGER PRIMARY KEY,
@@ -158,9 +157,9 @@ export class MemoryDatabase {
158
157
  metadata TEXT,
159
158
  discovered_at TEXT DEFAULT (datetime('now')),
160
159
  confidence REAL DEFAULT 1.0
161
- );
162
-
163
- -- Architectural and design decisions
160
+ )
161
+ `);
162
+ this.db.run(`
164
163
  CREATE TABLE IF NOT EXISTS decisions (
165
164
  id INTEGER PRIMARY KEY,
166
165
  topic TEXT NOT NULL,
@@ -169,9 +168,9 @@ export class MemoryDatabase {
169
168
  alternatives TEXT,
170
169
  decided_at TEXT DEFAULT (datetime('now')),
171
170
  source TEXT
172
- );
173
-
174
- -- User preferences and conventions
171
+ )
172
+ `);
173
+ this.db.run(`
175
174
  CREATE TABLE IF NOT EXISTS preferences (
176
175
  id INTEGER PRIMARY KEY,
177
176
  category TEXT NOT NULL,
@@ -180,9 +179,9 @@ export class MemoryDatabase {
180
179
  notes TEXT,
181
180
  updated_at TEXT DEFAULT (datetime('now')),
182
181
  UNIQUE(category, key)
183
- );
184
-
185
- -- Session summaries for continuity
182
+ )
183
+ `);
184
+ this.db.run(`
186
185
  CREATE TABLE IF NOT EXISTS sessions (
187
186
  id INTEGER PRIMARY KEY,
188
187
  started_at TEXT DEFAULT (datetime('now')),
@@ -192,9 +191,9 @@ export class MemoryDatabase {
192
191
  next_steps TEXT,
193
192
  key_files TEXT,
194
193
  tags TEXT
195
- );
196
-
197
- -- Entities/domain model understanding
194
+ )
195
+ `);
196
+ this.db.run(`
198
197
  CREATE TABLE IF NOT EXISTS entities (
199
198
  id INTEGER PRIMARY KEY,
200
199
  name TEXT NOT NULL UNIQUE,
@@ -203,9 +202,9 @@ export class MemoryDatabase {
203
202
  attributes TEXT,
204
203
  location TEXT,
205
204
  updated_at TEXT DEFAULT (datetime('now'))
206
- );
207
-
208
- -- Open questions and blockers
205
+ )
206
+ `);
207
+ this.db.run(`
209
208
  CREATE TABLE IF NOT EXISTS open_questions (
210
209
  id INTEGER PRIMARY KEY,
211
210
  question TEXT NOT NULL,
@@ -214,128 +213,166 @@ export class MemoryDatabase {
214
213
  resolution TEXT,
215
214
  created_at TEXT DEFAULT (datetime('now')),
216
215
  resolved_at TEXT
217
- );
218
-
219
- -- Indexes for common queries
220
- CREATE INDEX IF NOT EXISTS idx_discoveries_category ON discoveries(category);
221
- CREATE INDEX IF NOT EXISTS idx_discoveries_name ON discoveries(name);
222
- CREATE INDEX IF NOT EXISTS idx_decisions_topic ON decisions(topic);
223
- CREATE INDEX IF NOT EXISTS idx_preferences_category ON preferences(category);
224
- CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at);
225
- CREATE INDEX IF NOT EXISTS idx_open_questions_status ON open_questions(status);
216
+ )
226
217
  `);
218
+ // Create indexes
219
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_discoveries_category ON discoveries(category)');
220
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_discoveries_name ON discoveries(name)');
221
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_decisions_topic ON decisions(topic)');
222
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_preferences_category ON preferences(category)');
223
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at)');
224
+ this.db.run('CREATE INDEX IF NOT EXISTS idx_open_questions_status ON open_questions(status)');
225
+ }
226
+ /**
227
+ * Get last inserted row ID
228
+ */
229
+ getLastInsertRowId() {
230
+ const result = this.db.exec('SELECT last_insert_rowid()');
231
+ return result.length > 0 ? result[0].values[0][0] : 0;
232
+ }
233
+ /**
234
+ * Execute a query and return all rows as objects
235
+ */
236
+ queryAll(sql, params = []) {
237
+ const stmt = this.db.prepare(sql);
238
+ stmt.bind(params);
239
+ const results = [];
240
+ while (stmt.step()) {
241
+ results.push(stmt.getAsObject());
242
+ }
243
+ stmt.free();
244
+ return results;
245
+ }
246
+ /**
247
+ * Execute a query and return the first row as an object
248
+ */
249
+ queryOne(sql, params = []) {
250
+ const results = this.queryAll(sql, params);
251
+ return results.length > 0 ? results[0] : undefined;
227
252
  }
228
253
  // Decision methods
229
254
  addDecision(decision) {
230
- this.ensureWritable();
231
255
  this.pruneIfNeeded();
232
- const stmt = this.db.prepare(`
256
+ this.db.run(`
233
257
  INSERT INTO decisions (topic, decision, rationale, alternatives, source)
234
258
  VALUES (?, ?, ?, ?, ?)
235
- `);
236
- const result = stmt.run(this.truncateField(decision.topic, 'topic'), this.truncateField(decision.decision, 'decision'), this.truncateField(decision.rationale, 'rationale'), this.truncateField(decision.alternatives, 'alternatives'), decision.source || 'user');
237
- return result.lastInsertRowid;
259
+ `, [
260
+ this.truncateField(decision.topic, 'topic'),
261
+ this.truncateField(decision.decision, 'decision'),
262
+ this.truncateField(decision.rationale, 'rationale'),
263
+ this.truncateField(decision.alternatives, 'alternatives'),
264
+ decision.source || 'user'
265
+ ]);
266
+ const id = this.getLastInsertRowId();
267
+ this.save();
268
+ return id;
238
269
  }
239
270
  searchDecisions(query) {
240
- const stmt = this.db.prepare(`
271
+ const pattern = `%${query}%`;
272
+ return this.queryAll(`
241
273
  SELECT * FROM decisions
242
274
  WHERE topic LIKE ? OR decision LIKE ? OR rationale LIKE ?
243
275
  ORDER BY decided_at DESC
244
276
  LIMIT 50
245
- `);
246
- const pattern = `%${query}%`;
247
- return stmt.all(pattern, pattern, pattern);
277
+ `, [pattern, pattern, pattern]);
248
278
  }
249
279
  getRecentDecisions(limit = 10) {
250
- const stmt = this.db.prepare(`
280
+ return this.queryAll(`
251
281
  SELECT * FROM decisions
252
282
  ORDER BY decided_at DESC
253
283
  LIMIT ?
254
- `);
255
- return stmt.all(limit);
284
+ `, [limit]);
256
285
  }
257
286
  // Preference methods
258
287
  setPreference(pref) {
259
- this.ensureWritable();
260
- const stmt = this.db.prepare(`
288
+ this.db.run(`
261
289
  INSERT OR REPLACE INTO preferences (category, key, value, notes, updated_at)
262
290
  VALUES (?, ?, ?, ?, datetime('now'))
263
- `);
264
- stmt.run(this.truncateField(pref.category, 'category'), this.truncateField(pref.key, 'key'), this.truncateField(pref.value, 'value'), this.truncateField(pref.notes, 'notes'));
291
+ `, [
292
+ this.truncateField(pref.category, 'category'),
293
+ this.truncateField(pref.key, 'key'),
294
+ this.truncateField(pref.value, 'value'),
295
+ this.truncateField(pref.notes, 'notes')
296
+ ]);
297
+ this.save();
265
298
  }
266
299
  getPreference(category, key) {
267
- const stmt = this.db.prepare(`
300
+ return this.queryOne(`
268
301
  SELECT * FROM preferences WHERE category = ? AND key = ?
269
- `);
270
- return stmt.get(category, key);
302
+ `, [category, key]);
271
303
  }
272
304
  getPreferencesByCategory(category) {
273
- const stmt = this.db.prepare(`
305
+ return this.queryAll(`
274
306
  SELECT * FROM preferences WHERE category = ? ORDER BY key
275
- `);
276
- return stmt.all(category);
307
+ `, [category]);
277
308
  }
278
309
  getAllPreferences() {
279
- const stmt = this.db.prepare(`
310
+ return this.queryAll(`
280
311
  SELECT * FROM preferences ORDER BY category, key
281
312
  `);
282
- return stmt.all();
283
313
  }
284
314
  // Discovery methods
285
315
  addDiscovery(discovery) {
286
- this.ensureWritable();
287
316
  this.pruneIfNeeded();
288
- const stmt = this.db.prepare(`
317
+ this.db.run(`
289
318
  INSERT INTO discoveries (category, name, location, description, metadata, confidence)
290
319
  VALUES (?, ?, ?, ?, ?, ?)
291
- `);
292
- const result = stmt.run(this.truncateField(discovery.category, 'category'), this.truncateField(discovery.name, 'name'), this.truncateField(discovery.location, 'location'), this.truncateField(discovery.description, 'description'), this.truncateField(discovery.metadata, 'metadata'), discovery.confidence || 1.0);
293
- return result.lastInsertRowid;
320
+ `, [
321
+ this.truncateField(discovery.category, 'category'),
322
+ this.truncateField(discovery.name, 'name'),
323
+ this.truncateField(discovery.location, 'location'),
324
+ this.truncateField(discovery.description, 'description'),
325
+ this.truncateField(discovery.metadata, 'metadata'),
326
+ discovery.confidence || 1.0
327
+ ]);
328
+ const id = this.getLastInsertRowId();
329
+ this.save();
330
+ return id;
294
331
  }
295
332
  searchDiscoveries(query) {
296
- const stmt = this.db.prepare(`
333
+ const pattern = `%${query}%`;
334
+ return this.queryAll(`
297
335
  SELECT * FROM discoveries
298
336
  WHERE name LIKE ? OR description LIKE ? OR location LIKE ?
299
337
  ORDER BY discovered_at DESC
300
338
  LIMIT 50
301
- `);
302
- const pattern = `%${query}%`;
303
- return stmt.all(pattern, pattern, pattern);
339
+ `, [pattern, pattern, pattern]);
304
340
  }
305
341
  getDiscoveriesByCategory(category) {
306
- const stmt = this.db.prepare(`
342
+ return this.queryAll(`
307
343
  SELECT * FROM discoveries WHERE category = ? ORDER BY name
308
- `);
309
- return stmt.all(category);
344
+ `, [category]);
310
345
  }
311
346
  // Entity methods
312
347
  upsertEntity(entity) {
313
- this.ensureWritable();
314
- const stmt = this.db.prepare(`
348
+ this.db.run(`
315
349
  INSERT OR REPLACE INTO entities (name, description, relationships, attributes, location, updated_at)
316
350
  VALUES (?, ?, ?, ?, ?, datetime('now'))
317
- `);
318
- stmt.run(this.truncateField(entity.name, 'name'), this.truncateField(entity.description, 'description'), this.truncateField(entity.relationships, 'relationships'), this.truncateField(entity.attributes, 'attributes'), this.truncateField(entity.location, 'location'));
351
+ `, [
352
+ this.truncateField(entity.name, 'name'),
353
+ this.truncateField(entity.description, 'description'),
354
+ this.truncateField(entity.relationships, 'relationships'),
355
+ this.truncateField(entity.attributes, 'attributes'),
356
+ this.truncateField(entity.location, 'location')
357
+ ]);
358
+ this.save();
319
359
  }
320
360
  getEntity(name) {
321
- const stmt = this.db.prepare(`SELECT * FROM entities WHERE name = ?`);
322
- return stmt.get(name);
361
+ return this.queryOne(`SELECT * FROM entities WHERE name = ?`, [name]);
323
362
  }
324
363
  getAllEntities() {
325
- const stmt = this.db.prepare(`SELECT * FROM entities ORDER BY name`);
326
- return stmt.all();
364
+ return this.queryAll(`SELECT * FROM entities ORDER BY name`);
327
365
  }
328
366
  // Session methods
329
367
  startSession() {
330
- this.ensureWritable();
331
- const stmt = this.db.prepare(`INSERT INTO sessions (started_at) VALUES (datetime('now'))`);
332
- const result = stmt.run();
333
- return result.lastInsertRowid;
368
+ this.db.run(`INSERT INTO sessions (started_at) VALUES (datetime('now'))`);
369
+ const id = this.getLastInsertRowId();
370
+ this.save();
371
+ return id;
334
372
  }
335
373
  endSession(sessionId, summary, workInProgress, nextSteps, keyFiles, tags) {
336
- this.ensureWritable();
337
374
  this.pruneIfNeeded();
338
- const stmt = this.db.prepare(`
375
+ this.db.run(`
339
376
  UPDATE sessions
340
377
  SET ended_at = datetime('now'),
341
378
  summary = ?,
@@ -344,44 +381,50 @@ export class MemoryDatabase {
344
381
  key_files = ?,
345
382
  tags = ?
346
383
  WHERE id = ?
347
- `);
348
- stmt.run(this.truncateField(summary, 'summary'), this.truncateField(workInProgress, 'work_in_progress'), this.truncateField(nextSteps, 'next_steps'), keyFiles ? this.truncateField(JSON.stringify(keyFiles), 'key_files') : null, tags ? tags.join(',') : null, sessionId);
384
+ `, [
385
+ this.truncateField(summary, 'summary'),
386
+ this.truncateField(workInProgress, 'work_in_progress'),
387
+ this.truncateField(nextSteps, 'next_steps'),
388
+ keyFiles ? this.truncateField(JSON.stringify(keyFiles), 'key_files') : null,
389
+ tags ? tags.join(',') : null,
390
+ sessionId
391
+ ]);
392
+ this.save();
349
393
  }
350
394
  getLastSession() {
351
- const stmt = this.db.prepare(`
395
+ return this.queryOne(`
352
396
  SELECT * FROM sessions ORDER BY started_at DESC LIMIT 1
353
397
  `);
354
- return stmt.get();
355
398
  }
356
399
  getCurrentSession() {
357
- const stmt = this.db.prepare(`
400
+ return this.queryOne(`
358
401
  SELECT * FROM sessions WHERE ended_at IS NULL ORDER BY started_at DESC LIMIT 1
359
402
  `);
360
- return stmt.get();
361
403
  }
362
404
  // Open questions methods
363
405
  addQuestion(question, context) {
364
- this.ensureWritable();
365
- const stmt = this.db.prepare(`
406
+ this.db.run(`
366
407
  INSERT INTO open_questions (question, context) VALUES (?, ?)
367
- `);
368
- const result = stmt.run(this.truncateField(question, 'question'), this.truncateField(context, 'context'));
369
- return result.lastInsertRowid;
408
+ `, [
409
+ this.truncateField(question, 'question'),
410
+ this.truncateField(context, 'context')
411
+ ]);
412
+ const id = this.getLastInsertRowId();
413
+ this.save();
414
+ return id;
370
415
  }
371
416
  resolveQuestion(id, resolution) {
372
- this.ensureWritable();
373
- const stmt = this.db.prepare(`
417
+ this.db.run(`
374
418
  UPDATE open_questions
375
419
  SET status = 'resolved', resolution = ?, resolved_at = datetime('now')
376
420
  WHERE id = ?
377
- `);
378
- stmt.run(resolution, id);
421
+ `, [resolution, id]);
422
+ this.save();
379
423
  }
380
424
  getOpenQuestions() {
381
- const stmt = this.db.prepare(`
425
+ return this.queryAll(`
382
426
  SELECT * FROM open_questions WHERE status = 'open' ORDER BY created_at DESC
383
427
  `);
384
- return stmt.all();
385
428
  }
386
429
  // General search
387
430
  search(query) {
@@ -397,6 +440,7 @@ export class MemoryDatabase {
397
440
  return this.dbPath;
398
441
  }
399
442
  close() {
443
+ this.save();
400
444
  this.db.close();
401
445
  }
402
446
  }
package/dist/index.js CHANGED
@@ -4,8 +4,12 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { MemoryDatabase } from './database.js';
6
6
  import { checkFieldsForSecrets, formatSecretWarning } from './security.js';
7
- // Initialize database
8
- const db = new MemoryDatabase();
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname, join } from 'path';
9
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync } from 'fs';
10
+ import { homedir } from 'os';
11
+ // Database instance (initialized async in main)
12
+ let db;
9
13
  // Create MCP server
10
14
  const server = new Server({
11
15
  name: 'pensieve',
@@ -515,6 +519,57 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
515
519
  };
516
520
  }
517
521
  });
522
+ // Install slash commands to user's ~/.claude/commands/ directory
523
+ function installCommands() {
524
+ try {
525
+ // Find the package's .claude/commands directory
526
+ const __filename = fileURLToPath(import.meta.url);
527
+ const __dirname = dirname(__filename);
528
+ // Go up from dist/ to package root, then into .claude/commands
529
+ const packageCommandsDir = join(__dirname, '..', '.claude', 'commands');
530
+ if (!existsSync(packageCommandsDir)) {
531
+ // Commands directory not found in package - this is fine for dev mode
532
+ return;
533
+ }
534
+ // Target directory
535
+ const userCommandsDir = join(homedir(), '.claude', 'commands');
536
+ // Create target directory if it doesn't exist
537
+ if (!existsSync(userCommandsDir)) {
538
+ mkdirSync(userCommandsDir, { recursive: true });
539
+ }
540
+ // Get all command files from package
541
+ const commandFiles = readdirSync(packageCommandsDir).filter(f => f.endsWith('.md'));
542
+ let installed = 0;
543
+ let updated = 0;
544
+ for (const file of commandFiles) {
545
+ const sourcePath = join(packageCommandsDir, file);
546
+ const targetPath = join(userCommandsDir, file);
547
+ // Read source content
548
+ const sourceContent = readFileSync(sourcePath, 'utf-8');
549
+ // Check if target exists and compare content
550
+ if (existsSync(targetPath)) {
551
+ const targetContent = readFileSync(targetPath, 'utf-8');
552
+ if (sourceContent !== targetContent) {
553
+ // Update if different
554
+ copyFileSync(sourcePath, targetPath);
555
+ updated++;
556
+ }
557
+ }
558
+ else {
559
+ // Install if missing
560
+ copyFileSync(sourcePath, targetPath);
561
+ installed++;
562
+ }
563
+ }
564
+ if (installed > 0 || updated > 0) {
565
+ console.error(`[Pensieve] Commands: ${installed} installed, ${updated} updated in ~/.claude/commands/`);
566
+ }
567
+ }
568
+ catch (error) {
569
+ // Non-fatal - just log and continue
570
+ console.error(`[Pensieve] Warning: Could not install commands: ${error instanceof Error ? error.message : String(error)}`);
571
+ }
572
+ }
518
573
  // Output prior context on startup
519
574
  function outputPriorContext() {
520
575
  const lastSession = db.getLastSession();
@@ -574,6 +629,10 @@ function outputPriorContext() {
574
629
  }
575
630
  // Start server
576
631
  async function main() {
632
+ // Initialize database (async for sql.js WASM loading)
633
+ db = await MemoryDatabase.create();
634
+ // Install slash commands to ~/.claude/commands/
635
+ installCommands();
577
636
  const transport = new StdioServerTransport();
578
637
  await server.connect(transport);
579
638
  // Output prior context so Claude sees it automatically
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esparkman/pensieve",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Pensieve - persistent memory for Claude Code. Remember decisions, preferences, and context across sessions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,10 +27,10 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@modelcontextprotocol/sdk": "^1.25.1",
30
- "better-sqlite3": "^12.5.0"
30
+ "sql.js": "^1.12.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@types/better-sqlite3": "^7.6.13",
33
+ "@types/sql.js": "^1.4.9",
34
34
  "@types/node": "^25.0.3",
35
35
  "tsx": "^4.21.0",
36
36
  "typescript": "^5.9.3",