@cpretzinger/boss-claude 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/memory.js ADDED
@@ -0,0 +1,94 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import dotenv from 'dotenv';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { existsSync } from 'fs';
6
+ import os from 'os';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // Load environment variables from ~/.boss-claude/.env
12
+ const envPath = join(os.homedir(), '.boss-claude', '.env');
13
+ if (existsSync(envPath)) {
14
+ dotenv.config({ path: envPath });
15
+ }
16
+
17
+ let octokit = null;
18
+
19
+ function getOctokit() {
20
+ if (!octokit) {
21
+ if (!process.env.GITHUB_TOKEN) {
22
+ throw new Error('GITHUB_TOKEN not found. Please run: boss-claude init');
23
+ }
24
+ octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
25
+ }
26
+ return octokit;
27
+ }
28
+
29
+ export async function saveMemory({ repo_name, summary, content, tags = [] }) {
30
+ const client = getOctokit();
31
+
32
+ const owner = process.env.GITHUB_OWNER || 'cpretzinger';
33
+ const repo = process.env.GITHUB_MEMORY_REPO || 'boss-claude-memory';
34
+
35
+ // Create issue with session data
36
+ const issue = await client.issues.create({
37
+ owner,
38
+ repo,
39
+ title: `[${repo_name}] ${summary}`,
40
+ body: `## Session Summary\n\n${summary}\n\n## Session Data\n\n\`\`\`json\n${content}\n\`\`\``,
41
+ labels: ['session', repo_name, ...tags]
42
+ });
43
+
44
+ return {
45
+ issue_number: issue.data.number,
46
+ url: issue.data.html_url,
47
+ summary,
48
+ tags
49
+ };
50
+ }
51
+
52
+ export async function searchMemory(query, limit = 5) {
53
+ const client = getOctokit();
54
+
55
+ const owner = process.env.GITHUB_OWNER || 'cpretzinger';
56
+ const repo = process.env.GITHUB_MEMORY_REPO || 'boss-claude-memory';
57
+
58
+ // Search issues
59
+ const { data } = await client.search.issuesAndPullRequests({
60
+ q: `repo:${owner}/${repo} ${query} label:session`,
61
+ sort: 'created',
62
+ order: 'desc',
63
+ per_page: limit
64
+ });
65
+
66
+ return data.items.map(issue => ({
67
+ title: issue.title,
68
+ summary: issue.body.split('\n\n')[1] || '',
69
+ url: issue.html_url,
70
+ created_at: issue.created_at,
71
+ labels: issue.labels.map(l => l.name)
72
+ }));
73
+ }
74
+
75
+ export async function getMemoryByIssue(issueNumber) {
76
+ const client = getOctokit();
77
+
78
+ const owner = process.env.GITHUB_OWNER || 'cpretzinger';
79
+ const repo = process.env.GITHUB_MEMORY_REPO || 'boss-claude-memory';
80
+
81
+ const { data } = await client.issues.get({
82
+ owner,
83
+ repo,
84
+ issue_number: issueNumber
85
+ });
86
+
87
+ return {
88
+ title: data.title,
89
+ body: data.body,
90
+ url: data.html_url,
91
+ created_at: data.created_at,
92
+ labels: data.labels.map(l => l.name)
93
+ };
94
+ }
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Boss Claude PostgreSQL Integration
3
+ * Railway Instance: turntable.proxy.rlwy.net:46272
4
+ * Database: railway
5
+ * Schema: boss_claude
6
+ */
7
+
8
+ import pg from 'pg';
9
+ const { Pool } = pg;
10
+
11
+ // Connection pool for optimal performance
12
+ // BOSS_CLAUDE_PG_URL must be set in environment variables
13
+ const pool = new Pool({
14
+ connectionString: process.env.BOSS_CLAUDE_PG_URL,
15
+ max: 10, // Maximum connections
16
+ idleTimeoutMillis: 30000, // Close idle connections after 30s
17
+ connectionTimeoutMillis: 2000,
18
+ // SSL configuration for Railway
19
+ ssl: {
20
+ rejectUnauthorized: false
21
+ }
22
+ });
23
+
24
+ // Handle pool errors
25
+ pool.on('error', (err) => {
26
+ console.error('Unexpected PostgreSQL pool error:', err);
27
+ });
28
+
29
+ /**
30
+ * Session Management
31
+ */
32
+ export const sessions = {
33
+ /**
34
+ * Start a new Boss Claude session
35
+ */
36
+ async start(userId, project, levelAtStart, contextData = {}) {
37
+ const query = `
38
+ INSERT INTO boss_claude.sessions (
39
+ user_id, project, level_at_start, context_data
40
+ ) VALUES ($1, $2, $3, $4)
41
+ RETURNING id, user_id, project, start_time, level_at_start
42
+ `;
43
+
44
+ const result = await pool.query(query, [
45
+ userId,
46
+ project,
47
+ levelAtStart,
48
+ JSON.stringify(contextData)
49
+ ]);
50
+
51
+ return result.rows[0];
52
+ },
53
+
54
+ /**
55
+ * Get current active session for user
56
+ */
57
+ async getCurrent(userId) {
58
+ const query = `
59
+ SELECT * FROM boss_claude.fn_get_current_session($1)
60
+ `;
61
+ const result = await pool.query(query, [userId]);
62
+ return result.rows[0] || null;
63
+ },
64
+
65
+ /**
66
+ * Update session progress
67
+ */
68
+ async updateProgress(sessionId, updates) {
69
+ const {
70
+ xpEarned = 0,
71
+ tokensSaved = 0,
72
+ tasksCompleted = 0,
73
+ perfectExecutions = 0,
74
+ efficiency = null,
75
+ contextData = null
76
+ } = updates;
77
+
78
+ let query = `
79
+ UPDATE boss_claude.sessions
80
+ SET
81
+ xp_earned = xp_earned + $2,
82
+ tokens_saved = tokens_saved + $3,
83
+ tasks_completed = tasks_completed + $4,
84
+ perfect_executions = perfect_executions + $5
85
+ `;
86
+
87
+ const params = [sessionId, xpEarned, tokensSaved, tasksCompleted, perfectExecutions];
88
+ let paramCount = 5;
89
+
90
+ if (efficiency !== null) {
91
+ paramCount++;
92
+ query += `, efficiency_multiplier = $${paramCount}`;
93
+ params.push(efficiency);
94
+ }
95
+
96
+ if (contextData !== null) {
97
+ paramCount++;
98
+ query += `, context_data = $${paramCount}`;
99
+ params.push(JSON.stringify(contextData));
100
+ }
101
+
102
+ query += `
103
+ WHERE id = $1
104
+ RETURNING id, xp_earned, tokens_saved, tasks_completed, perfect_executions, efficiency_multiplier
105
+ `;
106
+
107
+ const result = await pool.query(query, params);
108
+ return result.rows[0];
109
+ },
110
+
111
+ /**
112
+ * End a session
113
+ */
114
+ async end(sessionId, levelAtEnd, summary) {
115
+ const query = `
116
+ UPDATE boss_claude.sessions
117
+ SET
118
+ end_time = NOW(),
119
+ level_at_end = $2,
120
+ summary = $3
121
+ WHERE id = $1
122
+ RETURNING id, start_time, end_time, duration_seconds, xp_earned, tokens_saved
123
+ `;
124
+
125
+ const result = await pool.query(query, [sessionId, levelAtEnd, summary]);
126
+ return result.rows[0];
127
+ },
128
+
129
+ /**
130
+ * Get recent sessions
131
+ */
132
+ async getRecent(userId, limit = 10) {
133
+ const query = `
134
+ SELECT
135
+ id,
136
+ project,
137
+ start_time,
138
+ end_time,
139
+ duration_seconds,
140
+ xp_earned,
141
+ tokens_saved,
142
+ tasks_completed,
143
+ efficiency_multiplier,
144
+ summary
145
+ FROM boss_claude.sessions
146
+ WHERE user_id = $1
147
+ ORDER BY start_time DESC
148
+ LIMIT $2
149
+ `;
150
+
151
+ const result = await pool.query(query, [userId, limit]);
152
+ return result.rows;
153
+ }
154
+ };
155
+
156
+ /**
157
+ * Achievement Management
158
+ */
159
+ export const achievements = {
160
+ /**
161
+ * Award an achievement
162
+ */
163
+ async award(userId, type, name, description, xpReward, metadata = {}) {
164
+ const query = `
165
+ INSERT INTO boss_claude.achievements (
166
+ user_id, achievement_type, achievement_name, description, xp_reward, metadata
167
+ ) VALUES ($1, $2, $3, $4, $5, $6)
168
+ RETURNING id, achievement_name, xp_reward, earned_at
169
+ `;
170
+
171
+ const result = await pool.query(query, [
172
+ userId,
173
+ type,
174
+ name,
175
+ description,
176
+ xpReward,
177
+ JSON.stringify(metadata)
178
+ ]);
179
+
180
+ return result.rows[0];
181
+ },
182
+
183
+ /**
184
+ * Get user achievements
185
+ */
186
+ async getAll(userId, limit = 50) {
187
+ const query = `
188
+ SELECT
189
+ id,
190
+ achievement_type,
191
+ achievement_name,
192
+ description,
193
+ xp_reward,
194
+ metadata,
195
+ earned_at
196
+ FROM boss_claude.achievements
197
+ WHERE user_id = $1
198
+ ORDER BY earned_at DESC
199
+ LIMIT $2
200
+ `;
201
+
202
+ const result = await pool.query(query, [userId, limit]);
203
+ return result.rows;
204
+ },
205
+
206
+ /**
207
+ * Check if user has specific achievement
208
+ */
209
+ async has(userId, achievementType) {
210
+ const query = `
211
+ SELECT EXISTS(
212
+ SELECT 1 FROM boss_claude.achievements
213
+ WHERE user_id = $1 AND achievement_type = $2
214
+ ) as has_achievement
215
+ `;
216
+
217
+ const result = await pool.query(query, [userId, achievementType]);
218
+ return result.rows[0].has_achievement;
219
+ }
220
+ };
221
+
222
+ /**
223
+ * Memory Snapshot Management
224
+ */
225
+ export const snapshots = {
226
+ /**
227
+ * Create a memory snapshot
228
+ */
229
+ async create(userId, sessionId, type, data) {
230
+ const { level, tokenBank, totalXp, efficiency, snapshotData } = data;
231
+
232
+ const query = `
233
+ INSERT INTO boss_claude.memory_snapshots (
234
+ user_id, session_id, snapshot_type, snapshot_data,
235
+ level, token_bank, total_xp, efficiency
236
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
237
+ RETURNING id, snapshot_type, created_at
238
+ `;
239
+
240
+ const result = await pool.query(query, [
241
+ userId,
242
+ sessionId,
243
+ type,
244
+ JSON.stringify(snapshotData),
245
+ level,
246
+ tokenBank,
247
+ totalXp,
248
+ efficiency
249
+ ]);
250
+
251
+ return result.rows[0];
252
+ },
253
+
254
+ /**
255
+ * Get latest snapshot
256
+ */
257
+ async getLatest(userId) {
258
+ const query = `
259
+ SELECT
260
+ id,
261
+ snapshot_type,
262
+ snapshot_data,
263
+ level,
264
+ token_bank,
265
+ total_xp,
266
+ efficiency,
267
+ created_at
268
+ FROM boss_claude.memory_snapshots
269
+ WHERE user_id = $1
270
+ ORDER BY created_at DESC
271
+ LIMIT 1
272
+ `;
273
+
274
+ const result = await pool.query(query, [userId]);
275
+ return result.rows[0] || null;
276
+ }
277
+ };
278
+
279
+ /**
280
+ * Stats and Analytics
281
+ */
282
+ export const stats = {
283
+ /**
284
+ * Get user statistics summary
285
+ */
286
+ async getSummary(userId) {
287
+ const query = `SELECT * FROM boss_claude.fn_get_user_stats($1)`;
288
+ const result = await pool.query(query, [userId]);
289
+ return result.rows[0];
290
+ },
291
+
292
+ /**
293
+ * Get session activity over time
294
+ */
295
+ async getActivity(userId, days = 30) {
296
+ const query = `
297
+ SELECT
298
+ DATE(start_time) as date,
299
+ COUNT(*) as sessions,
300
+ SUM(xp_earned) as total_xp,
301
+ SUM(tokens_saved) as total_tokens,
302
+ ROUND(AVG(efficiency_multiplier), 2) as avg_efficiency
303
+ FROM boss_claude.sessions
304
+ WHERE user_id = $1
305
+ AND start_time >= NOW() - INTERVAL '1 day' * $2
306
+ GROUP BY DATE(start_time)
307
+ ORDER BY date DESC
308
+ `;
309
+
310
+ const result = await pool.query(query, [userId, days]);
311
+ return result.rows;
312
+ },
313
+
314
+ /**
315
+ * Get top performing sessions
316
+ */
317
+ async getTopSessions(userId, limit = 5) {
318
+ const query = `
319
+ SELECT
320
+ project,
321
+ start_time,
322
+ xp_earned,
323
+ tokens_saved,
324
+ efficiency_multiplier,
325
+ tasks_completed,
326
+ summary
327
+ FROM boss_claude.sessions
328
+ WHERE user_id = $1
329
+ AND end_time IS NOT NULL
330
+ ORDER BY xp_earned DESC
331
+ LIMIT $2
332
+ `;
333
+
334
+ const result = await pool.query(query, [userId, limit]);
335
+ return result.rows;
336
+ }
337
+ };
338
+
339
+ /**
340
+ * Utility functions
341
+ */
342
+ export const utils = {
343
+ /**
344
+ * Test database connection
345
+ */
346
+ async testConnection() {
347
+ try {
348
+ const result = await pool.query('SELECT NOW() as current_time, version()');
349
+ return {
350
+ connected: true,
351
+ timestamp: result.rows[0].current_time,
352
+ version: result.rows[0].version
353
+ };
354
+ } catch (error) {
355
+ return {
356
+ connected: false,
357
+ error: error.message
358
+ };
359
+ }
360
+ },
361
+
362
+ /**
363
+ * Get database stats
364
+ */
365
+ async getDatabaseStats() {
366
+ const query = `
367
+ SELECT
368
+ schemaname,
369
+ tablename,
370
+ pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size,
371
+ (SELECT COUNT(*) FROM boss_claude.sessions WHERE tablename = 'sessions') as sessions_count,
372
+ (SELECT COUNT(*) FROM boss_claude.achievements WHERE tablename = 'achievements') as achievements_count,
373
+ (SELECT COUNT(*) FROM boss_claude.memory_snapshots WHERE tablename = 'memory_snapshots') as snapshots_count
374
+ FROM pg_tables
375
+ WHERE schemaname = 'boss_claude'
376
+ ORDER BY tablename
377
+ `;
378
+
379
+ const result = await pool.query(query);
380
+ return result.rows;
381
+ },
382
+
383
+ /**
384
+ * Close connection pool (for graceful shutdown)
385
+ */
386
+ async close() {
387
+ await pool.end();
388
+ }
389
+ };
390
+
391
+ export default {
392
+ sessions,
393
+ achievements,
394
+ snapshots,
395
+ stats,
396
+ utils,
397
+ pool
398
+ };
package/lib/session.js ADDED
@@ -0,0 +1,158 @@
1
+ import Redis from 'ioredis';
2
+ import dotenv from 'dotenv';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { existsSync } from 'fs';
6
+ import os from 'os';
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import { saveMemory } from './memory.js';
10
+ import { addXP, addTokens, incrementSessions } from './identity.js';
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ // Load environment variables from ~/.boss-claude/.env
18
+ const envPath = join(os.homedir(), '.boss-claude', '.env');
19
+ if (existsSync(envPath)) {
20
+ dotenv.config({ path: envPath });
21
+ }
22
+
23
+ let redis = null;
24
+
25
+ function getRedis() {
26
+ if (!redis) {
27
+ if (!process.env.REDIS_URL) {
28
+ throw new Error('REDIS_URL not found. Please run: boss-claude init');
29
+ }
30
+ redis = new Redis(process.env.REDIS_URL);
31
+ }
32
+ return redis;
33
+ }
34
+
35
+ async function getCurrentRepo() {
36
+ try {
37
+ const { stdout: repoPath } = await execAsync('git rev-parse --show-toplevel');
38
+ const { stdout: repoUrl } = await execAsync('git config --get remote.origin.url');
39
+
40
+ const repoName = repoUrl.trim().split('/').pop().replace('.git', '');
41
+
42
+ return {
43
+ name: repoName,
44
+ path: repoPath.trim(),
45
+ url: repoUrl.trim()
46
+ };
47
+ } catch (error) {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ export async function loadSession() {
53
+ const repo = await getCurrentRepo();
54
+
55
+ if (!repo) {
56
+ return null;
57
+ }
58
+
59
+ const client = getRedis();
60
+ const sessionKey = `boss:session:${repo.name}:current`;
61
+
62
+ const data = await client.get(sessionKey);
63
+
64
+ if (!data) {
65
+ return {
66
+ repo,
67
+ started_at: new Date().toISOString(),
68
+ messages: [],
69
+ tokens_used: 0
70
+ };
71
+ }
72
+
73
+ return {
74
+ ...JSON.parse(data),
75
+ repo
76
+ };
77
+ }
78
+
79
+ export async function saveSession(summary, tags) {
80
+ const repo = await getCurrentRepo();
81
+
82
+ if (!repo) {
83
+ throw new Error('Not in a git repository');
84
+ }
85
+
86
+ const client = getRedis();
87
+ const sessionKey = `boss:session:${repo.name}:current`;
88
+
89
+ const sessionData = await client.get(sessionKey);
90
+ const session = sessionData ? JSON.parse(sessionData) : {
91
+ started_at: new Date().toISOString(),
92
+ messages: [],
93
+ tokens_used: 0
94
+ };
95
+
96
+ // Save to GitHub Issues
97
+ const memory = await saveMemory({
98
+ repo_name: repo.name,
99
+ summary: summary || 'Session saved',
100
+ content: JSON.stringify(session, null, 2),
101
+ tags: tags ? tags.split(',').map(t => t.trim()) : []
102
+ });
103
+
104
+ // Calculate rewards
105
+ const xpEarned = 50; // Base XP per session
106
+ const tokensEarned = session.tokens_used || 0;
107
+
108
+ await addXP(xpEarned);
109
+ await addTokens(tokensEarned);
110
+ await incrementSessions();
111
+
112
+ // Update repo stats
113
+ const repoKey = `boss:repo:${repo.name}`;
114
+ const repoData = await client.get(repoKey);
115
+ const repoStats = repoData ? JSON.parse(repoData) : {
116
+ name: repo.name,
117
+ path: repo.path,
118
+ session_count: 0,
119
+ first_seen: new Date().toISOString()
120
+ };
121
+
122
+ repoStats.session_count++;
123
+ repoStats.last_active = new Date().toISOString();
124
+
125
+ await client.set(repoKey, JSON.stringify(repoStats));
126
+
127
+ // Clear current session
128
+ await client.del(sessionKey);
129
+
130
+ return {
131
+ ...memory,
132
+ repo_name: repo.name,
133
+ xp_earned: xpEarned,
134
+ tokens_earned: tokensEarned
135
+ };
136
+ }
137
+
138
+ export async function updateSessionTokens(tokens) {
139
+ const repo = await getCurrentRepo();
140
+
141
+ if (!repo) {
142
+ return;
143
+ }
144
+
145
+ const client = getRedis();
146
+ const sessionKey = `boss:session:${repo.name}:current`;
147
+
148
+ const data = await client.get(sessionKey);
149
+ const session = data ? JSON.parse(data) : {
150
+ started_at: new Date().toISOString(),
151
+ messages: [],
152
+ tokens_used: 0
153
+ };
154
+
155
+ session.tokens_used = (session.tokens_used || 0) + tokens;
156
+
157
+ await client.set(sessionKey, JSON.stringify(session));
158
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@cpretzinger/boss-claude",
3
+ "version": "1.0.0",
4
+ "description": "Boss Claude - Gamified AI assistant with persistent memory across all repos",
5
+ "type": "module",
6
+ "bin": {
7
+ "boss-claude": "bin/boss-claude.js"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node scripts/install.js"
11
+ },
12
+ "keywords": [
13
+ "claude",
14
+ "ai",
15
+ "assistant",
16
+ "cli",
17
+ "memory",
18
+ "gamification"
19
+ ],
20
+ "author": "Craig Pretzinger <craig@example.com>",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "ioredis": "^5.3.2",
24
+ "@octokit/rest": "^20.0.2",
25
+ "pg": "^8.11.3",
26
+ "dotenv": "^16.3.1",
27
+ "commander": "^11.1.0",
28
+ "chalk": "^5.3.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/cpretzinger/boss-claude.git"
36
+ }
37
+ }