@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.
- package/README.md +304 -1
- package/bin/boss-claude.js +1138 -0
- package/bin/commands/mode.js +250 -0
- package/bin/onyx-guard.js +259 -0
- package/bin/onyx-guard.sh +251 -0
- package/bin/prompts.js +284 -0
- package/bin/rollback.js +85 -0
- package/bin/setup-wizard.js +492 -0
- package/config/.env.example +17 -0
- package/lib/README.md +83 -0
- package/lib/agent-logger.js +61 -0
- package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
- package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
- package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
- package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
- package/lib/agents/memory-supervisor.js +526 -0
- package/lib/agents/registry.js +135 -0
- package/lib/auto-monitor.js +131 -0
- package/lib/checkpoint-hook.js +112 -0
- package/lib/checkpoint.js +319 -0
- package/lib/commentator.js +213 -0
- package/lib/context-scribe.js +120 -0
- package/lib/delegation-strategies.js +326 -0
- package/lib/hierarchy-validator.js +643 -0
- package/lib/index.js +15 -0
- package/lib/init-with-mode.js +261 -0
- package/lib/init.js +44 -6
- package/lib/memory-result-aggregator.js +252 -0
- package/lib/memory.js +35 -7
- package/lib/mode-enforcer.js +473 -0
- package/lib/onyx-banner.js +169 -0
- package/lib/onyx-identity.js +214 -0
- package/lib/onyx-monitor.js +381 -0
- package/lib/onyx-reminder.js +188 -0
- package/lib/onyx-tool-interceptor.js +341 -0
- package/lib/onyx-wrapper.js +315 -0
- package/lib/orchestrator-gate.js +334 -0
- package/lib/output-formatter.js +296 -0
- package/lib/postgres.js +1 -1
- package/lib/prompt-injector.js +220 -0
- package/lib/prompts.js +532 -0
- package/lib/session.js +153 -6
- package/lib/setup/README.md +187 -0
- package/lib/setup/env-manager.js +785 -0
- package/lib/setup/error-recovery.js +630 -0
- package/lib/setup/explain-scopes.js +385 -0
- package/lib/setup/github-instructions.js +333 -0
- package/lib/setup/github-repo.js +254 -0
- package/lib/setup/import-credentials.js +498 -0
- package/lib/setup/index.js +62 -0
- package/lib/setup/init-postgres.js +785 -0
- package/lib/setup/init-redis.js +456 -0
- package/lib/setup/integration-test.js +652 -0
- package/lib/setup/progress.js +357 -0
- package/lib/setup/rollback.js +670 -0
- package/lib/setup/rollback.test.js +452 -0
- package/lib/setup/setup-with-rollback.example.js +351 -0
- package/lib/setup/summary.js +400 -0
- package/lib/setup/test-github-setup.js +10 -0
- package/lib/setup/test-postgres-init.js +98 -0
- package/lib/setup/verify-setup.js +102 -0
- package/lib/task-agent-worker.js +235 -0
- package/lib/token-monitor.js +466 -0
- package/lib/tool-wrapper-integration.js +369 -0
- package/lib/tool-wrapper.js +387 -0
- package/lib/validators/README.md +497 -0
- package/lib/validators/config.js +583 -0
- package/lib/validators/config.test.js +175 -0
- package/lib/validators/github.js +310 -0
- package/lib/validators/github.test.js +61 -0
- package/lib/validators/index.js +15 -0
- package/lib/validators/postgres.js +525 -0
- package/package.json +98 -13
- package/scripts/benchmark-memory.js +433 -0
- package/scripts/check-secrets.sh +12 -0
- package/scripts/fetch-todos.mjs +148 -0
- package/scripts/graceful-shutdown.sh +156 -0
- package/scripts/install-onyx-hooks.js +373 -0
- package/scripts/install.js +119 -18
- package/scripts/redis-monitor.js +284 -0
- package/scripts/redis-setup.js +412 -0
- package/scripts/test-memory-retrieval.js +201 -0
- package/scripts/validate-exports.js +68 -0
- package/scripts/validate-package.js +120 -0
- package/scripts/verify-onyx-deployment.js +309 -0
- package/scripts/verify-redis-deployment.js +354 -0
- 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;
|