dev-mcp-server 0.0.2 → 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/.env.example +23 -55
- package/README.md +609 -219
- package/cli.js +486 -160
- package/package.json +2 -2
- package/src/agents/BaseAgent.js +113 -0
- package/src/agents/dreamer.js +165 -0
- package/src/agents/improver.js +175 -0
- package/src/agents/specialists.js +202 -0
- package/src/agents/taskDecomposer.js +176 -0
- package/src/agents/teamCoordinator.js +153 -0
- package/src/api/routes/agents.js +172 -0
- package/src/api/routes/extras.js +115 -0
- package/src/api/routes/git.js +72 -0
- package/src/api/routes/ingest.js +60 -40
- package/src/api/routes/knowledge.js +59 -41
- package/src/api/routes/memory.js +41 -0
- package/src/api/routes/newRoutes.js +168 -0
- package/src/api/routes/pipelines.js +41 -0
- package/src/api/routes/planner.js +54 -0
- package/src/api/routes/query.js +24 -0
- package/src/api/routes/sessions.js +54 -0
- package/src/api/routes/tasks.js +67 -0
- package/src/api/routes/tools.js +85 -0
- package/src/api/routes/v5routes.js +196 -0
- package/src/api/server.js +133 -5
- package/src/context/compactor.js +151 -0
- package/src/context/contextEngineer.js +181 -0
- package/src/context/contextVisualizer.js +140 -0
- package/src/core/conversationEngine.js +231 -0
- package/src/core/indexer.js +169 -143
- package/src/core/ingester.js +141 -126
- package/src/core/queryEngine.js +286 -236
- package/src/cron/cronScheduler.js +260 -0
- package/src/dashboard/index.html +1181 -0
- package/src/lsp/symbolNavigator.js +220 -0
- package/src/memory/memoryManager.js +186 -0
- package/src/memory/teamMemory.js +111 -0
- package/src/messaging/messageBus.js +177 -0
- package/src/monitor/proactiveMonitor.js +337 -0
- package/src/pipelines/pipelineEngine.js +230 -0
- package/src/planner/plannerEngine.js +202 -0
- package/src/plugins/builtin/stats-plugin.js +29 -0
- package/src/plugins/pluginManager.js +144 -0
- package/src/prompts/promptEngineer.js +289 -0
- package/src/sessions/sessionManager.js +166 -0
- package/src/skills/skillsManager.js +263 -0
- package/src/storage/store.js +127 -105
- package/src/tasks/taskManager.js +151 -0
- package/src/tools/BashTool.js +154 -0
- package/src/tools/FileEditTool.js +280 -0
- package/src/tools/GitTool.js +212 -0
- package/src/tools/GrepTool.js +199 -0
- package/src/tools/registry.js +1380 -0
- package/src/utils/costTracker.js +69 -0
- package/src/utils/fileParser.js +176 -153
- package/src/utils/llmClient.js +355 -206
- package/src/watcher/fileWatcher.js +137 -0
- package/src/worktrees/worktreeManager.js +176 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSPTool — lightweight symbol navigation
|
|
3
|
+
* without requiring a full Language Server. Uses grep + indexer + AI.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - Go-to-definition: find where a symbol is defined
|
|
7
|
+
* - Find references: find all usages of a symbol
|
|
8
|
+
* - Hover docs: generate documentation for a symbol at a location
|
|
9
|
+
* - Symbol outline: list all symbols in a file
|
|
10
|
+
* - Workspace symbols: find symbols matching a query
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const llm = require('../utils/llmClient');
|
|
14
|
+
const GrepTool = require('../tools/GrepTool');
|
|
15
|
+
const store = require('../storage/store');
|
|
16
|
+
const indexer = require('../core/indexer');
|
|
17
|
+
const costTracker = require('../utils/costTracker');
|
|
18
|
+
const logger = require('../utils/logger');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
class SymbolNavigator {
|
|
23
|
+
/**
|
|
24
|
+
* Go-to-definition: find where a symbol is defined
|
|
25
|
+
*/
|
|
26
|
+
async goToDefinition(symbol, cwd = process.cwd()) {
|
|
27
|
+
logger.info(`[LSP] go-to-definition: ${symbol}`);
|
|
28
|
+
|
|
29
|
+
const definitions = await GrepTool.findDefinitions(symbol, cwd);
|
|
30
|
+
|
|
31
|
+
// Also search the indexed store for this symbol in metadata
|
|
32
|
+
const fromIndex = store.getAll()
|
|
33
|
+
.filter(doc => doc.metadata?.functions?.includes(symbol) || doc.metadata?.classes?.includes(symbol))
|
|
34
|
+
.map(doc => ({
|
|
35
|
+
file: doc.filePath,
|
|
36
|
+
lineNumber: null, // We don't store line numbers in index
|
|
37
|
+
kind: doc.metadata?.classes?.includes(symbol) ? 'class' : 'function',
|
|
38
|
+
fromIndex: true,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const combined = [
|
|
42
|
+
...definitions.map(d => ({ ...d, kind: this._inferKind(d.line), fromIndex: false })),
|
|
43
|
+
...fromIndex,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Deduplicate by file
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const deduped = combined.filter(d => {
|
|
49
|
+
const key = d.file + (d.lineNumber || '');
|
|
50
|
+
if (seen.has(key)) return false;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
symbol,
|
|
57
|
+
definitions: deduped,
|
|
58
|
+
count: deduped.length,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find all references to a symbol
|
|
64
|
+
*/
|
|
65
|
+
async findReferences(symbol, cwd = process.cwd()) {
|
|
66
|
+
logger.info(`[LSP] find-references: ${symbol}`);
|
|
67
|
+
|
|
68
|
+
const usages = await GrepTool.search(symbol, { cwd, maxResults: 50, contextLines: 1 });
|
|
69
|
+
const imports = await GrepTool.findImports(symbol, cwd);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
symbol,
|
|
73
|
+
references: usages.matches.map(m => ({
|
|
74
|
+
file: m.file,
|
|
75
|
+
line: m.lineNumber,
|
|
76
|
+
text: m.line.trim(),
|
|
77
|
+
type: m.line.includes('import') || m.line.includes('require') ? 'import' : 'usage',
|
|
78
|
+
})),
|
|
79
|
+
total: usages.total,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Hover documentation: generate docs for a symbol using AI
|
|
85
|
+
*/
|
|
86
|
+
async hover(symbol, filePath = null, sessionId = 'default') {
|
|
87
|
+
logger.info(`[LSP] hover: ${symbol}`);
|
|
88
|
+
|
|
89
|
+
// Find definition to get the actual code
|
|
90
|
+
const def = await this.goToDefinition(symbol);
|
|
91
|
+
const docs = indexer.searchForUsages(symbol, 4);
|
|
92
|
+
|
|
93
|
+
const contextParts = [];
|
|
94
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
95
|
+
const content = fs.readFileSync(filePath, 'utf-8').slice(0, 3000);
|
|
96
|
+
contextParts.push(`File context:\n${content}`);
|
|
97
|
+
}
|
|
98
|
+
if (docs.length > 0) {
|
|
99
|
+
contextParts.push('Codebase context:\n' + docs.map(d => d.content.slice(0, 500)).join('\n---\n'));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const response = await llm.chat({
|
|
103
|
+
model: llm.model('fast'),
|
|
104
|
+
max_tokens: 400,
|
|
105
|
+
messages: [{
|
|
106
|
+
role: 'user',
|
|
107
|
+
content: `Generate concise hover documentation for the symbol "${symbol}".
|
|
108
|
+
Include: type, purpose (1-2 sentences), parameters (if function), return value, and any gotchas.
|
|
109
|
+
Format as markdown. Be brief — this is hover text, not a full doc.
|
|
110
|
+
|
|
111
|
+
${contextParts.join('\n\n')}`,
|
|
112
|
+
}],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
costTracker.record({
|
|
116
|
+
model: llm.model('fast'),
|
|
117
|
+
inputTokens: response.usage.input_tokens,
|
|
118
|
+
outputTokens: response.usage.output_tokens,
|
|
119
|
+
sessionId,
|
|
120
|
+
queryType: 'lsp-hover',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
symbol,
|
|
125
|
+
documentation: response.content[0].text,
|
|
126
|
+
definitions: def.definitions.slice(0, 3),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Symbol outline: list all symbols (functions, classes, exports) in a file
|
|
132
|
+
*/
|
|
133
|
+
async outline(filePath) {
|
|
134
|
+
const abs = path.resolve(filePath);
|
|
135
|
+
if (!fs.existsSync(abs)) throw new Error(`File not found: ${abs}`);
|
|
136
|
+
|
|
137
|
+
const content = fs.readFileSync(abs, 'utf-8');
|
|
138
|
+
const symbols = [];
|
|
139
|
+
const lines = content.split('\n');
|
|
140
|
+
|
|
141
|
+
const patterns = [
|
|
142
|
+
{ regex: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/, kind: 'function' },
|
|
143
|
+
{ regex: /^(?:export\s+)?class\s+(\w+)/, kind: 'class' },
|
|
144
|
+
{ regex: /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\(/, kind: 'arrow-function' },
|
|
145
|
+
{ regex: /^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, kind: 'method' },
|
|
146
|
+
{ regex: /^module\.exports\s*=\s*(?:new\s+)?(\w+)/, kind: 'export' },
|
|
147
|
+
{ regex: /^exports\.(\w+)\s*=/, kind: 'export' },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
lines.forEach((line, idx) => {
|
|
151
|
+
for (const { regex, kind } of patterns) {
|
|
152
|
+
const m = line.match(regex);
|
|
153
|
+
if (m && m[1] && !['if', 'for', 'while', 'catch', 'switch'].includes(m[1])) {
|
|
154
|
+
symbols.push({ name: m[1], kind, line: idx + 1, text: line.trim().slice(0, 80) });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
filePath: abs,
|
|
162
|
+
filename: path.basename(abs),
|
|
163
|
+
symbols,
|
|
164
|
+
count: symbols.length,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Workspace symbol search: find any symbol matching a query
|
|
170
|
+
*/
|
|
171
|
+
async workspaceSymbols(query, cwd = process.cwd()) {
|
|
172
|
+
const docs = indexer.search(query, 10);
|
|
173
|
+
const symbols = [];
|
|
174
|
+
|
|
175
|
+
for (const doc of docs) {
|
|
176
|
+
const meta = doc.metadata || {};
|
|
177
|
+
const allSymbols = [
|
|
178
|
+
...(meta.functions || []).map(n => ({ name: n, kind: 'function', file: doc.filename, path: doc.filePath })),
|
|
179
|
+
...(meta.classes || []).map(n => ({ name: n, kind: 'class', file: doc.filename, path: doc.filePath })),
|
|
180
|
+
...(meta.exports || []).map(n => ({ name: n, kind: 'export', file: doc.filename, path: doc.filePath })),
|
|
181
|
+
];
|
|
182
|
+
symbols.push(...allSymbols.filter(s => s.name.toLowerCase().includes(query.toLowerCase())));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Deduplicate
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
return {
|
|
188
|
+
query,
|
|
189
|
+
symbols: symbols.filter(s => {
|
|
190
|
+
const key = `${s.name}:${s.path}`;
|
|
191
|
+
if (seen.has(key)) return false;
|
|
192
|
+
seen.add(key);
|
|
193
|
+
return true;
|
|
194
|
+
}).slice(0, 30),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Rename symbol — suggest all places that need updating
|
|
200
|
+
*/
|
|
201
|
+
async renameSymbol(oldName, newName, cwd = process.cwd()) {
|
|
202
|
+
const refs = await this.findReferences(oldName, cwd);
|
|
203
|
+
return {
|
|
204
|
+
oldName,
|
|
205
|
+
newName,
|
|
206
|
+
affectedFiles: [...new Set(refs.references.map(r => r.file))],
|
|
207
|
+
references: refs.references,
|
|
208
|
+
suggestion: `Update ${refs.references.length} occurrences across ${[...new Set(refs.references.map(r => r.file))].length} files`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_inferKind(line = '') {
|
|
213
|
+
if (/class\s+\w/.test(line)) return 'class';
|
|
214
|
+
if (/function\s+\w/.test(line)) return 'function';
|
|
215
|
+
if (/const\s+\w+\s*=\s*(?:async\s*)?\(/.test(line)) return 'arrow-function';
|
|
216
|
+
return 'symbol';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = new SymbolNavigator();
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-extracts important facts from conversations and persists them.
|
|
3
|
+
* Memory is injected into future queries as additional context.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const llm = require('../utils/llmClient');
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const costTracker = require('../utils/costTracker');
|
|
11
|
+
|
|
12
|
+
const MEMORY_FILE = path.join(process.cwd(), 'data', 'memories.json');
|
|
13
|
+
|
|
14
|
+
// Memory categories
|
|
15
|
+
const MEMORY_TYPES = {
|
|
16
|
+
FACT: 'fact', // Static facts about the codebase
|
|
17
|
+
PATTERN: 'pattern', // Recurring patterns or conventions
|
|
18
|
+
BUG: 'bug', // Known bugs and their fixes
|
|
19
|
+
DECISION: 'decision', // Architecture/design decisions
|
|
20
|
+
PERSON: 'person', // Who owns/knows what
|
|
21
|
+
PREFERENCE: 'preference', // Team preferences and conventions
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
class MemoryManager {
|
|
25
|
+
constructor() {
|
|
26
|
+
this._memories = this._load();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_load() {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(MEMORY_FILE)) {
|
|
32
|
+
return JSON.parse(fs.readFileSync(MEMORY_FILE, 'utf-8'));
|
|
33
|
+
}
|
|
34
|
+
} catch { }
|
|
35
|
+
return { entries: [], version: 1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_save() {
|
|
39
|
+
fs.writeFileSync(MEMORY_FILE, JSON.stringify(this._memories, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Add a memory entry manually
|
|
44
|
+
*/
|
|
45
|
+
add(content, type = MEMORY_TYPES.FACT, tags = []) {
|
|
46
|
+
const entry = {
|
|
47
|
+
id: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
48
|
+
content: content.trim(),
|
|
49
|
+
type,
|
|
50
|
+
tags,
|
|
51
|
+
createdAt: new Date().toISOString(),
|
|
52
|
+
useCount: 0,
|
|
53
|
+
lastUsedAt: null,
|
|
54
|
+
};
|
|
55
|
+
this._memories.entries.push(entry);
|
|
56
|
+
this._save();
|
|
57
|
+
logger.info(`[Memory] Added: "${content.slice(0, 80)}"`);
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Auto-extract memories from a Q&A exchange
|
|
63
|
+
*/
|
|
64
|
+
async extractFromExchange(question, answer, sessionId = 'default') {
|
|
65
|
+
try {
|
|
66
|
+
const response = await llm.chat({
|
|
67
|
+
model: llm.model('fast'),
|
|
68
|
+
max_tokens: 400,
|
|
69
|
+
system: `Extract important, reusable facts from this developer Q&A exchange.
|
|
70
|
+
Only extract things that would be useful context for FUTURE questions about this codebase.
|
|
71
|
+
Return a JSON array of objects: [{"content": "...", "type": "fact|pattern|bug|decision|person|preference", "tags": ["tag1"]}]
|
|
72
|
+
Return [] if nothing worth remembering. Be selective — only extract genuinely useful facts.
|
|
73
|
+
Return ONLY the JSON array, no other text.`,
|
|
74
|
+
messages: [{
|
|
75
|
+
role: 'user',
|
|
76
|
+
content: `Question: ${question}\n\nAnswer: ${answer.slice(0, 1500)}`,
|
|
77
|
+
}],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
costTracker.record({
|
|
81
|
+
model: llm.model('fast'),
|
|
82
|
+
inputTokens: response.usage.input_tokens,
|
|
83
|
+
outputTokens: response.usage.output_tokens,
|
|
84
|
+
sessionId,
|
|
85
|
+
queryType: 'memory-extract',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const text = response.content[0].text.trim();
|
|
89
|
+
const extracted = JSON.parse(text.replace(/```json\n?|\n?```/g, '').trim());
|
|
90
|
+
|
|
91
|
+
if (!Array.isArray(extracted)) return [];
|
|
92
|
+
|
|
93
|
+
const added = [];
|
|
94
|
+
for (const item of extracted) {
|
|
95
|
+
if (item.content && item.content.length > 10) {
|
|
96
|
+
const entry = this.add(item.content, item.type || MEMORY_TYPES.FACT, item.tags || []);
|
|
97
|
+
added.push(entry);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return added;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
logger.warn(`[Memory] Extraction failed: ${err.message}`);
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get memories relevant to a query (simple keyword match)
|
|
109
|
+
*/
|
|
110
|
+
getRelevant(query, limit = 5) {
|
|
111
|
+
const q = query.toLowerCase();
|
|
112
|
+
const words = q.split(/\W+/).filter(w => w.length > 3);
|
|
113
|
+
|
|
114
|
+
const scored = this._memories.entries.map(mem => {
|
|
115
|
+
const text = (mem.content + ' ' + mem.tags.join(' ')).toLowerCase();
|
|
116
|
+
let score = 0;
|
|
117
|
+
for (const word of words) {
|
|
118
|
+
if (text.includes(word)) score += 1;
|
|
119
|
+
}
|
|
120
|
+
// Boost recent memories
|
|
121
|
+
const age = Date.now() - new Date(mem.createdAt).getTime();
|
|
122
|
+
const ageDays = age / (1000 * 60 * 60 * 24);
|
|
123
|
+
if (ageDays < 7) score += 0.5;
|
|
124
|
+
// Boost frequently used
|
|
125
|
+
score += mem.useCount * 0.1;
|
|
126
|
+
return { mem, score };
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return scored
|
|
130
|
+
.filter(s => s.score > 0)
|
|
131
|
+
.sort((a, b) => b.score - a.score)
|
|
132
|
+
.slice(0, limit)
|
|
133
|
+
.map(s => {
|
|
134
|
+
s.mem.useCount++;
|
|
135
|
+
s.mem.lastUsedAt = new Date().toISOString();
|
|
136
|
+
return s.mem;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Format memories as context string for injection into prompts
|
|
142
|
+
*/
|
|
143
|
+
formatAsContext(memories) {
|
|
144
|
+
if (!memories || memories.length === 0) return '';
|
|
145
|
+
return `## Persistent Memory (what I know about this codebase)\n` +
|
|
146
|
+
memories.map(m => `- [${m.type}] ${m.content}`).join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List all memories
|
|
151
|
+
*/
|
|
152
|
+
list(type = null) {
|
|
153
|
+
const entries = type
|
|
154
|
+
? this._memories.entries.filter(m => m.type === type)
|
|
155
|
+
: this._memories.entries;
|
|
156
|
+
return entries.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Delete a memory by ID
|
|
161
|
+
*/
|
|
162
|
+
delete(id) {
|
|
163
|
+
const before = this._memories.entries.length;
|
|
164
|
+
this._memories.entries = this._memories.entries.filter(m => m.id !== id);
|
|
165
|
+
this._save();
|
|
166
|
+
return before !== this._memories.entries.length;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Clear all memories
|
|
171
|
+
*/
|
|
172
|
+
clear() {
|
|
173
|
+
this._memories.entries = [];
|
|
174
|
+
this._save();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getStats() {
|
|
178
|
+
const byType = {};
|
|
179
|
+
for (const entry of this._memories.entries) {
|
|
180
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
181
|
+
}
|
|
182
|
+
return { total: this._memories.entries.length, byType };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { MemoryManager: new MemoryManager(), MEMORY_TYPES };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* A shared memory store that persists across all sessions and is visible
|
|
4
|
+
* to all agents. Unlike personal MemoryManager (per-session), team memory
|
|
5
|
+
* is a shared knowledge base that every agent reads from automatically.
|
|
6
|
+
*
|
|
7
|
+
* Use cases:
|
|
8
|
+
* - Codebase-wide conventions that all agents should know
|
|
9
|
+
* - Known bugs and their fixes
|
|
10
|
+
* - Architecture decisions
|
|
11
|
+
* - Onboarding facts for new developers
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const logger = require('../utils/logger');
|
|
17
|
+
|
|
18
|
+
const TEAM_MEM_FILE = path.join(process.cwd(), 'data', 'team-memory.json');
|
|
19
|
+
|
|
20
|
+
class TeamMemory {
|
|
21
|
+
constructor() {
|
|
22
|
+
this._store = this._load();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_load() {
|
|
26
|
+
try { if (fs.existsSync(TEAM_MEM_FILE)) return JSON.parse(fs.readFileSync(TEAM_MEM_FILE, 'utf-8')); } catch { }
|
|
27
|
+
return { entries: {}, nextId: 1 };
|
|
28
|
+
}
|
|
29
|
+
_save() { fs.writeFileSync(TEAM_MEM_FILE, JSON.stringify(this._store, null, 2)); }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add a team-wide memory entry
|
|
33
|
+
*/
|
|
34
|
+
add({ team = 'global', content, type = 'fact', tags = [], author = 'system' }) {
|
|
35
|
+
if (!content) throw new Error('content required');
|
|
36
|
+
const id = `tmem_${this._store.nextId++}`;
|
|
37
|
+
const entry = { id, team, content, type, tags, author, createdAt: new Date().toISOString(), useCount: 0 };
|
|
38
|
+
if (!this._store.entries[team]) this._store.entries[team] = [];
|
|
39
|
+
this._store.entries[team].push(entry);
|
|
40
|
+
this._save();
|
|
41
|
+
logger.info(`[TeamMemory] Added to "${team}": ${content.slice(0, 60)}`);
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all entries for a team (or global if no team)
|
|
47
|
+
*/
|
|
48
|
+
get(team = 'global', opts = {}) {
|
|
49
|
+
const { type, limit = 50 } = opts;
|
|
50
|
+
const entries = [
|
|
51
|
+
...(this._store.entries['global'] || []),
|
|
52
|
+
...(team !== 'global' ? (this._store.entries[team] || []) : []),
|
|
53
|
+
];
|
|
54
|
+
const filtered = type ? entries.filter(e => e.type === type) : entries;
|
|
55
|
+
return filtered
|
|
56
|
+
.sort((a, b) => b.useCount - a.useCount || new Date(b.createdAt) - new Date(a.createdAt))
|
|
57
|
+
.slice(0, limit);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Search team memory by keyword
|
|
62
|
+
*/
|
|
63
|
+
search(query, team = 'global') {
|
|
64
|
+
const entries = this.get(team, { limit: 200 });
|
|
65
|
+
const q = query.toLowerCase();
|
|
66
|
+
return entries
|
|
67
|
+
.filter(e => e.content.toLowerCase().includes(q) || e.tags.some(t => t.includes(q)))
|
|
68
|
+
.map(e => { e.useCount++; return e; });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format team memory as a system context block for injection into agents
|
|
73
|
+
*/
|
|
74
|
+
formatForAgent(team = 'global') {
|
|
75
|
+
const entries = this.get(team, { limit: 20 });
|
|
76
|
+
if (!entries.length) return '';
|
|
77
|
+
return `## Team Knowledge Base (${team})\n` +
|
|
78
|
+
entries.map(e => `- [${e.type}] ${e.content}`).join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* List all teams
|
|
83
|
+
*/
|
|
84
|
+
listTeams() {
|
|
85
|
+
return Object.keys(this._store.entries).map(team => ({
|
|
86
|
+
team,
|
|
87
|
+
count: this._store.entries[team].length,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
delete(id) {
|
|
92
|
+
for (const team of Object.keys(this._store.entries)) {
|
|
93
|
+
const before = this._store.entries[team].length;
|
|
94
|
+
this._store.entries[team] = this._store.entries[team].filter(e => e.id !== id);
|
|
95
|
+
if (this._store.entries[team].length !== before) { this._save(); return true; }
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clearTeam(team) {
|
|
101
|
+
delete this._store.entries[team];
|
|
102
|
+
this._save();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getStats() {
|
|
106
|
+
const total = Object.values(this._store.entries).reduce((s, a) => s + a.length, 0);
|
|
107
|
+
return { total, teams: this.listTeams() };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = new TeamMemory();
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* A lightweight in-process message bus allowing agents to communicate.
|
|
4
|
+
* Each agent has an inbox. Messages can be sent with priority levels.
|
|
5
|
+
*
|
|
6
|
+
* Used when:
|
|
7
|
+
* - DebugAgent finds a security issue → notifies SecurityAgent
|
|
8
|
+
* - PlannerAgent creates tasks → notifies the right specialist
|
|
9
|
+
* - TeamCoordinator broadcasts findings to all agents
|
|
10
|
+
* - Cron jobs need to alert about findings
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const logger = require('../utils/logger');
|
|
16
|
+
|
|
17
|
+
const MSG_FILE = path.join(process.cwd(), 'data', 'messages.json');
|
|
18
|
+
|
|
19
|
+
const PRIORITY = { LOW: 'low', NORMAL: 'normal', HIGH: 'high', URGENT: 'urgent' };
|
|
20
|
+
|
|
21
|
+
class MessageBus {
|
|
22
|
+
constructor() {
|
|
23
|
+
this._store = this._load();
|
|
24
|
+
this._subscribers = new Map(); // agentName → callback[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_load() {
|
|
28
|
+
try { if (fs.existsSync(MSG_FILE)) return JSON.parse(fs.readFileSync(MSG_FILE, 'utf-8')); } catch { }
|
|
29
|
+
return { messages: [], nextId: 1 };
|
|
30
|
+
}
|
|
31
|
+
_save() { fs.writeFileSync(MSG_FILE, JSON.stringify(this._store, null, 2)); }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Send a message from one agent to another
|
|
35
|
+
*/
|
|
36
|
+
send({ from, to, content, priority = PRIORITY.NORMAL, metadata = {}, replyTo }) {
|
|
37
|
+
if (!from || !to || !content) throw new Error('from, to, content required');
|
|
38
|
+
|
|
39
|
+
const msg = {
|
|
40
|
+
id: this._store.nextId++,
|
|
41
|
+
from,
|
|
42
|
+
to,
|
|
43
|
+
content,
|
|
44
|
+
priority,
|
|
45
|
+
metadata,
|
|
46
|
+
replyTo: replyTo || null,
|
|
47
|
+
sentAt: new Date().toISOString(),
|
|
48
|
+
readAt: null,
|
|
49
|
+
read: false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
this._store.messages.push(msg);
|
|
53
|
+
|
|
54
|
+
// Keep last 500 messages
|
|
55
|
+
if (this._store.messages.length > 500) {
|
|
56
|
+
this._store.messages = this._store.messages.slice(-500);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this._save();
|
|
60
|
+
logger.info(`[MsgBus] ${from} → ${to}: "${content.slice(0, 60)}" [${priority}]`);
|
|
61
|
+
|
|
62
|
+
// Notify live subscribers (real-time delivery)
|
|
63
|
+
const subs = this._subscribers.get(to) || [];
|
|
64
|
+
for (const cb of subs) {
|
|
65
|
+
try { cb(msg); } catch { }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return msg;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Broadcast to all agents (or a list)
|
|
73
|
+
*/
|
|
74
|
+
broadcast({ from, content, to: recipients, priority = PRIORITY.NORMAL, metadata = {} }) {
|
|
75
|
+
const all = recipients || ['DebugAgent', 'ArchitectureAgent', 'SecurityAgent', 'DocumentationAgent', 'RefactorAgent', 'PerformanceAgent', 'TestAgent', 'DevOpsAgent', 'DataAgent', 'PlannerAgent'];
|
|
76
|
+
return all.map(to => this.send({ from, to, content, priority, metadata }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get inbox for an agent
|
|
81
|
+
*/
|
|
82
|
+
inbox(agentName, opts = {}) {
|
|
83
|
+
const { unreadOnly = false, priority, limit = 50 } = opts;
|
|
84
|
+
let msgs = this._store.messages.filter(m => m.to === agentName);
|
|
85
|
+
if (unreadOnly) msgs = msgs.filter(m => !m.read);
|
|
86
|
+
if (priority) msgs = msgs.filter(m => m.priority === priority);
|
|
87
|
+
return msgs
|
|
88
|
+
.sort((a, b) => {
|
|
89
|
+
const pOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
|
|
90
|
+
return (pOrder[a.priority] ?? 2) - (pOrder[b.priority] ?? 2);
|
|
91
|
+
})
|
|
92
|
+
.slice(-limit)
|
|
93
|
+
.reverse();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get all messages from a specific sender
|
|
98
|
+
*/
|
|
99
|
+
sentBy(agentName, limit = 20) {
|
|
100
|
+
return this._store.messages
|
|
101
|
+
.filter(m => m.from === agentName)
|
|
102
|
+
.slice(-limit)
|
|
103
|
+
.reverse();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Mark a message as read
|
|
108
|
+
*/
|
|
109
|
+
markRead(id) {
|
|
110
|
+
const msg = this._store.messages.find(m => m.id === id);
|
|
111
|
+
if (!msg) throw new Error(`Message ${id} not found`);
|
|
112
|
+
msg.read = true;
|
|
113
|
+
msg.readAt = new Date().toISOString();
|
|
114
|
+
this._save();
|
|
115
|
+
return msg;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
markAllRead(agentName) {
|
|
119
|
+
let count = 0;
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
for (const msg of this._store.messages) {
|
|
122
|
+
if (msg.to === agentName && !msg.read) {
|
|
123
|
+
msg.read = true; msg.readAt = now; count++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
this._save();
|
|
127
|
+
return count;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Reply to a message
|
|
132
|
+
*/
|
|
133
|
+
reply(originalId, { from, content, priority = PRIORITY.NORMAL }) {
|
|
134
|
+
const original = this._store.messages.find(m => m.id === originalId);
|
|
135
|
+
if (!original) throw new Error(`Message ${originalId} not found`);
|
|
136
|
+
return this.send({ from, to: original.from, content, priority, replyTo: originalId });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Subscribe to live messages for an agent (callback-based)
|
|
141
|
+
*/
|
|
142
|
+
subscribe(agentName, callback) {
|
|
143
|
+
if (!this._subscribers.has(agentName)) this._subscribers.set(agentName, []);
|
|
144
|
+
this._subscribers.get(agentName).push(callback);
|
|
145
|
+
return () => {
|
|
146
|
+
const subs = this._subscribers.get(agentName) || [];
|
|
147
|
+
this._subscribers.set(agentName, subs.filter(c => c !== callback));
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
delete(id) {
|
|
152
|
+
const before = this._store.messages.length;
|
|
153
|
+
this._store.messages = this._store.messages.filter(m => m.id !== id);
|
|
154
|
+
this._save();
|
|
155
|
+
return before !== this._store.messages.length;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
clearInbox(agentName) {
|
|
159
|
+
this._store.messages = this._store.messages.filter(m => m.to !== agentName);
|
|
160
|
+
this._save();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getStats() {
|
|
164
|
+
const msgs = this._store.messages;
|
|
165
|
+
return {
|
|
166
|
+
total: msgs.length,
|
|
167
|
+
unread: msgs.filter(m => !m.read).length,
|
|
168
|
+
byAgent: [...new Set(msgs.map(m => m.to))].map(a => ({
|
|
169
|
+
agent: a,
|
|
170
|
+
unread: msgs.filter(m => m.to === a && !m.read).length,
|
|
171
|
+
total: msgs.filter(m => m.to === a).length,
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { MessageBus: new MessageBus(), PRIORITY };
|