@sriinnu/harmon-store 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @sriinnu/harmon-store
2
+
3
+ ![logo](./logo.svg)
4
+
5
+ > SQLite persistence layer for sessions, journal entries, events, and settings.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @sriinnu/harmon-store
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { createStore } from '@sriinnu/harmon-store';
17
+
18
+ const store = await createStore({ dbPath: '.harmon.db' });
19
+ const sessionId = await store.createSession(JSON.stringify(policy));
20
+ await store.logEvent('track.started', { trackId: '123' }, sessionId);
21
+ await store.endSession(sessionId);
22
+ ```
23
+
24
+ ## API
25
+
26
+ | Export | Description |
27
+ |---|---|
28
+ | `createStore(config?)` | Create store and run migrations |
29
+ | `HarmonStore` | Store class with full CRUD |
30
+ | `store.createSession(policy)` | Start a new session |
31
+ | `store.endSession(id)` | Mark session completed |
32
+ | `store.logEvent(type, payload, sessionId?)` | Append to event log |
33
+ | `store.addJournalEntry(entry)` | Insert a mood journal entry |
34
+ | `store.getJournalEntriesByMood(mood)` | Query entries by mood tag |
35
+ | `store.getSetting(key)` / `setSetting(key, value)` | Key-value settings |
36
+ | `store.getStats()` | Aggregate counts and mood distribution |
37
+
38
+ ## Architecture
39
+
40
+ harmon-store sits beneath the daemon, providing durable storage via libSQL/SQLite. It manages automatic migrations, journal entries with mood tags, session lifecycle, and a typed event log. The daemon reads and writes through this layer exclusively.
41
+
42
+ ## License
43
+
44
+ GNU Affero General Public License v3.0 only. See [LICENSE](../../LICENSE).
package/SKILL.md ADDED
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: harmon-store
3
+ description: SQLite persistence layer with versioned migrations for sessions, journals, and events
4
+ capabilities:
5
+ - Store and query journal entries with mood tags, energy levels, and embeddings
6
+ - Manage session lifecycle (create, end, cancel) with status tracking
7
+ - Log and retrieve events with session correlation and time-based queries
8
+ tags:
9
+ - database
10
+ - sqlite
11
+ - persistence
12
+ - storage
13
+ provider: harmon
14
+ version: 0.1.0
15
+ ---
16
+
17
+ # Harmon Store
18
+
19
+ ## What this does
20
+ harmon-store provides the persistence layer for the entire harmon system. It manages a SQLite database (via libsql) with automatic versioned migrations, storing journal entries, sessions, event logs, and key-value settings. The store enforces WAL mode for concurrent read performance and validates that encryption is enabled in production environments.
21
+
22
+ ## When to use
23
+ - Persisting session history, event logs, or journal entries to disk
24
+ - Querying past sessions, mood distributions, or recent play statistics
25
+ - Adding a new migration when the schema needs to evolve
26
+
27
+ ## Key exports
28
+ - `HarmonStore` — class with methods for journals, sessions, events, settings, and stats
29
+ - `createStore` — async factory that instantiates HarmonStore and runs pending migrations
30
+ - `JournalEntry` — interface for a parsed journal record (mood, energy, content, embedding)
31
+ - `Session` — interface for a session record (id, policy, status, timestamps)
32
+
33
+ ## Example
34
+ ```typescript
35
+ import { createStore } from '@sriinnu/harmon-store';
36
+
37
+ const store = await createStore({ dbPath: '.harmon.db' });
38
+ const sessionId = await store.createSession(JSON.stringify(policy));
39
+ await store.logEvent('session.started', { sessionId });
40
+ const stats = await store.getStats();
41
+ ```
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Harmon Store - SQLite persistence layer with migrations
3
+ */
4
+ import { type Client } from '@libsql/client';
5
+ export interface Database {
6
+ client: Client;
7
+ }
8
+ export interface JournalEntry {
9
+ id: string;
10
+ filename: string;
11
+ timestamp: string;
12
+ source: string;
13
+ device: string;
14
+ sessionId?: string;
15
+ moodTags: string;
16
+ energyLevel?: string;
17
+ context?: string;
18
+ content: string;
19
+ policy?: string;
20
+ embedding?: number[];
21
+ createdAt: string;
22
+ }
23
+ export interface Session {
24
+ id: string;
25
+ policy: string;
26
+ startedAt: string;
27
+ endedAt?: string;
28
+ status: 'active' | 'completed' | 'cancelled';
29
+ }
30
+ export interface EventLog {
31
+ id: string;
32
+ sessionId?: string;
33
+ type: string;
34
+ payload: string;
35
+ createdAt: string;
36
+ }
37
+ export interface HarmonStoreConfig {
38
+ dbPath?: string;
39
+ memory?: boolean;
40
+ }
41
+ export declare class HarmonStore {
42
+ private client;
43
+ private dbPath;
44
+ private memory;
45
+ constructor(config?: HarmonStoreConfig);
46
+ /**
47
+ * Run migrations
48
+ */
49
+ migrate(): Promise<void>;
50
+ /**
51
+ * Close the database
52
+ */
53
+ close(): Promise<void>;
54
+ /**
55
+ * I create the SQLite files myself so the daemon never relies on process
56
+ * umask to keep journal data owner-only.
57
+ */
58
+ private ensureDatabaseFiles;
59
+ /**
60
+ * I keep the main DB and SQLite sidecars private to the current user.
61
+ */
62
+ private enforceDatabaseFilePermissions;
63
+ addJournalEntry(entry: Omit<JournalEntry, 'id' | 'createdAt'>): Promise<string>;
64
+ getJournalEntry(id: string): Promise<JournalEntry | null>;
65
+ getJournalEntries(limit?: number, offset?: number): Promise<JournalEntry[]>;
66
+ getJournalEntriesByMood(mood: string, limit?: number): Promise<JournalEntry[]>;
67
+ getRecentJournalEntries(days?: number, limit?: number): Promise<JournalEntry[]>;
68
+ private rowToJournalEntry;
69
+ createSession(policy: string): Promise<string>;
70
+ endSession(id: string): Promise<void>;
71
+ cancelSession(id: string): Promise<void>;
72
+ getSession(id: string): Promise<Session | null>;
73
+ getActiveSession(): Promise<Session | null>;
74
+ getRecentSessions(limit?: number): Promise<Session[]>;
75
+ private rowToSession;
76
+ logEvent(type: string, payload: Record<string, unknown>, sessionId?: string): Promise<string>;
77
+ getEvents(sessionId?: string, limit?: number): Promise<EventLog[]>;
78
+ getRecentEvents(limit?: number): Promise<EventLog[]>;
79
+ private rowToEventLog;
80
+ getSetting(key: string): Promise<string | null>;
81
+ setSetting(key: string, value: string): Promise<void>;
82
+ deleteSetting(key: string): Promise<void>;
83
+ getStats(): Promise<{
84
+ totalEntries: number;
85
+ totalSessions: number;
86
+ activeSessions: number;
87
+ eventsLogged: number;
88
+ recentMoodDistribution: Record<string, number>;
89
+ }>;
90
+ /**
91
+ * Get database path
92
+ */
93
+ getDbPath(): string;
94
+ /**
95
+ * Validate encryption is enabled in production
96
+ * This should be called after the store is initialized
97
+ */
98
+ static validateEncryptionInProduction(encryptionEnabled: boolean): void;
99
+ /**
100
+ * Check if encryption should be required based on environment
101
+ */
102
+ static isEncryptionRequired(): boolean;
103
+ }
104
+ /**
105
+ * Create a store with default configuration
106
+ */
107
+ export declare function createStore(config?: HarmonStoreConfig): Promise<HarmonStore>;
108
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,473 @@
1
+ /**
2
+ * Harmon Store - SQLite persistence layer with migrations
3
+ */
4
+ import { createClient } from '@libsql/client';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import path from 'node:path';
7
+ import fs from 'node:fs';
8
+ const MIGRATIONS = [
9
+ {
10
+ version: 1,
11
+ statements: [
12
+ `
13
+ CREATE TABLE IF NOT EXISTS journal_entries (
14
+ id TEXT PRIMARY KEY,
15
+ filename TEXT NOT NULL,
16
+ timestamp TEXT NOT NULL,
17
+ source TEXT NOT NULL,
18
+ device TEXT NOT NULL,
19
+ sessionId TEXT,
20
+ moodTags TEXT,
21
+ energyLevel TEXT,
22
+ context TEXT,
23
+ content TEXT NOT NULL,
24
+ policy TEXT,
25
+ embedding BLOB,
26
+ createdAt TEXT DEFAULT (datetime('now'))
27
+ )
28
+ `,
29
+ 'CREATE INDEX IF NOT EXISTS idx_journal_timestamp ON journal_entries(timestamp)',
30
+ 'CREATE INDEX IF NOT EXISTS idx_journal_moodTags ON journal_entries(moodTags)',
31
+ 'CREATE INDEX IF NOT EXISTS idx_journal_sessionId ON journal_entries(sessionId)',
32
+ `
33
+ CREATE TABLE IF NOT EXISTS sessions (
34
+ id TEXT PRIMARY KEY,
35
+ policy TEXT NOT NULL,
36
+ startedAt TEXT NOT NULL,
37
+ endedAt TEXT,
38
+ status TEXT NOT NULL DEFAULT 'active',
39
+ createdAt TEXT DEFAULT (datetime('now'))
40
+ )
41
+ `,
42
+ 'CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status)',
43
+ 'CREATE INDEX IF NOT EXISTS idx_sessions_startedAt ON sessions(startedAt)',
44
+ `
45
+ CREATE TABLE IF NOT EXISTS event_log (
46
+ id TEXT PRIMARY KEY,
47
+ sessionId TEXT,
48
+ type TEXT NOT NULL,
49
+ payload TEXT NOT NULL,
50
+ createdAt TEXT DEFAULT (datetime('now'))
51
+ )
52
+ `,
53
+ 'CREATE INDEX IF NOT EXISTS idx_event_log_sessionId ON event_log(sessionId)',
54
+ 'CREATE INDEX IF NOT EXISTS idx_event_log_type ON event_log(type)',
55
+ 'CREATE INDEX IF NOT EXISTS idx_event_log_createdAt ON event_log(createdAt)',
56
+ ],
57
+ },
58
+ {
59
+ version: 2,
60
+ statements: [`
61
+ CREATE TABLE IF NOT EXISTS settings (
62
+ key TEXT PRIMARY KEY,
63
+ value TEXT NOT NULL,
64
+ updatedAt TEXT DEFAULT (datetime('now'))
65
+ )
66
+ `],
67
+ },
68
+ {
69
+ version: 3,
70
+ statements: ['PRAGMA journal_mode=WAL'],
71
+ },
72
+ ];
73
+ export class HarmonStore {
74
+ client;
75
+ dbPath;
76
+ memory;
77
+ constructor(config = {}) {
78
+ const dbPath = config.dbPath || '.harmon.db';
79
+ this.dbPath = path.resolve(dbPath);
80
+ this.memory = config.memory === true;
81
+ // Ensure directory exists
82
+ const dir = path.dirname(this.dbPath);
83
+ if (!fs.existsSync(dir)) {
84
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
85
+ }
86
+ if (!this.memory) {
87
+ this.ensureDatabaseFiles();
88
+ }
89
+ const url = this.memory ? 'file::memory:' : `file:${this.dbPath}`;
90
+ this.client = createClient({
91
+ url,
92
+ });
93
+ }
94
+ /**
95
+ * Run migrations
96
+ */
97
+ async migrate() {
98
+ // Ensure migration tracking table exists
99
+ await this.client.execute(`
100
+ CREATE TABLE IF NOT EXISTS _migrations (
101
+ version INTEGER PRIMARY KEY,
102
+ appliedAt TEXT DEFAULT (datetime('now'))
103
+ )
104
+ `);
105
+ // Get current version
106
+ const result = await this.client.execute('SELECT MAX(version) as v FROM _migrations');
107
+ const currentVersion = result.rows[0]?.v || 0;
108
+ // Run pending migrations (each wrapped in a transaction for atomicity).
109
+ // PRAGMA statements cannot execute inside a transaction, so they are
110
+ // run separately and the version is recorded in its own batch.
111
+ for (const migration of MIGRATIONS) {
112
+ if (migration.version > currentVersion) {
113
+ const pragmas = migration.statements.filter((s) => /^\s*PRAGMA\b/i.test(s));
114
+ const regular = migration.statements.filter((s) => !/^\s*PRAGMA\b/i.test(s));
115
+ // Execute PRAGMA statements outside a transaction
116
+ for (const pragma of pragmas) {
117
+ await this.client.execute(pragma.trim());
118
+ }
119
+ // Batch the remaining DDL/DML with the version bookmark
120
+ await this.client.batch([
121
+ ...regular.map((s) => s.trim()),
122
+ { sql: 'INSERT INTO _migrations (version) VALUES (?)', args: [migration.version] },
123
+ ], 'write');
124
+ }
125
+ }
126
+ this.enforceDatabaseFilePermissions();
127
+ }
128
+ /**
129
+ * Close the database
130
+ */
131
+ async close() {
132
+ this.client.close();
133
+ }
134
+ /**
135
+ * I create the SQLite files myself so the daemon never relies on process
136
+ * umask to keep journal data owner-only.
137
+ */
138
+ ensureDatabaseFiles() {
139
+ const handle = fs.openSync(this.dbPath, 'a', 0o600);
140
+ fs.closeSync(handle);
141
+ this.enforceDatabaseFilePermissions();
142
+ }
143
+ /**
144
+ * I keep the main DB and SQLite sidecars private to the current user.
145
+ */
146
+ enforceDatabaseFilePermissions() {
147
+ if (this.memory) {
148
+ return;
149
+ }
150
+ for (const filePath of [this.dbPath, `${this.dbPath}-shm`, `${this.dbPath}-wal`]) {
151
+ if (!fs.existsSync(filePath)) {
152
+ continue;
153
+ }
154
+ fs.chmodSync(filePath, 0o600);
155
+ }
156
+ }
157
+ // ============================================================================
158
+ // Journal Entries
159
+ // ============================================================================
160
+ async addJournalEntry(entry) {
161
+ const id = uuidv4();
162
+ const now = new Date().toISOString();
163
+ await this.client.execute({
164
+ sql: `
165
+ INSERT INTO journal_entries
166
+ (id, filename, timestamp, source, device, sessionId, moodTags, energyLevel, context, content, policy, embedding, createdAt)
167
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
168
+ `,
169
+ args: [
170
+ id,
171
+ entry.filename,
172
+ entry.timestamp,
173
+ entry.source,
174
+ entry.device,
175
+ entry.sessionId || null,
176
+ entry.moodTags,
177
+ entry.energyLevel || null,
178
+ entry.context || null,
179
+ entry.content,
180
+ entry.policy || null,
181
+ entry.embedding ? Buffer.from(JSON.stringify(entry.embedding)) : null,
182
+ now,
183
+ ],
184
+ });
185
+ return id;
186
+ }
187
+ async getJournalEntry(id) {
188
+ const result = await this.client.execute({
189
+ sql: 'SELECT * FROM journal_entries WHERE id = ?',
190
+ args: [id],
191
+ });
192
+ if (result.rows.length === 0)
193
+ return null;
194
+ return this.rowToJournalEntry(result.rows[0]);
195
+ }
196
+ async getJournalEntries(limit = 100, offset = 0) {
197
+ const result = await this.client.execute({
198
+ sql: 'SELECT * FROM journal_entries ORDER BY timestamp DESC LIMIT ? OFFSET ?',
199
+ args: [limit.toString(), offset.toString()],
200
+ });
201
+ return result.rows.map((row) => this.rowToJournalEntry(row));
202
+ }
203
+ async getJournalEntriesByMood(mood, limit = 50) {
204
+ const result = await this.client.execute({
205
+ sql: `
206
+ SELECT * FROM journal_entries
207
+ WHERE moodTags LIKE ?
208
+ ORDER BY timestamp DESC
209
+ LIMIT ?
210
+ `,
211
+ args: [`%${mood}%`, limit.toString()],
212
+ });
213
+ return result.rows.map((row) => this.rowToJournalEntry(row));
214
+ }
215
+ async getRecentJournalEntries(days = 7, limit = 100) {
216
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
217
+ const result = await this.client.execute({
218
+ sql: `
219
+ SELECT * FROM journal_entries
220
+ WHERE timestamp >= ?
221
+ ORDER BY timestamp DESC
222
+ LIMIT ?
223
+ `,
224
+ args: [cutoff, limit.toString()],
225
+ });
226
+ return result.rows.map((row) => this.rowToJournalEntry(row));
227
+ }
228
+ rowToJournalEntry(row) {
229
+ return {
230
+ id: row.id,
231
+ filename: row.filename,
232
+ timestamp: row.timestamp,
233
+ source: row.source,
234
+ device: row.device,
235
+ sessionId: row.sessionId,
236
+ moodTags: row.moodTags,
237
+ energyLevel: row.energyLevel,
238
+ context: row.context,
239
+ content: row.content,
240
+ policy: row.policy,
241
+ embedding: row.embedding ? JSON.parse(row.embedding.toString()) : undefined,
242
+ createdAt: row.createdAt,
243
+ };
244
+ }
245
+ // ============================================================================
246
+ // Sessions
247
+ // ============================================================================
248
+ async createSession(policy) {
249
+ const id = `sess_${uuidv4().slice(0, 8)}`;
250
+ const now = new Date().toISOString();
251
+ await this.client.execute({
252
+ sql: `
253
+ INSERT INTO sessions (id, policy, startedAt, status)
254
+ VALUES (?, ?, ?, 'active')
255
+ `,
256
+ args: [id, policy, now],
257
+ });
258
+ return id;
259
+ }
260
+ async endSession(id) {
261
+ const now = new Date().toISOString();
262
+ const result = await this.client.execute({
263
+ sql: `
264
+ UPDATE sessions
265
+ SET endedAt = ?, status = 'completed'
266
+ WHERE id = ? AND status = 'active'
267
+ `,
268
+ args: [now, id],
269
+ });
270
+ if (result.rowsAffected === 0) {
271
+ throw new Error(`Session ${id} not found or not active`);
272
+ }
273
+ }
274
+ async cancelSession(id) {
275
+ const now = new Date().toISOString();
276
+ const result = await this.client.execute({
277
+ sql: `
278
+ UPDATE sessions
279
+ SET endedAt = ?, status = 'cancelled'
280
+ WHERE id = ? AND status = 'active'
281
+ `,
282
+ args: [now, id],
283
+ });
284
+ if (result.rowsAffected === 0) {
285
+ throw new Error(`Session ${id} not found or not active`);
286
+ }
287
+ }
288
+ async getSession(id) {
289
+ const result = await this.client.execute({
290
+ sql: 'SELECT * FROM sessions WHERE id = ?',
291
+ args: [id],
292
+ });
293
+ if (result.rows.length === 0)
294
+ return null;
295
+ return this.rowToSession(result.rows[0]);
296
+ }
297
+ async getActiveSession() {
298
+ const result = await this.client.execute({
299
+ sql: 'SELECT * FROM sessions WHERE status = ? ORDER BY startedAt DESC LIMIT 1',
300
+ args: ['active'],
301
+ });
302
+ if (result.rows.length === 0)
303
+ return null;
304
+ return this.rowToSession(result.rows[0]);
305
+ }
306
+ async getRecentSessions(limit = 20) {
307
+ const result = await this.client.execute({
308
+ sql: 'SELECT * FROM sessions ORDER BY startedAt DESC LIMIT ?',
309
+ args: [limit.toString()],
310
+ });
311
+ return result.rows.map((row) => this.rowToSession(row));
312
+ }
313
+ rowToSession(row) {
314
+ return {
315
+ id: row.id,
316
+ policy: row.policy,
317
+ startedAt: row.startedAt,
318
+ endedAt: row.endedAt,
319
+ status: row.status,
320
+ };
321
+ }
322
+ // ============================================================================
323
+ // Event Log
324
+ // ============================================================================
325
+ async logEvent(type, payload, sessionId) {
326
+ const id = uuidv4();
327
+ const now = new Date().toISOString();
328
+ await this.client.execute({
329
+ sql: `
330
+ INSERT INTO event_log (id, sessionId, type, payload, createdAt)
331
+ VALUES (?, ?, ?, ?, ?)
332
+ `,
333
+ args: [id, sessionId || null, type, JSON.stringify(payload), now],
334
+ });
335
+ return id;
336
+ }
337
+ async getEvents(sessionId, limit = 100) {
338
+ let sql = 'SELECT * FROM event_log';
339
+ const args = [];
340
+ if (sessionId) {
341
+ sql += ' WHERE sessionId = ?';
342
+ args.push(sessionId);
343
+ }
344
+ sql += ' ORDER BY createdAt DESC LIMIT ?';
345
+ args.push(limit.toString());
346
+ const result = await this.client.execute({ sql, args });
347
+ return result.rows.map((row) => this.rowToEventLog(row));
348
+ }
349
+ async getRecentEvents(limit = 50) {
350
+ const result = await this.client.execute({
351
+ sql: 'SELECT * FROM event_log ORDER BY createdAt DESC LIMIT ?',
352
+ args: [limit.toString()],
353
+ });
354
+ return result.rows.map((row) => this.rowToEventLog(row));
355
+ }
356
+ rowToEventLog(row) {
357
+ return {
358
+ id: row.id,
359
+ sessionId: row.sessionId,
360
+ type: row.type,
361
+ payload: row.payload,
362
+ createdAt: row.createdAt,
363
+ };
364
+ }
365
+ // ============================================================================
366
+ // Settings
367
+ // ============================================================================
368
+ async getSetting(key) {
369
+ const result = await this.client.execute({
370
+ sql: 'SELECT value FROM settings WHERE key = ?',
371
+ args: [key],
372
+ });
373
+ if (result.rows.length === 0)
374
+ return null;
375
+ return result.rows[0]?.value;
376
+ }
377
+ async setSetting(key, value) {
378
+ await this.client.execute({
379
+ sql: `
380
+ INSERT INTO settings (key, value, updatedAt)
381
+ VALUES (?, ?, datetime('now'))
382
+ ON CONFLICT(key) DO UPDATE SET
383
+ value = excluded.value,
384
+ updatedAt = datetime('now')
385
+ `,
386
+ args: [key, value],
387
+ });
388
+ }
389
+ async deleteSetting(key) {
390
+ await this.client.execute({
391
+ sql: 'DELETE FROM settings WHERE key = ?',
392
+ args: [key],
393
+ });
394
+ }
395
+ // ============================================================================
396
+ // Statistics
397
+ // ============================================================================
398
+ async getStats() {
399
+ const entriesResult = await this.client.execute({
400
+ sql: 'SELECT COUNT(*) as count FROM journal_entries',
401
+ args: [],
402
+ });
403
+ const sessionsResult = await this.client.execute({
404
+ sql: 'SELECT COUNT(*) as count FROM sessions WHERE status = ?',
405
+ args: ['active'],
406
+ });
407
+ const totalSessionsResult = await this.client.execute({
408
+ sql: 'SELECT COUNT(*) as count FROM sessions',
409
+ args: [],
410
+ });
411
+ const eventsResult = await this.client.execute({
412
+ sql: 'SELECT COUNT(*) as count FROM event_log',
413
+ args: [],
414
+ });
415
+ // Get mood distribution from last 7 days
416
+ const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
417
+ const moodResult = await this.client.execute({
418
+ sql: `
419
+ SELECT moodTags, COUNT(*) as count FROM journal_entries
420
+ WHERE timestamp >= ?
421
+ GROUP BY moodTags
422
+ ORDER BY count DESC
423
+ `,
424
+ args: [cutoff],
425
+ });
426
+ const recentMoodDistribution = {};
427
+ for (const row of moodResult.rows) {
428
+ const tags = (row.moodTags || '').split(',').map((t) => t.trim());
429
+ for (const tag of tags) {
430
+ if (tag) {
431
+ recentMoodDistribution[tag] = (recentMoodDistribution[tag] || 0) + row.count;
432
+ }
433
+ }
434
+ }
435
+ return {
436
+ totalEntries: entriesResult.rows[0]?.count || 0,
437
+ totalSessions: totalSessionsResult.rows[0]?.count || 0,
438
+ activeSessions: sessionsResult.rows[0]?.count || 0,
439
+ eventsLogged: eventsResult.rows[0]?.count || 0,
440
+ recentMoodDistribution,
441
+ };
442
+ }
443
+ /**
444
+ * Get database path
445
+ */
446
+ getDbPath() {
447
+ return this.dbPath;
448
+ }
449
+ /**
450
+ * Validate encryption is enabled in production
451
+ * This should be called after the store is initialized
452
+ */
453
+ static validateEncryptionInProduction(encryptionEnabled) {
454
+ if (process.env.NODE_ENV === 'production' && !encryptionEnabled) {
455
+ throw new Error('Encryption is required in production. Set HARMON_ENCRYPTION_SECRET environment variable.');
456
+ }
457
+ }
458
+ /**
459
+ * Check if encryption should be required based on environment
460
+ */
461
+ static isEncryptionRequired() {
462
+ return process.env.NODE_ENV === 'production';
463
+ }
464
+ }
465
+ /**
466
+ * Create a store with default configuration
467
+ */
468
+ export async function createStore(config) {
469
+ const store = new HarmonStore(config);
470
+ await store.migrate();
471
+ return store;
472
+ }
473
+ //# sourceMappingURL=index.js.map