@cpretzinger/boss-claude 1.0.0 → 1.0.2

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 (87) hide show
  1. package/README.md +304 -1
  2. package/bin/boss-claude.js +1138 -0
  3. package/bin/commands/mode.js +250 -0
  4. package/bin/onyx-guard.js +259 -0
  5. package/bin/onyx-guard.sh +251 -0
  6. package/bin/prompts.js +284 -0
  7. package/bin/rollback.js +85 -0
  8. package/bin/setup-wizard.js +492 -0
  9. package/config/.env.example +17 -0
  10. package/lib/README.md +83 -0
  11. package/lib/agent-logger.js +61 -0
  12. package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
  13. package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
  14. package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
  15. package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
  16. package/lib/agents/memory-supervisor.js +526 -0
  17. package/lib/agents/registry.js +135 -0
  18. package/lib/auto-monitor.js +131 -0
  19. package/lib/checkpoint-hook.js +112 -0
  20. package/lib/checkpoint.js +319 -0
  21. package/lib/commentator.js +213 -0
  22. package/lib/context-scribe.js +120 -0
  23. package/lib/delegation-strategies.js +326 -0
  24. package/lib/hierarchy-validator.js +643 -0
  25. package/lib/index.js +15 -0
  26. package/lib/init-with-mode.js +261 -0
  27. package/lib/init.js +44 -6
  28. package/lib/memory-result-aggregator.js +252 -0
  29. package/lib/memory.js +35 -7
  30. package/lib/mode-enforcer.js +473 -0
  31. package/lib/onyx-banner.js +169 -0
  32. package/lib/onyx-identity.js +214 -0
  33. package/lib/onyx-monitor.js +381 -0
  34. package/lib/onyx-reminder.js +188 -0
  35. package/lib/onyx-tool-interceptor.js +341 -0
  36. package/lib/onyx-wrapper.js +315 -0
  37. package/lib/orchestrator-gate.js +334 -0
  38. package/lib/output-formatter.js +296 -0
  39. package/lib/postgres.js +1 -1
  40. package/lib/prompt-injector.js +220 -0
  41. package/lib/prompts.js +532 -0
  42. package/lib/session.js +153 -6
  43. package/lib/setup/README.md +187 -0
  44. package/lib/setup/env-manager.js +785 -0
  45. package/lib/setup/error-recovery.js +630 -0
  46. package/lib/setup/explain-scopes.js +385 -0
  47. package/lib/setup/github-instructions.js +333 -0
  48. package/lib/setup/github-repo.js +254 -0
  49. package/lib/setup/import-credentials.js +498 -0
  50. package/lib/setup/index.js +62 -0
  51. package/lib/setup/init-postgres.js +785 -0
  52. package/lib/setup/init-redis.js +456 -0
  53. package/lib/setup/integration-test.js +652 -0
  54. package/lib/setup/progress.js +357 -0
  55. package/lib/setup/rollback.js +670 -0
  56. package/lib/setup/rollback.test.js +452 -0
  57. package/lib/setup/setup-with-rollback.example.js +351 -0
  58. package/lib/setup/summary.js +400 -0
  59. package/lib/setup/test-github-setup.js +10 -0
  60. package/lib/setup/test-postgres-init.js +98 -0
  61. package/lib/setup/verify-setup.js +102 -0
  62. package/lib/task-agent-worker.js +235 -0
  63. package/lib/token-monitor.js +466 -0
  64. package/lib/tool-wrapper-integration.js +369 -0
  65. package/lib/tool-wrapper.js +387 -0
  66. package/lib/validators/README.md +497 -0
  67. package/lib/validators/config.js +583 -0
  68. package/lib/validators/config.test.js +175 -0
  69. package/lib/validators/github.js +310 -0
  70. package/lib/validators/github.test.js +61 -0
  71. package/lib/validators/index.js +15 -0
  72. package/lib/validators/postgres.js +525 -0
  73. package/package.json +98 -13
  74. package/scripts/benchmark-memory.js +433 -0
  75. package/scripts/check-secrets.sh +12 -0
  76. package/scripts/fetch-todos.mjs +148 -0
  77. package/scripts/graceful-shutdown.sh +156 -0
  78. package/scripts/install-onyx-hooks.js +373 -0
  79. package/scripts/install.js +119 -18
  80. package/scripts/redis-monitor.js +284 -0
  81. package/scripts/redis-setup.js +412 -0
  82. package/scripts/test-memory-retrieval.js +201 -0
  83. package/scripts/validate-exports.js +68 -0
  84. package/scripts/validate-package.js +120 -0
  85. package/scripts/verify-onyx-deployment.js +309 -0
  86. package/scripts/verify-redis-deployment.js +354 -0
  87. package/scripts/verify-redis-init.js +219 -0
@@ -0,0 +1,633 @@
1
+ /**
2
+ * PostgreSQL Memory Engineer
3
+ *
4
+ * Component: Structured queries on PostgreSQL
5
+ * Schema: boss_claude.sessions table
6
+ *
7
+ * Features:
8
+ * - Query boss_claude.sessions table
9
+ * - Full-text search on summaries and context
10
+ * - Date range filtering
11
+ * - Tag-based filtering
12
+ * - Session retrieval with metadata
13
+ */
14
+
15
+ import pg from 'pg';
16
+ const { Pool } = pg;
17
+
18
+ // Use shared connection pool
19
+ let pool;
20
+
21
+ /**
22
+ * Initialize PostgreSQL connection pool
23
+ */
24
+ function getPool() {
25
+ if (!pool) {
26
+ pool = new Pool({
27
+ connectionString: process.env.BOSS_CLAUDE_PG_URL,
28
+ max: 10,
29
+ idleTimeoutMillis: 30000,
30
+ connectionTimeoutMillis: 2000,
31
+ ssl: {
32
+ rejectUnauthorized: true
33
+ }
34
+ });
35
+
36
+ pool.on('error', (err) => {
37
+ console.error('[PostgresMemoryEngineer] Pool error:', err);
38
+ });
39
+ }
40
+ return pool;
41
+ }
42
+
43
+ /**
44
+ * Execute a query with timeout protection
45
+ * @param {Object} pool - PostgreSQL pool
46
+ * @param {string} query - SQL query
47
+ * @param {Array} params - Query parameters
48
+ * @param {number} timeout - Timeout in milliseconds (default: 30000)
49
+ * @returns {Promise<Object>} Query result
50
+ */
51
+ async function queryWithTimeout(pool, query, params, timeout = 30000) {
52
+ return Promise.race([
53
+ pool.query(query, params),
54
+ new Promise((_, reject) =>
55
+ setTimeout(() => reject(new Error('[PostgresMemoryEngineer] Query timeout')), timeout)
56
+ )
57
+ ]);
58
+ }
59
+
60
+ /**
61
+ * Validate ORDER BY clause to prevent SQL injection
62
+ * @param {string} orderBy - Column name to order by
63
+ * @param {string} orderDir - Direction (ASC or DESC)
64
+ * @returns {Object} Validated orderBy and orderDir
65
+ * @throws {Error} If invalid column or direction
66
+ */
67
+ function validateOrderClause(orderBy, orderDir) {
68
+ // Whitelist of allowed ORDER BY columns
69
+ const ALLOWED_ORDER_COLUMNS = [
70
+ 'id',
71
+ 'user_id',
72
+ 'project',
73
+ 'start_time',
74
+ 'end_time',
75
+ 'duration_seconds',
76
+ 'xp_earned',
77
+ 'tokens_saved',
78
+ 'tasks_completed',
79
+ 'perfect_executions',
80
+ 'efficiency_multiplier',
81
+ 'level_at_start',
82
+ 'level_at_end',
83
+ 'relevance'
84
+ ];
85
+
86
+ // Whitelist of allowed directions
87
+ const ALLOWED_DIRECTIONS = ['ASC', 'DESC'];
88
+
89
+ const normalizedOrderBy = orderBy?.toLowerCase() || 'start_time';
90
+ const normalizedOrderDir = orderDir?.toUpperCase() || 'DESC';
91
+
92
+ if (!ALLOWED_ORDER_COLUMNS.includes(normalizedOrderBy)) {
93
+ throw new Error(`Invalid ORDER BY column: ${orderBy}. Allowed columns: ${ALLOWED_ORDER_COLUMNS.join(', ')}`);
94
+ }
95
+
96
+ if (!ALLOWED_DIRECTIONS.includes(normalizedOrderDir)) {
97
+ throw new Error(`Invalid ORDER BY direction: ${orderDir}. Allowed: ASC, DESC`);
98
+ }
99
+
100
+ return {
101
+ orderBy: normalizedOrderBy,
102
+ orderDir: normalizedOrderDir
103
+ };
104
+ }
105
+
106
+ /**
107
+ * PostgreSQL Memory Engineer
108
+ */
109
+ export class PostgresMemoryEngineer {
110
+ constructor() {
111
+ this.pool = getPool();
112
+ }
113
+
114
+ /**
115
+ * Search sessions with full-text search on summary and context_data
116
+ * @param {string} userId - User identifier
117
+ * @param {string} searchTerm - Search term for full-text search
118
+ * @param {Object} options - Search options
119
+ * @returns {Promise<Array>} Matching sessions
120
+ */
121
+ async fullTextSearch(userId, searchTerm, options = {}) {
122
+ try {
123
+ const {
124
+ limit = 50,
125
+ offset = 0,
126
+ orderBy = 'start_time',
127
+ orderDir = 'DESC'
128
+ } = options;
129
+
130
+ // Validate ORDER BY clause to prevent SQL injection
131
+ const validated = validateOrderClause(orderBy, orderDir);
132
+
133
+ const query = `
134
+ SELECT
135
+ id,
136
+ user_id,
137
+ project,
138
+ start_time,
139
+ end_time,
140
+ duration_seconds,
141
+ xp_earned,
142
+ tokens_saved,
143
+ tasks_completed,
144
+ perfect_executions,
145
+ efficiency_multiplier,
146
+ level_at_start,
147
+ level_at_end,
148
+ summary,
149
+ context_data,
150
+ ts_rank(
151
+ to_tsvector('english', COALESCE(summary, '') || ' ' || COALESCE(context_data::text, '')),
152
+ plainto_tsquery('english', $2)
153
+ ) as relevance
154
+ FROM boss_claude.sessions
155
+ WHERE user_id = $1
156
+ AND (
157
+ to_tsvector('english', COALESCE(summary, '') || ' ' || COALESCE(context_data::text, ''))
158
+ @@ plainto_tsquery('english', $2)
159
+ )
160
+ ORDER BY ${validated.orderBy} ${validated.orderDir}
161
+ LIMIT $3 OFFSET $4
162
+ `;
163
+
164
+ const result = await queryWithTimeout(this.pool, query, [userId, searchTerm, limit, offset]);
165
+ return result.rows;
166
+ } catch (error) {
167
+ console.error('[PostgresMemoryEngineer] fullTextSearch error:', error);
168
+ throw new Error(`Failed to execute full-text search: ${error.message}`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Filter sessions by date range
174
+ * @param {string} userId - User identifier
175
+ * @param {Date|string} startDate - Start date
176
+ * @param {Date|string} endDate - End date
177
+ * @param {Object} options - Query options
178
+ * @returns {Promise<Array>} Sessions in date range
179
+ */
180
+ async filterByDateRange(userId, startDate, endDate, options = {}) {
181
+ try {
182
+ const {
183
+ limit = 100,
184
+ offset = 0,
185
+ includeActive = true,
186
+ orderBy = 'start_time',
187
+ orderDir = 'DESC'
188
+ } = options;
189
+
190
+ // Validate ORDER BY clause to prevent SQL injection
191
+ const validated = validateOrderClause(orderBy, orderDir);
192
+
193
+ let query = `
194
+ SELECT
195
+ id,
196
+ user_id,
197
+ project,
198
+ start_time,
199
+ end_time,
200
+ duration_seconds,
201
+ xp_earned,
202
+ tokens_saved,
203
+ tasks_completed,
204
+ perfect_executions,
205
+ efficiency_multiplier,
206
+ level_at_start,
207
+ level_at_end,
208
+ summary,
209
+ context_data
210
+ FROM boss_claude.sessions
211
+ WHERE user_id = $1
212
+ AND start_time >= $2
213
+ AND start_time <= $3
214
+ `;
215
+
216
+ const params = [userId, startDate, endDate];
217
+
218
+ if (!includeActive) {
219
+ query += ` AND end_time IS NOT NULL`;
220
+ }
221
+
222
+ query += `
223
+ ORDER BY ${validated.orderBy} ${validated.orderDir}
224
+ LIMIT $4 OFFSET $5
225
+ `;
226
+
227
+ params.push(limit, offset);
228
+
229
+ const result = await queryWithTimeout(this.pool, query, params);
230
+ return result.rows;
231
+ } catch (error) {
232
+ console.error('[PostgresMemoryEngineer] filterByDateRange error:', error);
233
+ throw new Error(`Failed to filter by date range: ${error.message}`);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Filter sessions by tags in context_data
239
+ * @param {string} userId - User identifier
240
+ * @param {Array<string>} tags - Tags to filter by
241
+ * @param {Object} options - Query options
242
+ * @returns {Promise<Array>} Sessions matching tags
243
+ */
244
+ async filterByTags(userId, tags, options = {}) {
245
+ try {
246
+ const {
247
+ limit = 50,
248
+ offset = 0,
249
+ matchAll = false, // true = AND, false = OR
250
+ orderBy = 'start_time',
251
+ orderDir = 'DESC'
252
+ } = options;
253
+
254
+ // Validate ORDER BY clause to prevent SQL injection
255
+ const validated = validateOrderClause(orderBy, orderDir);
256
+
257
+ // Build JSONB query for tags
258
+ const tagConditions = tags.map((tag, idx) => {
259
+ return `context_data->'tags' @> $${idx + 2}`;
260
+ }).join(matchAll ? ' AND ' : ' OR ');
261
+
262
+ const query = `
263
+ SELECT
264
+ id,
265
+ user_id,
266
+ project,
267
+ start_time,
268
+ end_time,
269
+ duration_seconds,
270
+ xp_earned,
271
+ tokens_saved,
272
+ tasks_completed,
273
+ perfect_executions,
274
+ efficiency_multiplier,
275
+ level_at_start,
276
+ level_at_end,
277
+ summary,
278
+ context_data,
279
+ context_data->'tags' as tags
280
+ FROM boss_claude.sessions
281
+ WHERE user_id = $1
282
+ AND (${tagConditions})
283
+ ORDER BY ${validated.orderBy} ${validated.orderDir}
284
+ LIMIT $${tags.length + 2} OFFSET $${tags.length + 3}
285
+ `;
286
+
287
+ const params = [
288
+ userId,
289
+ ...tags.map(tag => JSON.stringify([tag])),
290
+ limit,
291
+ offset
292
+ ];
293
+
294
+ const result = await queryWithTimeout(this.pool, query, params);
295
+ return result.rows;
296
+ } catch (error) {
297
+ console.error('[PostgresMemoryEngineer] filterByTags error:', error);
298
+ throw new Error(`Failed to filter by tags: ${error.message}`);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Get sessions by project name
304
+ * @param {string} userId - User identifier
305
+ * @param {string} projectName - Project name
306
+ * @param {Object} options - Query options
307
+ * @returns {Promise<Array>} Sessions for project
308
+ */
309
+ async filterByProject(userId, projectName, options = {}) {
310
+ try {
311
+ const {
312
+ limit = 50,
313
+ offset = 0,
314
+ orderBy = 'start_time',
315
+ orderDir = 'DESC'
316
+ } = options;
317
+
318
+ // Validate ORDER BY clause to prevent SQL injection
319
+ const validated = validateOrderClause(orderBy, orderDir);
320
+
321
+ const query = `
322
+ SELECT
323
+ id,
324
+ user_id,
325
+ project,
326
+ start_time,
327
+ end_time,
328
+ duration_seconds,
329
+ xp_earned,
330
+ tokens_saved,
331
+ tasks_completed,
332
+ perfect_executions,
333
+ efficiency_multiplier,
334
+ level_at_start,
335
+ level_at_end,
336
+ summary,
337
+ context_data
338
+ FROM boss_claude.sessions
339
+ WHERE user_id = $1
340
+ AND project = $2
341
+ ORDER BY ${validated.orderBy} ${validated.orderDir}
342
+ LIMIT $3 OFFSET $4
343
+ `;
344
+
345
+ const result = await queryWithTimeout(this.pool, query, [userId, projectName, limit, offset]);
346
+ return result.rows;
347
+ } catch (error) {
348
+ console.error('[PostgresMemoryEngineer] filterByProject error:', error);
349
+ throw new Error(`Failed to filter by project: ${error.message}`);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Advanced query builder with multiple filters
355
+ * @param {string} userId - User identifier
356
+ * @param {Object} filters - Combined filters
357
+ * @returns {Promise<Array>} Filtered sessions
358
+ */
359
+ async advancedQuery(userId, filters = {}) {
360
+ const {
361
+ searchTerm,
362
+ startDate,
363
+ endDate,
364
+ tags,
365
+ project,
366
+ minXp,
367
+ minTokens,
368
+ minEfficiency,
369
+ includeActive = true,
370
+ limit = 50,
371
+ offset = 0,
372
+ orderBy = 'start_time',
373
+ orderDir = 'DESC'
374
+ } = filters;
375
+
376
+ // Validate ORDER BY clause to prevent SQL injection
377
+ const validated = validateOrderClause(orderBy, orderDir);
378
+
379
+ let conditions = ['user_id = $1'];
380
+ let params = [userId];
381
+ let paramCount = 1;
382
+
383
+ // Full-text search
384
+ if (searchTerm) {
385
+ paramCount++;
386
+ conditions.push(`
387
+ to_tsvector('english', COALESCE(summary, '') || ' ' || COALESCE(context_data::text, ''))
388
+ @@ plainto_tsquery('english', $${paramCount})
389
+ `);
390
+ params.push(searchTerm);
391
+ }
392
+
393
+ // Date range
394
+ if (startDate) {
395
+ paramCount++;
396
+ conditions.push(`start_time >= $${paramCount}`);
397
+ params.push(startDate);
398
+ }
399
+ if (endDate) {
400
+ paramCount++;
401
+ conditions.push(`start_time <= $${paramCount}`);
402
+ params.push(endDate);
403
+ }
404
+
405
+ // Tags
406
+ if (tags && tags.length > 0) {
407
+ const tagConditions = tags.map(tag => {
408
+ paramCount++;
409
+ params.push(JSON.stringify([tag]));
410
+ return `context_data->'tags' @> $${paramCount}`;
411
+ }).join(' OR ');
412
+ conditions.push(`(${tagConditions})`);
413
+ }
414
+
415
+ // Project
416
+ if (project) {
417
+ paramCount++;
418
+ conditions.push(`project = $${paramCount}`);
419
+ params.push(project);
420
+ }
421
+
422
+ // XP threshold
423
+ if (minXp !== undefined) {
424
+ paramCount++;
425
+ conditions.push(`xp_earned >= $${paramCount}`);
426
+ params.push(minXp);
427
+ }
428
+
429
+ // Token threshold
430
+ if (minTokens !== undefined) {
431
+ paramCount++;
432
+ conditions.push(`tokens_saved >= $${paramCount}`);
433
+ params.push(minTokens);
434
+ }
435
+
436
+ // Efficiency threshold
437
+ if (minEfficiency !== undefined) {
438
+ paramCount++;
439
+ conditions.push(`efficiency_multiplier >= $${paramCount}`);
440
+ params.push(minEfficiency);
441
+ }
442
+
443
+ // Active sessions filter
444
+ if (!includeActive) {
445
+ conditions.push('end_time IS NOT NULL');
446
+ }
447
+
448
+ const query = `
449
+ SELECT
450
+ id,
451
+ user_id,
452
+ project,
453
+ start_time,
454
+ end_time,
455
+ duration_seconds,
456
+ xp_earned,
457
+ tokens_saved,
458
+ tasks_completed,
459
+ perfect_executions,
460
+ efficiency_multiplier,
461
+ level_at_start,
462
+ level_at_end,
463
+ summary,
464
+ context_data,
465
+ ${searchTerm ? `
466
+ ts_rank(
467
+ to_tsvector('english', COALESCE(summary, '') || ' ' || COALESCE(context_data::text, '')),
468
+ plainto_tsquery('english', $2)
469
+ ) as relevance,
470
+ ` : ''}
471
+ context_data->'tags' as tags
472
+ FROM boss_claude.sessions
473
+ WHERE ${conditions.join(' AND ')}
474
+ ORDER BY ${validated.orderBy} ${validated.orderDir}
475
+ LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}
476
+ `;
477
+
478
+ params.push(limit, offset);
479
+
480
+ const result = await queryWithTimeout(this.pool, query, params);
481
+ return result.rows;
482
+ }
483
+
484
+ /**
485
+ * Get session statistics and aggregations
486
+ * @param {string} userId - User identifier
487
+ * @param {Object} filters - Filters to apply before aggregation
488
+ * @returns {Promise<Object>} Aggregated statistics
489
+ */
490
+ async getSessionStats(userId, filters = {}) {
491
+ const {
492
+ startDate,
493
+ endDate,
494
+ project,
495
+ tags
496
+ } = filters;
497
+
498
+ let conditions = ['user_id = $1'];
499
+ let params = [userId];
500
+ let paramCount = 1;
501
+
502
+ if (startDate) {
503
+ paramCount++;
504
+ conditions.push(`start_time >= $${paramCount}`);
505
+ params.push(startDate);
506
+ }
507
+ if (endDate) {
508
+ paramCount++;
509
+ conditions.push(`start_time <= $${paramCount}`);
510
+ params.push(endDate);
511
+ }
512
+ if (project) {
513
+ paramCount++;
514
+ conditions.push(`project = $${paramCount}`);
515
+ params.push(project);
516
+ }
517
+ if (tags && tags.length > 0) {
518
+ const tagConditions = tags.map(tag => {
519
+ paramCount++;
520
+ params.push(JSON.stringify([tag]));
521
+ return `context_data->'tags' @> $${paramCount}`;
522
+ }).join(' OR ');
523
+ conditions.push(`(${tagConditions})`);
524
+ }
525
+
526
+ const query = `
527
+ SELECT
528
+ COUNT(*) as total_sessions,
529
+ COUNT(*) FILTER (WHERE end_time IS NOT NULL) as completed_sessions,
530
+ COUNT(*) FILTER (WHERE end_time IS NULL) as active_sessions,
531
+ COALESCE(SUM(xp_earned), 0) as total_xp,
532
+ COALESCE(SUM(tokens_saved), 0) as total_tokens_saved,
533
+ COALESCE(SUM(tasks_completed), 0) as total_tasks,
534
+ COALESCE(SUM(perfect_executions), 0) as total_perfect_executions,
535
+ COALESCE(AVG(efficiency_multiplier), 0) as avg_efficiency,
536
+ COALESCE(MAX(efficiency_multiplier), 0) as max_efficiency,
537
+ COALESCE(AVG(duration_seconds), 0) as avg_duration_seconds,
538
+ COALESCE(MAX(xp_earned), 0) as max_xp_session,
539
+ MIN(start_time) as earliest_session,
540
+ MAX(start_time) as latest_session,
541
+ COUNT(DISTINCT project) as unique_projects
542
+ FROM boss_claude.sessions
543
+ WHERE ${conditions.join(' AND ')}
544
+ `;
545
+
546
+ const result = await queryWithTimeout(this.pool, query, params);
547
+ return result.rows[0];
548
+ }
549
+
550
+ /**
551
+ * Get top performing sessions
552
+ * @param {string} userId - User identifier
553
+ * @param {Object} options - Query options
554
+ * @returns {Promise<Array>} Top sessions by metric
555
+ */
556
+ async getTopSessions(userId, options = {}) {
557
+ const {
558
+ metric = 'xp_earned', // xp_earned, tokens_saved, efficiency_multiplier, tasks_completed
559
+ limit = 10,
560
+ startDate,
561
+ endDate
562
+ } = options;
563
+
564
+ const validMetrics = ['xp_earned', 'tokens_saved', 'efficiency_multiplier', 'tasks_completed'];
565
+ if (!validMetrics.includes(metric)) {
566
+ throw new Error(`Invalid metric: ${metric}. Must be one of: ${validMetrics.join(', ')}`);
567
+ }
568
+
569
+ let conditions = ['user_id = $1', 'end_time IS NOT NULL'];
570
+ let params = [userId];
571
+ let paramCount = 1;
572
+
573
+ if (startDate) {
574
+ paramCount++;
575
+ conditions.push(`start_time >= $${paramCount}`);
576
+ params.push(startDate);
577
+ }
578
+ if (endDate) {
579
+ paramCount++;
580
+ conditions.push(`start_time <= $${paramCount}`);
581
+ params.push(endDate);
582
+ }
583
+
584
+ const query = `
585
+ SELECT
586
+ id,
587
+ project,
588
+ start_time,
589
+ end_time,
590
+ duration_seconds,
591
+ xp_earned,
592
+ tokens_saved,
593
+ tasks_completed,
594
+ perfect_executions,
595
+ efficiency_multiplier,
596
+ summary,
597
+ context_data
598
+ FROM boss_claude.sessions
599
+ WHERE ${conditions.join(' AND ')}
600
+ ORDER BY ${metric} DESC, start_time DESC
601
+ LIMIT $${paramCount + 1}
602
+ `;
603
+
604
+ params.push(limit);
605
+
606
+ const result = await queryWithTimeout(this.pool, query, params);
607
+ return result.rows;
608
+ }
609
+
610
+ /**
611
+ * Close the connection pool
612
+ */
613
+ async close() {
614
+ try {
615
+ if (pool) {
616
+ await pool.end();
617
+ pool = null;
618
+ }
619
+ } catch (error) {
620
+ console.error('[PostgresMemoryEngineer] Error closing pool:', error);
621
+ throw error;
622
+ }
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Factory function to create a PostgreSQL Memory Engineer instance
628
+ */
629
+ export function createPostgresMemoryEngineer() {
630
+ return new PostgresMemoryEngineer();
631
+ }
632
+
633
+ export default PostgresMemoryEngineer;