@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,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
|
+
}
|