@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,785 @@
1
+ /**
2
+ * Boss Claude PostgreSQL Initialization
3
+ *
4
+ * Sets up all required PostgreSQL tables and functions according to schema.sql
5
+ * Creates boss_claude schema, tables, indexes, and helper functions.
6
+ *
7
+ * Schema Structure:
8
+ * - boss_claude.sessions - Session tracking and analytics
9
+ * - boss_claude.achievements - User achievement records
10
+ * - boss_claude.memory_snapshots - Point-in-time state snapshots
11
+ * - boss_claude.stats_rollups - Aggregated statistics
12
+ *
13
+ * This module runs automatically during setup wizard and can be
14
+ * re-run safely to repair/update the PostgreSQL schema.
15
+ */
16
+
17
+ import pg from 'pg';
18
+ import chalk from 'chalk';
19
+ import fs from 'fs/promises';
20
+ import path from 'path';
21
+ import { fileURLToPath } from 'url';
22
+
23
+ const { Pool } = pg;
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+
28
+ /**
29
+ * Execute SQL from schema.sql file
30
+ *
31
+ * @param {pg.Pool} pool - PostgreSQL connection pool
32
+ * @param {string} sqlContent - SQL content to execute
33
+ * @returns {Promise<Array>} Array of execution results
34
+ */
35
+ async function executeSchemaSql(pool, sqlContent) {
36
+ const results = [];
37
+
38
+ // Split SQL into individual statements (handle multi-line comments and statements)
39
+ const statements = sqlContent
40
+ .split(';')
41
+ .map(stmt => stmt.trim())
42
+ .filter(stmt => stmt.length > 0 && !stmt.startsWith('--'));
43
+
44
+ for (const statement of statements) {
45
+ try {
46
+ const result = await pool.query(statement);
47
+ results.push({ success: true, statement: statement.substring(0, 100) + '...' });
48
+ } catch (error) {
49
+ // Some errors are acceptable (e.g., already exists)
50
+ if (error.code === '42P07' || // relation already exists
51
+ error.code === '42710' || // object already exists
52
+ error.code === '42P06') { // schema already exists
53
+ results.push({ success: true, existed: true, statement: statement.substring(0, 100) + '...' });
54
+ } else {
55
+ results.push({ success: false, error: error.message, statement: statement.substring(0, 100) + '...' });
56
+ // Continue with other statements even if one fails
57
+ }
58
+ }
59
+ }
60
+
61
+ return results;
62
+ }
63
+
64
+ /**
65
+ * Initialize PostgreSQL with all required schema objects
66
+ *
67
+ * @param {string} connectionString - PostgreSQL connection string
68
+ * @param {boolean} force - Force re-initialization even if schema exists
69
+ * @returns {Promise<Object>} Initialization results
70
+ */
71
+ export async function initializePostgres(connectionString, force = false) {
72
+ const pool = new Pool({
73
+ connectionString,
74
+ max: 5,
75
+ idleTimeoutMillis: 30000,
76
+ connectionTimeoutMillis: 5000,
77
+ });
78
+
79
+ const results = {
80
+ extensions: { created: false, existed: false, count: 0 },
81
+ schema: { created: false, existed: false },
82
+ tables: { created: [], existed: [], failed: [] },
83
+ indexes: { created: [], existed: [], failed: [] },
84
+ functions: { created: [], existed: [], failed: [] },
85
+ triggers: { created: [], existed: [], failed: [] },
86
+ healthCheck: { passed: false }
87
+ };
88
+
89
+ try {
90
+ // Test connection
91
+ await pool.query('SELECT NOW()');
92
+
93
+ // 1. Check if schema exists
94
+ const schemaCheck = await pool.query(
95
+ `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'boss_claude'`
96
+ );
97
+
98
+ if (schemaCheck.rows.length > 0 && !force) {
99
+ results.schema.existed = true;
100
+ }
101
+
102
+ // 2. Enable extensions
103
+ try {
104
+ await pool.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
105
+ await pool.query('CREATE EXTENSION IF NOT EXISTS "pg_stat_statements"');
106
+ results.extensions.count = 2;
107
+ results.extensions.created = true;
108
+ } catch (error) {
109
+ // Extensions might already exist or require superuser
110
+ results.extensions.existed = true;
111
+ }
112
+
113
+ // 3. Create schema
114
+ try {
115
+ if (force) {
116
+ // In force mode, we don't drop the schema, just ensure it exists
117
+ await pool.query('CREATE SCHEMA IF NOT EXISTS boss_claude');
118
+ results.schema.created = true;
119
+ } else {
120
+ await pool.query('CREATE SCHEMA IF NOT EXISTS boss_claude');
121
+ results.schema.created = !results.schema.existed;
122
+ }
123
+ } catch (error) {
124
+ if (error.code === '42P06') { // schema already exists
125
+ results.schema.existed = true;
126
+ } else {
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ // 4. Create tables
132
+ const tables = [
133
+ {
134
+ name: 'sessions',
135
+ sql: `
136
+ CREATE TABLE IF NOT EXISTS boss_claude.sessions (
137
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
138
+ user_id VARCHAR(255) NOT NULL,
139
+ project VARCHAR(255),
140
+ start_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
141
+ end_time TIMESTAMPTZ,
142
+ duration_seconds INTEGER GENERATED ALWAYS AS (
143
+ EXTRACT(EPOCH FROM (end_time - start_time))::INTEGER
144
+ ) STORED,
145
+ summary TEXT,
146
+ xp_earned INTEGER DEFAULT 0,
147
+ tokens_saved INTEGER DEFAULT 0,
148
+ level_at_start INTEGER DEFAULT 0,
149
+ level_at_end INTEGER,
150
+ tasks_completed INTEGER DEFAULT 0,
151
+ perfect_executions INTEGER DEFAULT 0,
152
+ efficiency_multiplier NUMERIC(4,2) DEFAULT 1.0,
153
+ context_data JSONB,
154
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
155
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
156
+ )
157
+ `
158
+ },
159
+ {
160
+ name: 'achievements',
161
+ sql: `
162
+ CREATE TABLE IF NOT EXISTS boss_claude.achievements (
163
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
164
+ user_id VARCHAR(255) NOT NULL,
165
+ achievement_type VARCHAR(100) NOT NULL,
166
+ achievement_name VARCHAR(255) NOT NULL,
167
+ description TEXT,
168
+ xp_reward INTEGER DEFAULT 0,
169
+ metadata JSONB,
170
+ earned_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
171
+ )
172
+ `
173
+ },
174
+ {
175
+ name: 'memory_snapshots',
176
+ sql: `
177
+ CREATE TABLE IF NOT EXISTS boss_claude.memory_snapshots (
178
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
179
+ user_id VARCHAR(255) NOT NULL,
180
+ session_id UUID REFERENCES boss_claude.sessions(id) ON DELETE CASCADE,
181
+ snapshot_type VARCHAR(50) NOT NULL,
182
+ snapshot_data JSONB NOT NULL,
183
+ level INTEGER,
184
+ token_bank INTEGER,
185
+ total_xp INTEGER,
186
+ efficiency NUMERIC(4,2),
187
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
188
+ )
189
+ `
190
+ },
191
+ {
192
+ name: 'stats_rollups',
193
+ sql: `
194
+ CREATE TABLE IF NOT EXISTS boss_claude.stats_rollups (
195
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
196
+ user_id VARCHAR(255) NOT NULL,
197
+ rollup_period VARCHAR(20) NOT NULL,
198
+ period_start TIMESTAMPTZ NOT NULL,
199
+ period_end TIMESTAMPTZ NOT NULL,
200
+ total_sessions INTEGER DEFAULT 0,
201
+ total_xp_earned INTEGER DEFAULT 0,
202
+ total_tokens_saved INTEGER DEFAULT 0,
203
+ total_tasks_completed INTEGER DEFAULT 0,
204
+ avg_efficiency NUMERIC(4,2),
205
+ top_projects TEXT[],
206
+ achievements_earned INTEGER DEFAULT 0,
207
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
208
+ UNIQUE(user_id, rollup_period, period_start)
209
+ )
210
+ `
211
+ }
212
+ ];
213
+
214
+ for (const table of tables) {
215
+ try {
216
+ await pool.query(table.sql);
217
+
218
+ // Check if table existed before
219
+ const tableExists = await pool.query(
220
+ `SELECT table_name FROM information_schema.tables
221
+ WHERE table_schema = 'boss_claude' AND table_name = $1`,
222
+ [table.name]
223
+ );
224
+
225
+ if (tableExists.rows.length > 0) {
226
+ results.tables.created.push(table.name);
227
+ }
228
+ } catch (error) {
229
+ if (error.code === '42P07') { // table already exists
230
+ results.tables.existed.push(table.name);
231
+ } else {
232
+ results.tables.failed.push({ name: table.name, error: error.message });
233
+ }
234
+ }
235
+ }
236
+
237
+ // 5. Create indexes
238
+ const indexes = [
239
+ 'CREATE INDEX IF NOT EXISTS idx_sessions_user_start ON boss_claude.sessions(user_id, start_time DESC)',
240
+ 'CREATE INDEX IF NOT EXISTS idx_sessions_project ON boss_claude.sessions(project) WHERE project IS NOT NULL',
241
+ 'CREATE INDEX IF NOT EXISTS idx_sessions_end_time ON boss_claude.sessions(end_time) WHERE end_time IS NOT NULL',
242
+ 'CREATE INDEX IF NOT EXISTS idx_sessions_xp ON boss_claude.sessions(xp_earned DESC) WHERE xp_earned > 0',
243
+ 'CREATE INDEX IF NOT EXISTS idx_achievements_user_earned ON boss_claude.achievements(user_id, earned_at DESC)',
244
+ 'CREATE INDEX IF NOT EXISTS idx_achievements_type ON boss_claude.achievements(achievement_type)',
245
+ 'CREATE INDEX IF NOT EXISTS idx_achievements_earned_at ON boss_claude.achievements(earned_at DESC)',
246
+ 'CREATE INDEX IF NOT EXISTS idx_snapshots_user_created ON boss_claude.memory_snapshots(user_id, created_at DESC)',
247
+ 'CREATE INDEX IF NOT EXISTS idx_snapshots_session ON boss_claude.memory_snapshots(session_id)',
248
+ 'CREATE INDEX IF NOT EXISTS idx_snapshots_type ON boss_claude.memory_snapshots(snapshot_type)',
249
+ 'CREATE INDEX IF NOT EXISTS idx_rollups_user_period ON boss_claude.stats_rollups(user_id, rollup_period, period_start DESC)'
250
+ ];
251
+
252
+ for (const indexSql of indexes) {
253
+ try {
254
+ await pool.query(indexSql);
255
+ const indexName = indexSql.match(/idx_\w+/)?.[0];
256
+ if (indexName) {
257
+ results.indexes.created.push(indexName);
258
+ }
259
+ } catch (error) {
260
+ if (error.code === '42P07') { // index already exists
261
+ const indexName = indexSql.match(/idx_\w+/)?.[0];
262
+ if (indexName) {
263
+ results.indexes.existed.push(indexName);
264
+ }
265
+ } else {
266
+ results.indexes.failed.push(error.message);
267
+ }
268
+ }
269
+ }
270
+
271
+ // 6. Create trigger function
272
+ try {
273
+ await pool.query(`
274
+ CREATE OR REPLACE FUNCTION boss_claude.update_updated_at()
275
+ RETURNS TRIGGER AS $$
276
+ BEGIN
277
+ NEW.updated_at = NOW();
278
+ RETURN NEW;
279
+ END;
280
+ $$ LANGUAGE plpgsql
281
+ `);
282
+ results.functions.created.push('update_updated_at');
283
+ } catch (error) {
284
+ results.functions.failed.push({ name: 'update_updated_at', error: error.message });
285
+ }
286
+
287
+ // 7. Create helper functions
288
+ const helperFunctions = [
289
+ {
290
+ name: 'fn_get_current_session',
291
+ sql: `
292
+ CREATE OR REPLACE FUNCTION boss_claude.fn_get_current_session(p_user_id VARCHAR)
293
+ RETURNS TABLE(
294
+ session_id UUID,
295
+ start_time TIMESTAMPTZ,
296
+ project VARCHAR,
297
+ xp_earned INTEGER,
298
+ level_at_start INTEGER
299
+ ) AS $$
300
+ BEGIN
301
+ RETURN QUERY
302
+ SELECT
303
+ id,
304
+ start_time,
305
+ project,
306
+ xp_earned,
307
+ level_at_start
308
+ FROM boss_claude.sessions
309
+ WHERE user_id = p_user_id
310
+ AND end_time IS NULL
311
+ ORDER BY start_time DESC
312
+ LIMIT 1;
313
+ END;
314
+ $$ LANGUAGE plpgsql
315
+ `
316
+ },
317
+ {
318
+ name: 'fn_get_user_stats',
319
+ sql: `
320
+ CREATE OR REPLACE FUNCTION boss_claude.fn_get_user_stats(p_user_id VARCHAR)
321
+ RETURNS TABLE(
322
+ total_sessions BIGINT,
323
+ total_xp BIGINT,
324
+ total_tokens_saved BIGINT,
325
+ total_tasks BIGINT,
326
+ avg_efficiency NUMERIC,
327
+ achievements_count BIGINT,
328
+ last_session_end TIMESTAMPTZ
329
+ ) AS $$
330
+ BEGIN
331
+ RETURN QUERY
332
+ SELECT
333
+ COUNT(DISTINCT s.id)::BIGINT,
334
+ COALESCE(SUM(s.xp_earned), 0)::BIGINT,
335
+ COALESCE(SUM(s.tokens_saved), 0)::BIGINT,
336
+ COALESCE(SUM(s.tasks_completed), 0)::BIGINT,
337
+ ROUND(AVG(s.efficiency_multiplier), 2),
338
+ (SELECT COUNT(*) FROM boss_claude.achievements WHERE user_id = p_user_id)::BIGINT,
339
+ MAX(s.end_time)
340
+ FROM boss_claude.sessions s
341
+ WHERE s.user_id = p_user_id;
342
+ END;
343
+ $$ LANGUAGE plpgsql
344
+ `
345
+ }
346
+ ];
347
+
348
+ for (const func of helperFunctions) {
349
+ try {
350
+ await pool.query(func.sql);
351
+ results.functions.created.push(func.name);
352
+ } catch (error) {
353
+ results.functions.failed.push({ name: func.name, error: error.message });
354
+ }
355
+ }
356
+
357
+ // 8. Create trigger
358
+ try {
359
+ // Drop trigger if exists (for force mode)
360
+ if (force) {
361
+ await pool.query('DROP TRIGGER IF EXISTS sessions_updated_at ON boss_claude.sessions');
362
+ }
363
+
364
+ await pool.query(`
365
+ CREATE TRIGGER sessions_updated_at
366
+ BEFORE UPDATE ON boss_claude.sessions
367
+ FOR EACH ROW
368
+ EXECUTE FUNCTION boss_claude.update_updated_at()
369
+ `);
370
+ results.triggers.created.push('sessions_updated_at');
371
+ } catch (error) {
372
+ if (error.code === '42710') { // trigger already exists
373
+ results.triggers.existed.push('sessions_updated_at');
374
+ } else {
375
+ results.triggers.failed.push({ name: 'sessions_updated_at', error: error.message });
376
+ }
377
+ }
378
+
379
+ // 9. Grant permissions
380
+ try {
381
+ await pool.query('GRANT USAGE ON SCHEMA boss_claude TO postgres');
382
+ await pool.query('GRANT ALL ON ALL TABLES IN SCHEMA boss_claude TO postgres');
383
+ await pool.query('GRANT ALL ON ALL SEQUENCES IN SCHEMA boss_claude TO postgres');
384
+ await pool.query('GRANT ALL ON ALL FUNCTIONS IN SCHEMA boss_claude TO postgres');
385
+ } catch (error) {
386
+ // Permission grants might fail in some environments, but that's okay
387
+ }
388
+
389
+ // 10. Add comments
390
+ try {
391
+ await pool.query(`COMMENT ON SCHEMA boss_claude IS 'Boss Claude AI assistant tracking and analytics system'`);
392
+ await pool.query(`COMMENT ON TABLE boss_claude.sessions IS 'Individual Boss Claude conversation sessions'`);
393
+ await pool.query(`COMMENT ON TABLE boss_claude.achievements IS 'User achievements and milestones'`);
394
+ await pool.query(`COMMENT ON TABLE boss_claude.memory_snapshots IS 'Point-in-time state snapshots'`);
395
+ await pool.query(`COMMENT ON TABLE boss_claude.stats_rollups IS 'Aggregated statistics for analytics'`);
396
+ } catch (error) {
397
+ // Comments are nice-to-have
398
+ }
399
+
400
+ // 11. Health check
401
+ const healthCheckResult = await performHealthCheck(pool);
402
+ results.healthCheck = healthCheckResult;
403
+
404
+ return results;
405
+
406
+ } catch (error) {
407
+ throw new Error(`PostgreSQL initialization failed: ${error.message}`);
408
+ } finally {
409
+ await pool.end();
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Perform comprehensive health check on PostgreSQL schema
415
+ *
416
+ * @param {pg.Pool} pool - PostgreSQL connection pool
417
+ * @returns {Promise<Object>} Health check results
418
+ */
419
+ async function performHealthCheck(pool) {
420
+ const checks = {
421
+ passed: true,
422
+ details: {}
423
+ };
424
+
425
+ try {
426
+ // Check 1: Schema exists
427
+ const schemaResult = await pool.query(
428
+ `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'boss_claude'`
429
+ );
430
+
431
+ if (schemaResult.rows.length === 0) {
432
+ checks.passed = false;
433
+ checks.details.schema = 'Schema boss_claude does not exist';
434
+ } else {
435
+ checks.details.schema = 'OK';
436
+ }
437
+
438
+ // Check 2: All tables exist
439
+ const requiredTables = ['sessions', 'achievements', 'memory_snapshots', 'stats_rollups'];
440
+ const tableResult = await pool.query(
441
+ `SELECT table_name FROM information_schema.tables
442
+ WHERE table_schema = 'boss_claude' AND table_name = ANY($1)`,
443
+ [requiredTables]
444
+ );
445
+
446
+ const foundTables = tableResult.rows.map(r => r.table_name);
447
+ const missingTables = requiredTables.filter(t => !foundTables.includes(t));
448
+
449
+ if (missingTables.length > 0) {
450
+ checks.passed = false;
451
+ checks.details.tables = `Missing tables: ${missingTables.join(', ')}`;
452
+ } else {
453
+ checks.details.tables = 'OK';
454
+ }
455
+
456
+ // Check 3: Functions exist
457
+ const functionResult = await pool.query(
458
+ `SELECT routine_name FROM information_schema.routines
459
+ WHERE routine_schema = 'boss_claude'
460
+ AND routine_name IN ('fn_get_current_session', 'fn_get_user_stats', 'update_updated_at')`
461
+ );
462
+
463
+ if (functionResult.rows.length < 3) {
464
+ checks.passed = false;
465
+ checks.details.functions = `Found ${functionResult.rows.length}/3 required functions`;
466
+ } else {
467
+ checks.details.functions = 'OK';
468
+ }
469
+
470
+ // Check 4: Test write operations
471
+ const testUserId = `health_check_${Date.now()}`;
472
+
473
+ try {
474
+ // Insert test session
475
+ const insertResult = await pool.query(
476
+ `INSERT INTO boss_claude.sessions (user_id, project, level_at_start)
477
+ VALUES ($1, $2, $3) RETURNING id`,
478
+ [testUserId, 'health_check', 0]
479
+ );
480
+
481
+ const sessionId = insertResult.rows[0].id;
482
+
483
+ // Read it back
484
+ const readResult = await pool.query(
485
+ `SELECT * FROM boss_claude.sessions WHERE id = $1`,
486
+ [sessionId]
487
+ );
488
+
489
+ // Delete it
490
+ await pool.query(
491
+ `DELETE FROM boss_claude.sessions WHERE id = $1`,
492
+ [sessionId]
493
+ );
494
+
495
+ if (readResult.rows.length === 1 && readResult.rows[0].user_id === testUserId) {
496
+ checks.details.readWrite = 'OK';
497
+ } else {
498
+ checks.passed = false;
499
+ checks.details.readWrite = 'Read/write test failed';
500
+ }
501
+ } catch (error) {
502
+ checks.passed = false;
503
+ checks.details.readWrite = `Read/write test failed: ${error.message}`;
504
+ }
505
+
506
+ // Check 5: Test helper function
507
+ try {
508
+ const statsResult = await pool.query(
509
+ `SELECT * FROM boss_claude.fn_get_user_stats($1)`,
510
+ ['test_user']
511
+ );
512
+
513
+ if (statsResult.rows.length === 1) {
514
+ checks.details.helperFunctions = 'OK';
515
+ } else {
516
+ checks.passed = false;
517
+ checks.details.helperFunctions = 'Helper function test failed';
518
+ }
519
+ } catch (error) {
520
+ checks.passed = false;
521
+ checks.details.helperFunctions = `Helper function test failed: ${error.message}`;
522
+ }
523
+
524
+ } catch (error) {
525
+ checks.passed = false;
526
+ checks.details.error = error.message;
527
+ }
528
+
529
+ return checks;
530
+ }
531
+
532
+ /**
533
+ * Get current PostgreSQL statistics
534
+ *
535
+ * @param {string} connectionString - PostgreSQL connection string
536
+ * @returns {Promise<Object>} PostgreSQL statistics
537
+ */
538
+ export async function getPostgresStats(connectionString) {
539
+ const pool = new Pool({ connectionString });
540
+
541
+ try {
542
+ const stats = {
543
+ totalSessions: 0,
544
+ activeSessions: 0,
545
+ totalUsers: 0,
546
+ totalAchievements: 0,
547
+ totalSnapshots: 0,
548
+ schemaSize: null,
549
+ oldestSession: null,
550
+ newestSession: null
551
+ };
552
+
553
+ // Count sessions
554
+ const sessionCount = await pool.query(
555
+ `SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE end_time IS NULL) as active
556
+ FROM boss_claude.sessions`
557
+ );
558
+ stats.totalSessions = parseInt(sessionCount.rows[0].total);
559
+ stats.activeSessions = parseInt(sessionCount.rows[0].active);
560
+
561
+ // Count unique users
562
+ const userCount = await pool.query(
563
+ `SELECT COUNT(DISTINCT user_id) as total FROM boss_claude.sessions`
564
+ );
565
+ stats.totalUsers = parseInt(userCount.rows[0].total);
566
+
567
+ // Count achievements
568
+ const achievementCount = await pool.query(
569
+ `SELECT COUNT(*) as total FROM boss_claude.achievements`
570
+ );
571
+ stats.totalAchievements = parseInt(achievementCount.rows[0].total);
572
+
573
+ // Count snapshots
574
+ const snapshotCount = await pool.query(
575
+ `SELECT COUNT(*) as total FROM boss_claude.memory_snapshots`
576
+ );
577
+ stats.totalSnapshots = parseInt(snapshotCount.rows[0].total);
578
+
579
+ // Get schema size
580
+ const sizeResult = await pool.query(
581
+ `SELECT pg_size_pretty(pg_total_relation_size('boss_claude.sessions') +
582
+ pg_total_relation_size('boss_claude.achievements') +
583
+ pg_total_relation_size('boss_claude.memory_snapshots') +
584
+ pg_total_relation_size('boss_claude.stats_rollups')) as size`
585
+ );
586
+ stats.schemaSize = sizeResult.rows[0].size;
587
+
588
+ // Get session date range
589
+ const sessionRange = await pool.query(
590
+ `SELECT MIN(start_time) as oldest, MAX(start_time) as newest
591
+ FROM boss_claude.sessions`
592
+ );
593
+ stats.oldestSession = sessionRange.rows[0].oldest;
594
+ stats.newestSession = sessionRange.rows[0].newest;
595
+
596
+ return stats;
597
+
598
+ } catch (error) {
599
+ throw new Error(`Failed to get PostgreSQL stats: ${error.message}`);
600
+ } finally {
601
+ await pool.end();
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Verify PostgreSQL connection and schema
607
+ *
608
+ * @param {string} connectionString - PostgreSQL connection string
609
+ * @returns {Promise<Object>} Verification results
610
+ */
611
+ export async function verifyPostgres(connectionString) {
612
+ const pool = new Pool({
613
+ connectionString,
614
+ max: 1,
615
+ connectionTimeoutMillis: 5000
616
+ });
617
+
618
+ try {
619
+ // Test connection
620
+ const versionResult = await pool.query('SELECT version()');
621
+ const version = versionResult.rows[0].version;
622
+
623
+ // Check schema
624
+ const schemaResult = await pool.query(
625
+ `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'boss_claude'`
626
+ );
627
+
628
+ // Check tables
629
+ const tableResult = await pool.query(
630
+ `SELECT table_name FROM information_schema.tables
631
+ WHERE table_schema = 'boss_claude'`
632
+ );
633
+
634
+ // Perform health check
635
+ const healthCheck = await performHealthCheck(pool);
636
+
637
+ return {
638
+ connected: true,
639
+ version,
640
+ schemaExists: schemaResult.rows.length > 0,
641
+ tables: tableResult.rows.map(r => r.table_name),
642
+ healthCheck
643
+ };
644
+
645
+ } catch (error) {
646
+ return {
647
+ connected: false,
648
+ error: error.message
649
+ };
650
+ } finally {
651
+ await pool.end();
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Reset PostgreSQL schema to initial state (WARNING: Destructive!)
657
+ *
658
+ * @param {string} connectionString - PostgreSQL connection string
659
+ * @returns {Promise<void>}
660
+ */
661
+ export async function resetPostgres(connectionString) {
662
+ const pool = new Pool({ connectionString });
663
+
664
+ try {
665
+ // Drop schema cascade (removes all objects)
666
+ await pool.query('DROP SCHEMA IF EXISTS boss_claude CASCADE');
667
+
668
+ // Re-initialize
669
+ await pool.end();
670
+ return await initializePostgres(connectionString, true);
671
+
672
+ } catch (error) {
673
+ throw new Error(`Failed to reset PostgreSQL: ${error.message}`);
674
+ } finally {
675
+ if (pool.totalCount > 0) {
676
+ await pool.end();
677
+ }
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Print formatted initialization results
683
+ *
684
+ * @param {Object} results - Results from initializePostgres
685
+ */
686
+ export function printInitResults(results) {
687
+ console.log(chalk.cyan('\nšŸ“¦ PostgreSQL Initialization Results\n'));
688
+
689
+ // Schema
690
+ if (results.schema.created) {
691
+ console.log(chalk.green('āœ“ boss_claude schema') + chalk.dim(' - Created'));
692
+ } else if (results.schema.existed) {
693
+ console.log(chalk.yellow('ā—‹ boss_claude schema') + chalk.dim(' - Already exists'));
694
+ }
695
+
696
+ // Extensions
697
+ if (results.extensions.created) {
698
+ console.log(chalk.green('āœ“ Extensions') + chalk.dim(` - Enabled ${results.extensions.count} extensions`));
699
+ } else if (results.extensions.existed) {
700
+ console.log(chalk.yellow('ā—‹ Extensions') + chalk.dim(' - Already enabled'));
701
+ }
702
+
703
+ // Tables
704
+ console.log(chalk.cyan('\nTables:'));
705
+ results.tables.created.forEach(table => {
706
+ console.log(chalk.green(` āœ“ ${table}`) + chalk.dim(' - Created'));
707
+ });
708
+ results.tables.existed.forEach(table => {
709
+ console.log(chalk.yellow(` ā—‹ ${table}`) + chalk.dim(' - Already exists'));
710
+ });
711
+ results.tables.failed.forEach(failure => {
712
+ console.log(chalk.red(` āœ— ${failure.name}`) + chalk.dim(` - ${failure.error}`));
713
+ });
714
+
715
+ // Indexes
716
+ if (results.indexes.created.length > 0 || results.indexes.existed.length > 0) {
717
+ console.log(chalk.cyan(`\nIndexes: ${results.indexes.created.length + results.indexes.existed.length} total`));
718
+ }
719
+
720
+ // Functions
721
+ console.log(chalk.cyan('\nFunctions:'));
722
+ results.functions.created.forEach(func => {
723
+ console.log(chalk.green(` āœ“ ${func}`) + chalk.dim(' - Created'));
724
+ });
725
+ results.functions.failed.forEach(failure => {
726
+ console.log(chalk.red(` āœ— ${failure.name}`) + chalk.dim(` - ${failure.error}`));
727
+ });
728
+
729
+ // Triggers
730
+ if (results.triggers.created.length > 0) {
731
+ console.log(chalk.cyan('\nTriggers:'));
732
+ results.triggers.created.forEach(trigger => {
733
+ console.log(chalk.green(` āœ“ ${trigger}`) + chalk.dim(' - Created'));
734
+ });
735
+ }
736
+ if (results.triggers.existed.length > 0) {
737
+ results.triggers.existed.forEach(trigger => {
738
+ console.log(chalk.yellow(` ā—‹ ${trigger}`) + chalk.dim(' - Already exists'));
739
+ });
740
+ }
741
+
742
+ // Health Check
743
+ console.log();
744
+ if (results.healthCheck.passed) {
745
+ console.log(chalk.green('āœ“ Health Check Passed') + chalk.dim(' - All structures validated'));
746
+ } else {
747
+ console.log(chalk.red('āœ— Health Check Failed'));
748
+ Object.entries(results.healthCheck.details).forEach(([key, value]) => {
749
+ if (value !== 'OK') {
750
+ console.log(chalk.red(` āœ— ${key}: `) + chalk.dim(value));
751
+ }
752
+ });
753
+ }
754
+
755
+ console.log();
756
+ }
757
+
758
+ /**
759
+ * Integration point for setup wizard
760
+ * Called automatically after PostgreSQL connection is validated
761
+ *
762
+ * @param {string} connectionString - PostgreSQL connection string
763
+ * @returns {Promise<boolean>} Success status
764
+ */
765
+ export async function setupPostgresForWizard(connectionString) {
766
+ try {
767
+ console.log(chalk.cyan('\nšŸ”§ Initializing PostgreSQL schema...\n'));
768
+
769
+ const results = await initializePostgres(connectionString, false);
770
+ printInitResults(results);
771
+
772
+ if (!results.healthCheck.passed) {
773
+ console.log(chalk.yellow('\nāš ļø Warning: Health check failed. Some features may not work correctly.'));
774
+ console.log(chalk.dim('You can try running: boss-claude postgres:reset\n'));
775
+ return false;
776
+ }
777
+
778
+ console.log(chalk.green('āœ“ PostgreSQL initialization complete!\n'));
779
+ return true;
780
+
781
+ } catch (error) {
782
+ console.log(chalk.red(`\nāœ— PostgreSQL initialization failed: ${error.message}\n`));
783
+ return false;
784
+ }
785
+ }