@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/LICENSE +21 -0
- package/README.md +264 -0
- package/bin/boss-claude.js +150 -0
- package/lib/identity.js +115 -0
- package/lib/init.js +133 -0
- package/lib/memory.js +94 -0
- package/lib/postgres.js +398 -0
- package/lib/session.js +158 -0
- package/package.json +37 -0
- package/scripts/install.js +82 -0
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
|
+
}
|
package/lib/postgres.js
ADDED
|
@@ -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
|
+
}
|