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.
Files changed (58) hide show
  1. package/.env.example +23 -55
  2. package/README.md +609 -219
  3. package/cli.js +486 -160
  4. package/package.json +2 -2
  5. package/src/agents/BaseAgent.js +113 -0
  6. package/src/agents/dreamer.js +165 -0
  7. package/src/agents/improver.js +175 -0
  8. package/src/agents/specialists.js +202 -0
  9. package/src/agents/taskDecomposer.js +176 -0
  10. package/src/agents/teamCoordinator.js +153 -0
  11. package/src/api/routes/agents.js +172 -0
  12. package/src/api/routes/extras.js +115 -0
  13. package/src/api/routes/git.js +72 -0
  14. package/src/api/routes/ingest.js +60 -40
  15. package/src/api/routes/knowledge.js +59 -41
  16. package/src/api/routes/memory.js +41 -0
  17. package/src/api/routes/newRoutes.js +168 -0
  18. package/src/api/routes/pipelines.js +41 -0
  19. package/src/api/routes/planner.js +54 -0
  20. package/src/api/routes/query.js +24 -0
  21. package/src/api/routes/sessions.js +54 -0
  22. package/src/api/routes/tasks.js +67 -0
  23. package/src/api/routes/tools.js +85 -0
  24. package/src/api/routes/v5routes.js +196 -0
  25. package/src/api/server.js +133 -5
  26. package/src/context/compactor.js +151 -0
  27. package/src/context/contextEngineer.js +181 -0
  28. package/src/context/contextVisualizer.js +140 -0
  29. package/src/core/conversationEngine.js +231 -0
  30. package/src/core/indexer.js +169 -143
  31. package/src/core/ingester.js +141 -126
  32. package/src/core/queryEngine.js +286 -236
  33. package/src/cron/cronScheduler.js +260 -0
  34. package/src/dashboard/index.html +1181 -0
  35. package/src/lsp/symbolNavigator.js +220 -0
  36. package/src/memory/memoryManager.js +186 -0
  37. package/src/memory/teamMemory.js +111 -0
  38. package/src/messaging/messageBus.js +177 -0
  39. package/src/monitor/proactiveMonitor.js +337 -0
  40. package/src/pipelines/pipelineEngine.js +230 -0
  41. package/src/planner/plannerEngine.js +202 -0
  42. package/src/plugins/builtin/stats-plugin.js +29 -0
  43. package/src/plugins/pluginManager.js +144 -0
  44. package/src/prompts/promptEngineer.js +289 -0
  45. package/src/sessions/sessionManager.js +166 -0
  46. package/src/skills/skillsManager.js +263 -0
  47. package/src/storage/store.js +127 -105
  48. package/src/tasks/taskManager.js +151 -0
  49. package/src/tools/BashTool.js +154 -0
  50. package/src/tools/FileEditTool.js +280 -0
  51. package/src/tools/GitTool.js +212 -0
  52. package/src/tools/GrepTool.js +199 -0
  53. package/src/tools/registry.js +1380 -0
  54. package/src/utils/costTracker.js +69 -0
  55. package/src/utils/fileParser.js +176 -153
  56. package/src/utils/llmClient.js +355 -206
  57. package/src/watcher/fileWatcher.js +137 -0
  58. 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 };