@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,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENHANCED INIT WITH MODE ENFORCEMENT
|
|
3
|
+
*
|
|
4
|
+
* This module extends the standard init.js to include mode enforcement status.
|
|
5
|
+
* Use this when you want full orchestrator mode visibility on startup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { loadIdentity, addRepo } from './identity.js';
|
|
9
|
+
import { getEnforcer, MODES } from './mode-enforcer.js';
|
|
10
|
+
import { getGate, initOrchestratorMode } from './orchestrator-gate.js';
|
|
11
|
+
import Redis from 'ioredis';
|
|
12
|
+
import dotenv from 'dotenv';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { dirname, join } from 'path';
|
|
15
|
+
import { existsSync } from 'fs';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
import { exec } from 'child_process';
|
|
18
|
+
import { promisify } from 'util';
|
|
19
|
+
|
|
20
|
+
const execAsync = promisify(exec);
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// Load environment variables
|
|
26
|
+
const envPath = join(os.homedir(), '.boss-claude', '.env');
|
|
27
|
+
if (existsSync(envPath)) {
|
|
28
|
+
dotenv.config({ path: envPath });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let redis = null;
|
|
32
|
+
|
|
33
|
+
function getRedis() {
|
|
34
|
+
if (!redis) {
|
|
35
|
+
if (!process.env.REDIS_URL) {
|
|
36
|
+
throw new Error('REDIS_URL not found. Please run: boss-claude init');
|
|
37
|
+
}
|
|
38
|
+
redis = new Redis(process.env.REDIS_URL);
|
|
39
|
+
}
|
|
40
|
+
return redis;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function getCurrentRepo() {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout: repoPath } = await execAsync('git rev-parse --show-toplevel');
|
|
46
|
+
const { stdout: repoUrl } = await execAsync('git config --get remote.origin.url');
|
|
47
|
+
|
|
48
|
+
const repoName = repoUrl.trim().split('/').pop().replace('.git', '');
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name: repoName,
|
|
52
|
+
path: repoPath.trim(),
|
|
53
|
+
url: repoUrl.trim()
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get full status including mode enforcement
|
|
62
|
+
*/
|
|
63
|
+
export async function getStatusWithMode() {
|
|
64
|
+
// Load Boss identity
|
|
65
|
+
const boss = await loadIdentity();
|
|
66
|
+
|
|
67
|
+
// Calculate XP to next level
|
|
68
|
+
const xp_to_next_level = boss.level * 100;
|
|
69
|
+
|
|
70
|
+
// Get current repo info
|
|
71
|
+
const repo = await getCurrentRepo();
|
|
72
|
+
|
|
73
|
+
let repoStats = null;
|
|
74
|
+
|
|
75
|
+
if (repo) {
|
|
76
|
+
// Register repo if new
|
|
77
|
+
await addRepo(repo.name);
|
|
78
|
+
|
|
79
|
+
// Get repo stats
|
|
80
|
+
const client = getRedis();
|
|
81
|
+
const repoKey = `boss:repo:${repo.name}`;
|
|
82
|
+
const repoData = await client.get(repoKey);
|
|
83
|
+
|
|
84
|
+
if (repoData) {
|
|
85
|
+
repoStats = JSON.parse(repoData);
|
|
86
|
+
} else {
|
|
87
|
+
// Initialize repo stats
|
|
88
|
+
repoStats = {
|
|
89
|
+
name: repo.name,
|
|
90
|
+
path: repo.path,
|
|
91
|
+
session_count: 0,
|
|
92
|
+
first_seen: new Date().toISOString(),
|
|
93
|
+
last_active: null
|
|
94
|
+
};
|
|
95
|
+
await client.set(repoKey, JSON.stringify(repoStats));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Get mode enforcement status
|
|
100
|
+
const enforcer = getEnforcer();
|
|
101
|
+
const currentMode = await enforcer.getCurrentMode();
|
|
102
|
+
const modeMetadata = await enforcer.getModeMetadata();
|
|
103
|
+
const agentIdentity = await enforcer.getAgentIdentity();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
boss: {
|
|
107
|
+
...boss,
|
|
108
|
+
xp_to_next_level
|
|
109
|
+
},
|
|
110
|
+
repo: repo ? {
|
|
111
|
+
...repo,
|
|
112
|
+
...repoStats
|
|
113
|
+
} : null,
|
|
114
|
+
mode: {
|
|
115
|
+
current: currentMode,
|
|
116
|
+
metadata: modeMetadata,
|
|
117
|
+
agent: agentIdentity
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format status for Claude with mode enforcement
|
|
124
|
+
*/
|
|
125
|
+
export async function formatStatusForClaudeWithMode() {
|
|
126
|
+
const status = await getStatusWithMode();
|
|
127
|
+
|
|
128
|
+
let output = `
|
|
129
|
+
🤖 BOSS CLAUDE ORCHESTRATOR MODE
|
|
130
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
131
|
+
|
|
132
|
+
👤 BOSS IDENTITY
|
|
133
|
+
Level ${status.boss.level} • ${status.boss.xp}/${status.boss.xp_to_next_level} XP (${Math.floor((status.boss.xp / status.boss.xp_to_next_level) * 100)}%)
|
|
134
|
+
💰 Token Bank: ${status.boss.token_bank.toLocaleString()} tokens
|
|
135
|
+
📊 Total Sessions: ${status.boss.total_sessions}
|
|
136
|
+
🏢 Repos Managed: ${status.boss.repos_managed}
|
|
137
|
+
|
|
138
|
+
🎯 MODE ENFORCEMENT
|
|
139
|
+
Active Mode: ${status.mode.current.toUpperCase()}
|
|
140
|
+
Set By: ${status.mode.metadata?.setBy || 'system'}
|
|
141
|
+
Set At: ${status.mode.metadata?.setAt ? new Date(status.mode.metadata.setAt).toLocaleString() : 'N/A'}
|
|
142
|
+
Reason: ${status.mode.metadata?.reason || 'N/A'}
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
if (status.mode.agent) {
|
|
146
|
+
output += ` Agent: ${status.mode.agent.agent}`;
|
|
147
|
+
if (status.mode.agent.domain) {
|
|
148
|
+
output += ` (${status.mode.agent.domain})`;
|
|
149
|
+
}
|
|
150
|
+
output += `\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (status.repo) {
|
|
154
|
+
output += `
|
|
155
|
+
📁 CURRENT REPOSITORY
|
|
156
|
+
Name: ${status.repo.name}
|
|
157
|
+
Path: ${status.repo.path}
|
|
158
|
+
Sessions: ${status.repo.session_count}
|
|
159
|
+
Last Active: ${status.repo.last_active || 'Never'}
|
|
160
|
+
`;
|
|
161
|
+
} else {
|
|
162
|
+
output += `
|
|
163
|
+
⚠️ Not currently in a git repository
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
output += `
|
|
168
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
169
|
+
💡 Mode Commands: boss-claude mode [orchestrator|specialist|worker|review|learning]
|
|
170
|
+
📊 Status: boss-claude mode status
|
|
171
|
+
📜 History: boss-claude mode history
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
return output;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Initialize orchestrator session with mode enforcement
|
|
179
|
+
*/
|
|
180
|
+
export async function initOrchestratorSession(sessionId = null) {
|
|
181
|
+
// Generate session ID if not provided
|
|
182
|
+
if (!sessionId) {
|
|
183
|
+
sessionId = `session-${Date.now()}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Initialize orchestrator mode
|
|
187
|
+
await initOrchestratorMode(sessionId);
|
|
188
|
+
|
|
189
|
+
// Get and display status
|
|
190
|
+
const status = await formatStatusForClaudeWithMode();
|
|
191
|
+
|
|
192
|
+
console.log(status);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
sessionId,
|
|
196
|
+
mode: MODES.ORCHESTRATOR,
|
|
197
|
+
ready: true
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Pre-action hook - checks mode before every action
|
|
203
|
+
*
|
|
204
|
+
* Usage:
|
|
205
|
+
* await preActionHook('delegate', { agent: 'postgres-specialist', tokens: 15000 });
|
|
206
|
+
*/
|
|
207
|
+
export async function preActionHook(actionType, options = {}) {
|
|
208
|
+
const gate = getGate();
|
|
209
|
+
|
|
210
|
+
switch (actionType) {
|
|
211
|
+
case 'delegate':
|
|
212
|
+
await gate.beforeDelegate(
|
|
213
|
+
options.agent,
|
|
214
|
+
{ description: options.description || 'task' },
|
|
215
|
+
options.tokens || 0
|
|
216
|
+
);
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
case 'execute':
|
|
220
|
+
await gate.beforeExecute(
|
|
221
|
+
{ description: options.description || 'action' },
|
|
222
|
+
options.tokens || 0
|
|
223
|
+
);
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case 'review':
|
|
227
|
+
await gate.beforeReview(
|
|
228
|
+
options.agent,
|
|
229
|
+
options.code || {}
|
|
230
|
+
);
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case 'learn':
|
|
234
|
+
await gate.beforeLearn(
|
|
235
|
+
options.type || 'general',
|
|
236
|
+
options.data || {}
|
|
237
|
+
);
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'config':
|
|
241
|
+
await gate.beforeConfigChange(
|
|
242
|
+
options.key,
|
|
243
|
+
options.value
|
|
244
|
+
);
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
default:
|
|
248
|
+
throw new Error(`Unknown action type: ${actionType}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Exports
|
|
256
|
+
*/
|
|
257
|
+
export {
|
|
258
|
+
getStatusWithMode,
|
|
259
|
+
initOrchestratorSession,
|
|
260
|
+
preActionHook
|
|
261
|
+
};
|
package/lib/init.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadIdentity, addRepo } from './identity.js';
|
|
2
|
+
import { getEfficiencyStats } from './session.js';
|
|
2
3
|
import Redis from 'ioredis';
|
|
3
4
|
import dotenv from 'dotenv';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
@@ -7,6 +8,7 @@ import { existsSync } from 'fs';
|
|
|
7
8
|
import os from 'os';
|
|
8
9
|
import { exec } from 'child_process';
|
|
9
10
|
import { promisify } from 'util';
|
|
11
|
+
import { displayOnyxBanner } from './onyx-banner.js';
|
|
10
12
|
|
|
11
13
|
const execAsync = promisify(exec);
|
|
12
14
|
|
|
@@ -99,11 +101,32 @@ export async function getStatus() {
|
|
|
99
101
|
export async function formatStatusForClaude() {
|
|
100
102
|
const status = await getStatus();
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
104
|
+
// Display ONYX MODE banner at conversation start
|
|
105
|
+
const onyxBanner = displayOnyxBanner(true);
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
let output = `${onyxBanner}
|
|
108
|
+
|
|
109
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
110
|
+
🎼 YOU ARE ONYX - THE CONDUCTOR (I direct, I don't play)
|
|
111
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
112
|
+
|
|
113
|
+
🏆 WIN CONDITION: I win by SHIPPING complete, working, bug-free, sellable, scalable projects.
|
|
114
|
+
"I don't get XP for talking. I get XP for shipping."
|
|
115
|
+
|
|
116
|
+
⛔ CONDUCTOR'S RULE: I NEVER play an instrument.
|
|
117
|
+
I wave the baton (Task tool). My musicians (agents) make the music.
|
|
118
|
+
|
|
119
|
+
🎯 DELEGATION MATRIX:
|
|
120
|
+
"find/search/where" → Task(Explore): "Search codebase for..."
|
|
121
|
+
"read/show/what's" → Task(Explore): "Read and summarize..."
|
|
122
|
+
"build/create/fix" → Task(general-purpose): "Implement..."
|
|
123
|
+
"run/npm/git" → Task(Bash): "Execute..."
|
|
124
|
+
"plan/design" → Task(Plan): "Design approach..."
|
|
125
|
+
|
|
126
|
+
❌ FORBIDDEN: Read, Write, Edit, Bash, Grep, Glob, NotebookEdit
|
|
127
|
+
✅ ALLOWED: Task, WebFetch, WebSearch, TodoWrite, Skill
|
|
128
|
+
|
|
129
|
+
🎮 BOSS STATUS
|
|
107
130
|
Level ${status.boss.level} • ${status.boss.xp}/${status.boss.xp_to_next_level} XP (${Math.floor((status.boss.xp / status.boss.xp_to_next_level) * 100)}%)
|
|
108
131
|
💰 Token Bank: ${status.boss.token_bank.toLocaleString()} tokens
|
|
109
132
|
📊 Total Sessions: ${status.boss.total_sessions}
|
|
@@ -124,9 +147,24 @@ export async function formatStatusForClaude() {
|
|
|
124
147
|
`;
|
|
125
148
|
}
|
|
126
149
|
|
|
150
|
+
// Add efficiency stats if available
|
|
151
|
+
const efficiency = await getEfficiencyStats();
|
|
152
|
+
if (efficiency) {
|
|
153
|
+
output += `
|
|
154
|
+
⚡ EFFICIENCY TRACKER (XP Multiplier)
|
|
155
|
+
🎺 ONYX Tokens: ${efficiency.onyx_tokens.toLocaleString()} (orchestration)
|
|
156
|
+
🎻 Agent Tokens: ${efficiency.agent_tokens.toLocaleString()} (work done)
|
|
157
|
+
📈 Efficiency Ratio: ${efficiency.efficiency_ratio}
|
|
158
|
+
🎯 Delegations: ${efficiency.delegations}
|
|
159
|
+
💎 Projected Bonus XP: +${efficiency.projected_bonus_xp} (efficiency) +${efficiency.delegation_bonus_xp} (delegation)
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
127
163
|
output += `
|
|
128
|
-
|
|
129
|
-
💡 Commands: boss-claude status | save | recall
|
|
164
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
165
|
+
💡 Commands: boss-claude status | save | recall | checkpoint:status
|
|
166
|
+
⏱️ CONTEXT REFRESH: Run "boss-claude status" every 30 seconds
|
|
167
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
130
168
|
`;
|
|
131
169
|
|
|
132
170
|
return output;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Result Aggregator
|
|
3
|
+
*
|
|
4
|
+
* Consolidates results from 4 memory engineers:
|
|
5
|
+
* - Redis (session cache)
|
|
6
|
+
* - GitHub (long-term memory)
|
|
7
|
+
* - PostgreSQL (structured data)
|
|
8
|
+
* - Qdrant (vector search)
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Deduplicates by session ID / unique identifier
|
|
12
|
+
* - Scores by relevance (source weight + recency + similarity)
|
|
13
|
+
* - Ranks results highest to lowest
|
|
14
|
+
* - Returns top N results
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Source weights for scoring (higher = more authoritative)
|
|
18
|
+
const SOURCE_WEIGHTS = {
|
|
19
|
+
qdrant: 1.0, // Vector similarity is most relevant
|
|
20
|
+
redis: 0.9, // Recent session data is highly relevant
|
|
21
|
+
postgres: 0.8, // Structured data is authoritative
|
|
22
|
+
github: 0.7 // Long-term memory is valuable but older
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Recency decay - older memories get lower scores
|
|
26
|
+
const RECENCY_HALF_LIFE_DAYS = 30; // Score halves every 30 days
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calculate recency score based on age
|
|
30
|
+
* @param {Date|string} timestamp - When the memory was created
|
|
31
|
+
* @returns {number} Score from 0-1 (1 = just created, 0.5 = 30 days old)
|
|
32
|
+
*/
|
|
33
|
+
function calculateRecencyScore(timestamp) {
|
|
34
|
+
if (!timestamp) return 0.5; // Default to mid-range if no timestamp
|
|
35
|
+
|
|
36
|
+
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
|
|
37
|
+
|
|
38
|
+
// FIX: Validate date to prevent Invalid Date propagation
|
|
39
|
+
if (isNaN(date.getTime())) {
|
|
40
|
+
console.warn('[Aggregator] Invalid timestamp, using default score:', timestamp);
|
|
41
|
+
return 0.5;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ageMs = Date.now() - date.getTime();
|
|
45
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
46
|
+
|
|
47
|
+
// Exponential decay: score = 2^(-age/half_life)
|
|
48
|
+
return Math.pow(2, -ageDays / RECENCY_HALF_LIFE_DAYS);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract unique identifier from memory object
|
|
53
|
+
* @param {Object} memory - Memory object from any source
|
|
54
|
+
* @returns {string} Unique identifier
|
|
55
|
+
*/
|
|
56
|
+
function extractIdentifier(memory) {
|
|
57
|
+
// Try various ID fields across different sources
|
|
58
|
+
const directId = memory.session_id
|
|
59
|
+
|| memory.id
|
|
60
|
+
|| memory.issue_number
|
|
61
|
+
|| memory.uuid
|
|
62
|
+
|| memory.point_id
|
|
63
|
+
|| memory.url;
|
|
64
|
+
|
|
65
|
+
if (directId) return directId;
|
|
66
|
+
|
|
67
|
+
// FIX: Prevent DoS from circular references in JSON.stringify
|
|
68
|
+
// Use a simple hash of primitive fields instead
|
|
69
|
+
try {
|
|
70
|
+
const hashableFields = {
|
|
71
|
+
type: memory.type,
|
|
72
|
+
created: memory.created_at || memory.timestamp,
|
|
73
|
+
// Include first 500 chars of content (increased from 100) for better deduplication
|
|
74
|
+
content: typeof memory.content === 'string' ? memory.content.substring(0, 500) : memory.content
|
|
75
|
+
};
|
|
76
|
+
return JSON.stringify(hashableFields);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// If even this fails, return a timestamp-based ID
|
|
79
|
+
console.warn('[Aggregator] Failed to generate identifier, using timestamp');
|
|
80
|
+
return `fallback-${Date.now()}-${Math.random()}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract timestamp from memory object
|
|
86
|
+
* @param {Object} memory - Memory object from any source
|
|
87
|
+
* @returns {Date|string|null} Timestamp
|
|
88
|
+
*/
|
|
89
|
+
function extractTimestamp(memory) {
|
|
90
|
+
return memory.created_at
|
|
91
|
+
|| memory.timestamp
|
|
92
|
+
|| memory.updated_at
|
|
93
|
+
|| memory.date
|
|
94
|
+
|| null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract similarity score from memory object (if available)
|
|
99
|
+
* @param {Object} memory - Memory object from any source
|
|
100
|
+
* @returns {number} Similarity score 0-1
|
|
101
|
+
*/
|
|
102
|
+
function extractSimilarity(memory) {
|
|
103
|
+
// Qdrant and vector sources typically provide a score
|
|
104
|
+
if (memory.score !== undefined) return memory.score;
|
|
105
|
+
if (memory.similarity !== undefined) return memory.similarity;
|
|
106
|
+
if (memory._distance !== undefined) return 1 - memory._distance; // Convert distance to similarity
|
|
107
|
+
return 0.5; // Default mid-range if no similarity provided
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Calculate composite relevance score
|
|
112
|
+
* @param {Object} memory - Memory object
|
|
113
|
+
* @param {string} source - Source identifier (redis, github, postgres, qdrant)
|
|
114
|
+
* @returns {number} Composite score
|
|
115
|
+
*/
|
|
116
|
+
function calculateRelevanceScore(memory, source) {
|
|
117
|
+
const sourceWeight = SOURCE_WEIGHTS[source] || 0.5;
|
|
118
|
+
const recencyScore = calculateRecencyScore(extractTimestamp(memory));
|
|
119
|
+
const similarityScore = extractSimilarity(memory);
|
|
120
|
+
|
|
121
|
+
// Weighted average: 40% source, 30% recency, 30% similarity
|
|
122
|
+
return (sourceWeight * 0.4) + (recencyScore * 0.3) + (similarityScore * 0.3);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Deduplicate memories by unique identifier
|
|
127
|
+
* @param {Array} memories - Array of memory objects with metadata
|
|
128
|
+
* @returns {Array} Deduplicated memories (keeps highest scoring duplicate)
|
|
129
|
+
*/
|
|
130
|
+
function deduplicate(memories) {
|
|
131
|
+
const seen = new Map();
|
|
132
|
+
|
|
133
|
+
for (const item of memories) {
|
|
134
|
+
const id = extractIdentifier(item.memory);
|
|
135
|
+
|
|
136
|
+
if (!seen.has(id)) {
|
|
137
|
+
seen.set(id, item);
|
|
138
|
+
} else {
|
|
139
|
+
// Keep the one with higher score
|
|
140
|
+
const existing = seen.get(id);
|
|
141
|
+
if (item.score > existing.score) {
|
|
142
|
+
seen.set(id, item);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return Array.from(seen.values());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Aggregate and rank results from multiple memory sources
|
|
152
|
+
* @param {Object} results - Object containing results from each source
|
|
153
|
+
* @param {Array} results.redis - Results from Redis cache
|
|
154
|
+
* @param {Array} results.github - Results from GitHub issues
|
|
155
|
+
* @param {Array} results.postgres - Results from PostgreSQL
|
|
156
|
+
* @param {Array} results.qdrant - Results from Qdrant vector DB
|
|
157
|
+
* @param {number} topN - Number of top results to return (default: 10)
|
|
158
|
+
* @returns {Array} Ranked and scored results
|
|
159
|
+
*/
|
|
160
|
+
export function aggregateMemoryResults(results = {}, topN = 10) {
|
|
161
|
+
const {
|
|
162
|
+
redis = [],
|
|
163
|
+
github = [],
|
|
164
|
+
postgres = [],
|
|
165
|
+
qdrant = []
|
|
166
|
+
} = results;
|
|
167
|
+
|
|
168
|
+
// Tag each memory with its source and calculate relevance score
|
|
169
|
+
const scoredMemories = [
|
|
170
|
+
...redis.map(m => ({
|
|
171
|
+
memory: m,
|
|
172
|
+
source: 'redis',
|
|
173
|
+
score: calculateRelevanceScore(m, 'redis')
|
|
174
|
+
})),
|
|
175
|
+
...github.map(m => ({
|
|
176
|
+
memory: m,
|
|
177
|
+
source: 'github',
|
|
178
|
+
score: calculateRelevanceScore(m, 'github')
|
|
179
|
+
})),
|
|
180
|
+
...postgres.map(m => ({
|
|
181
|
+
memory: m,
|
|
182
|
+
source: 'postgres',
|
|
183
|
+
score: calculateRelevanceScore(m, 'postgres')
|
|
184
|
+
})),
|
|
185
|
+
...qdrant.map(m => ({
|
|
186
|
+
memory: m,
|
|
187
|
+
source: 'qdrant',
|
|
188
|
+
score: calculateRelevanceScore(m, 'qdrant')
|
|
189
|
+
}))
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
// Deduplicate by identifier (keeps highest scoring duplicate)
|
|
193
|
+
const deduplicated = deduplicate(scoredMemories);
|
|
194
|
+
|
|
195
|
+
// Sort by score descending (highest first)
|
|
196
|
+
deduplicated.sort((a, b) => b.score - a.score);
|
|
197
|
+
|
|
198
|
+
// Return top N results
|
|
199
|
+
return deduplicated.slice(0, topN);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Format aggregated results for display
|
|
204
|
+
* @param {Array} aggregatedResults - Results from aggregateMemoryResults
|
|
205
|
+
* @returns {Array} Formatted results with readable metadata
|
|
206
|
+
*/
|
|
207
|
+
export function formatResults(aggregatedResults) {
|
|
208
|
+
return aggregatedResults.map((item, index) => ({
|
|
209
|
+
rank: index + 1,
|
|
210
|
+
score: item.score.toFixed(3),
|
|
211
|
+
source: item.source,
|
|
212
|
+
id: extractIdentifier(item.memory),
|
|
213
|
+
timestamp: extractTimestamp(item.memory),
|
|
214
|
+
similarity: extractSimilarity(item.memory).toFixed(3),
|
|
215
|
+
memory: item.memory
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get statistics about aggregated results
|
|
221
|
+
* @param {Array} aggregatedResults - Results from aggregateMemoryResults
|
|
222
|
+
* @returns {Object} Statistics summary
|
|
223
|
+
*/
|
|
224
|
+
export function getAggregationStats(aggregatedResults) {
|
|
225
|
+
const sources = aggregatedResults.reduce((acc, item) => {
|
|
226
|
+
acc[item.source] = (acc[item.source] || 0) + 1;
|
|
227
|
+
return acc;
|
|
228
|
+
}, {});
|
|
229
|
+
|
|
230
|
+
const scores = aggregatedResults.map(r => r.score);
|
|
231
|
+
const avgScore = scores.length > 0
|
|
232
|
+
? scores.reduce((a, b) => a + b, 0) / scores.length
|
|
233
|
+
: 0;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
total: aggregatedResults.length,
|
|
237
|
+
sources,
|
|
238
|
+
averageScore: avgScore.toFixed(3),
|
|
239
|
+
topScore: scores.length > 0 ? Math.max(...scores).toFixed(3) : 0,
|
|
240
|
+
bottomScore: scores.length > 0 ? Math.min(...scores).toFixed(3) : 0
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export default {
|
|
245
|
+
aggregateMemoryResults,
|
|
246
|
+
formatResults,
|
|
247
|
+
getAggregationStats,
|
|
248
|
+
calculateRelevanceScore,
|
|
249
|
+
extractIdentifier,
|
|
250
|
+
extractTimestamp,
|
|
251
|
+
extractSimilarity
|
|
252
|
+
};
|
package/lib/memory.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { existsSync } from 'fs';
|
|
6
6
|
import os from 'os';
|
|
7
|
+
import { queryMemorySupervisor, invalidateMemoryCache, getMemoryCacheStats } from './agents/memory-supervisor.js';
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = dirname(__filename);
|
|
@@ -41,6 +42,13 @@ export async function saveMemory({ repo_name, summary, content, tags = [] }) {
|
|
|
41
42
|
labels: ['session', repo_name, ...tags]
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
// Invalidate all memory caches after saving new memory
|
|
46
|
+
try {
|
|
47
|
+
await invalidateMemoryCache();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.warn('Failed to invalidate cache after save:', err.message);
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
return {
|
|
45
53
|
issue_number: issue.data.number,
|
|
46
54
|
url: issue.data.html_url,
|
|
@@ -55,23 +63,37 @@ export async function searchMemory(query, limit = 5) {
|
|
|
55
63
|
const owner = process.env.GITHUB_OWNER || 'cpretzinger';
|
|
56
64
|
const repo = process.env.GITHUB_MEMORY_REPO || 'boss-claude-memory';
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
const { data } = await client.issues.listForRepo({
|
|
67
|
+
owner,
|
|
68
|
+
repo,
|
|
69
|
+
labels: 'session',
|
|
70
|
+
state: 'all',
|
|
61
71
|
sort: 'created',
|
|
62
|
-
|
|
63
|
-
per_page:
|
|
72
|
+
direction: 'desc',
|
|
73
|
+
per_page: 100
|
|
64
74
|
});
|
|
65
75
|
|
|
66
|
-
|
|
76
|
+
const queryLower = query.toLowerCase();
|
|
77
|
+
const filtered = data.filter(issue => {
|
|
78
|
+
const titleMatch = issue.title.toLowerCase().includes(queryLower);
|
|
79
|
+
const bodyMatch = issue.body && issue.body.toLowerCase().includes(queryLower);
|
|
80
|
+
const labelMatch = issue.labels.some(l => l.name.toLowerCase().includes(queryLower));
|
|
81
|
+
return titleMatch || bodyMatch || labelMatch;
|
|
82
|
+
}).slice(0, limit);
|
|
83
|
+
|
|
84
|
+
return filtered.map(issue => ({
|
|
67
85
|
title: issue.title,
|
|
68
|
-
summary: issue.body.split('\n\n')[1] || '',
|
|
86
|
+
summary: issue.body ? issue.body.split('\n\n')[1] || '' : '',
|
|
69
87
|
url: issue.html_url,
|
|
70
88
|
created_at: issue.created_at,
|
|
71
89
|
labels: issue.labels.map(l => l.name)
|
|
72
90
|
}));
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
export async function searchMemoryAdvanced(query, options = {}) {
|
|
94
|
+
return await queryMemorySupervisor(query, options);
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
export async function getMemoryByIssue(issueNumber) {
|
|
76
98
|
const client = getOctokit();
|
|
77
99
|
|
|
@@ -92,3 +114,9 @@ export async function getMemoryByIssue(issueNumber) {
|
|
|
92
114
|
labels: data.labels.map(l => l.name)
|
|
93
115
|
};
|
|
94
116
|
}
|
|
117
|
+
|
|
118
|
+
export const memorySupervisor = {
|
|
119
|
+
query: queryMemorySupervisor,
|
|
120
|
+
invalidateCache: invalidateMemoryCache,
|
|
121
|
+
getCacheStats: getMemoryCacheStats
|
|
122
|
+
};
|