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,166 @@
1
+ /**
2
+ * Saves full conversation history and context so sessions can be resumed later.
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const logger = require('../utils/logger');
8
+
9
+ const SESSIONS_DIR = path.join(process.cwd(), 'data', 'sessions');
10
+
11
+ class SessionManager {
12
+ constructor() {
13
+ this._ensureDir();
14
+ this._active = new Map(); // sessionId -> { messages, meta }
15
+ }
16
+
17
+ _ensureDir() {
18
+ if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
19
+ }
20
+
21
+ _sessionFile(id) {
22
+ return path.join(SESSIONS_DIR, `${id}.json`);
23
+ }
24
+
25
+ /**
26
+ * Create a new session
27
+ */
28
+ create(options = {}) {
29
+ const { name = null, context = {} } = options;
30
+ const id = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
31
+ const session = {
32
+ id,
33
+ name: name || `Session ${new Date().toLocaleString()}`,
34
+ createdAt: new Date().toISOString(),
35
+ updatedAt: new Date().toISOString(),
36
+ messages: [], // { role, content, mode, sources, timestamp }
37
+ context, // metadata about what was ingested, etc.
38
+ stats: { queries: 0, totalTokens: 0, totalCostUsd: 0 },
39
+ };
40
+ this._active.set(id, session);
41
+ this._save(session);
42
+ logger.info(`[Session] Created: ${id} "${session.name}"`);
43
+ return session;
44
+ }
45
+
46
+ /**
47
+ * Add a message to a session
48
+ */
49
+ addMessage(sessionId, message) {
50
+ const session = this._getOrLoad(sessionId);
51
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
52
+
53
+ session.messages.push({
54
+ ...message,
55
+ timestamp: new Date().toISOString(),
56
+ });
57
+ session.stats.queries++;
58
+ if (message.tokens) session.stats.totalTokens += message.tokens;
59
+ if (message.costUsd) session.stats.totalCostUsd += message.costUsd;
60
+ session.updatedAt = new Date().toISOString();
61
+
62
+ this._save(session);
63
+ return session;
64
+ }
65
+
66
+ /**
67
+ * Get conversation history for a session (for context compaction)
68
+ */
69
+ getHistory(sessionId, limit = 20) {
70
+ const session = this._getOrLoad(sessionId);
71
+ if (!session) return [];
72
+ return session.messages.slice(-limit);
73
+ }
74
+
75
+ /**
76
+ * Resume a session — returns session + last N messages
77
+ */
78
+ resume(sessionId) {
79
+ const session = this._getOrLoad(sessionId);
80
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
81
+ logger.info(`[Session] Resumed: ${sessionId}`);
82
+ return {
83
+ ...session,
84
+ resumedAt: new Date().toISOString(),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * List all saved sessions
90
+ */
91
+ list() {
92
+ this._ensureDir();
93
+ const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'));
94
+ return files.map(f => {
95
+ try {
96
+ const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf-8'));
97
+ return {
98
+ id: data.id,
99
+ name: data.name,
100
+ createdAt: data.createdAt,
101
+ updatedAt: data.updatedAt,
102
+ messageCount: data.messages?.length || 0,
103
+ stats: data.stats,
104
+ };
105
+ } catch { return null; }
106
+ }).filter(Boolean).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
107
+ }
108
+
109
+ /**
110
+ * Delete a session
111
+ */
112
+ delete(sessionId) {
113
+ this._active.delete(sessionId);
114
+ const file = this._sessionFile(sessionId);
115
+ if (fs.existsSync(file)) {
116
+ fs.unlinkSync(file);
117
+ return true;
118
+ }
119
+ return false;
120
+ }
121
+
122
+ /**
123
+ * Export a session as markdown (for sharing)
124
+ */
125
+ exportMarkdown(sessionId) {
126
+ const session = this._getOrLoad(sessionId);
127
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
128
+
129
+ const lines = [
130
+ `# Session: ${session.name}`,
131
+ `> Created: ${session.createdAt} | Queries: ${session.stats.queries}`,
132
+ '',
133
+ ];
134
+
135
+ for (const msg of session.messages) {
136
+ const role = msg.role === 'user' ? '**You**' : '**MCP**';
137
+ const mode = msg.mode ? ` _(${msg.mode})_` : '';
138
+ lines.push(`### ${role}${mode}`);
139
+ lines.push(msg.content);
140
+ if (msg.sources?.length) {
141
+ lines.push('');
142
+ lines.push(`*Sources: ${msg.sources.map(s => s.file).join(', ')}*`);
143
+ }
144
+ lines.push('');
145
+ }
146
+
147
+ return lines.join('\n');
148
+ }
149
+
150
+ _getOrLoad(id) {
151
+ if (this._active.has(id)) return this._active.get(id);
152
+ const file = this._sessionFile(id);
153
+ if (!fs.existsSync(file)) return null;
154
+ try {
155
+ const session = JSON.parse(fs.readFileSync(file, 'utf-8'));
156
+ this._active.set(id, session);
157
+ return session;
158
+ } catch { return null; }
159
+ }
160
+
161
+ _save(session) {
162
+ fs.writeFileSync(this._sessionFile(session.id), JSON.stringify(session, null, 2));
163
+ }
164
+ }
165
+
166
+ module.exports = new SessionManager();
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Skills are named, reusable prompt templates / workflows that users define once
3
+ * and reuse across queries. They can reference placeholders {target}, {context}, etc.
4
+ *
5
+ * Examples:
6
+ * /skill run add-error-handling UserService.js
7
+ * /skill run document-function getUserById
8
+ * /skill run check-security AuthController.js
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const llm = require('../utils/llmClient');
14
+ const logger = require('../utils/logger');
15
+ const costTracker = require('../utils/costTracker');
16
+ const { MemoryManager } = require('../memory/memoryManager');
17
+ const indexer = require('../core/indexer');
18
+
19
+ const SKILLS_FILE = path.join(process.cwd(), 'data', 'skills.json');
20
+
21
+ // ── Built-in skills (ship with the system) ─────────────────────────────────────
22
+ const BUILTIN_SKILLS = {
23
+ 'add-error-handling': {
24
+ name: 'add-error-handling',
25
+ description: 'Add proper try/catch error handling to a function or file',
26
+ prompt: `Analyse {target} in the codebase and add proper error handling:
27
+ - Wrap async operations in try/catch
28
+ - Log errors with context (function name, input params)
29
+ - Return/throw appropriate error types
30
+ - Don't swallow errors silently
31
+ Show the exact changes needed as a diff.`,
32
+ tags: ['code-quality', 'builtin'],
33
+ builtIn: true,
34
+ },
35
+ 'document-function': {
36
+ name: 'document-function',
37
+ description: 'Generate JSDoc documentation for a function or module',
38
+ prompt: `Generate complete JSDoc documentation for {target}:
39
+ - @description — what it does
40
+ - @param — each parameter with type and description
41
+ - @returns — return type and description
42
+ - @throws — errors it can throw
43
+ - @example — a realistic usage example
44
+ Base everything on the actual code, not assumptions.`,
45
+ tags: ['documentation', 'builtin'],
46
+ builtIn: true,
47
+ },
48
+ 'check-security': {
49
+ name: 'check-security',
50
+ description: 'Security audit of a specific file or function',
51
+ prompt: `Perform a targeted security audit of {target}:
52
+ 1. Input validation — are all inputs sanitized?
53
+ 2. Auth checks — are endpoints/methods properly protected?
54
+ 3. SQL/NoSQL injection — are queries parameterized?
55
+ 4. Sensitive data — are secrets/tokens handled safely?
56
+ 5. Error messages — do they leak internal details?
57
+ Rate each finding CRITICAL / HIGH / MEDIUM / LOW. Provide exact fixes.`,
58
+ tags: ['security', 'builtin'],
59
+ builtIn: true,
60
+ },
61
+ 'explain-flow': {
62
+ name: 'explain-flow',
63
+ description: 'Explain the execution flow through a function or module',
64
+ prompt: `Trace and explain the complete execution flow of {target}:
65
+ 1. Entry point and inputs
66
+ 2. Step-by-step what happens (include function calls, DB calls, external calls)
67
+ 3. All possible exit paths (happy path, errors, edge cases)
68
+ 4. Side effects (what state is changed, what events are emitted)
69
+ Use numbered steps. Reference actual file/function names from the codebase.`,
70
+ tags: ['understanding', 'builtin'],
71
+ builtIn: true,
72
+ },
73
+ 'find-similar': {
74
+ name: 'find-similar',
75
+ description: 'Find similar patterns or duplicated logic in the codebase',
76
+ prompt: `Find all code in the codebase that is similar to or duplicates {target}:
77
+ - Functions with the same purpose implemented differently
78
+ - Copy-pasted blocks with minor variations
79
+ - Patterns that should be extracted into a shared utility
80
+ List each occurrence with file name and line reference.
81
+ Suggest how to consolidate them.`,
82
+ tags: ['refactoring', 'builtin'],
83
+ builtIn: true,
84
+ },
85
+ 'write-tests': {
86
+ name: 'write-tests',
87
+ description: 'Generate unit test cases for a function',
88
+ prompt: `Write comprehensive unit tests for {target}:
89
+ - Happy path test cases
90
+ - Edge cases (null/undefined inputs, empty arrays, boundary values)
91
+ - Error cases (what should throw or return error)
92
+ - Mock external dependencies (DB, APIs, cache)
93
+ Use Jest syntax. Base tests on the ACTUAL behaviour in the code, not assumptions.`,
94
+ tags: ['testing', 'builtin'],
95
+ builtIn: true,
96
+ },
97
+ 'performance-audit': {
98
+ name: 'performance-audit',
99
+ description: 'Find performance issues in a specific file or function',
100
+ prompt: `Audit {target} for performance issues:
101
+ - N+1 database queries (calling DB inside a loop)
102
+ - Missing async/await (synchronous blocking operations)
103
+ - Unnecessary data loading (fetching more than needed)
104
+ - Missing caching opportunities
105
+ - Inefficient algorithms or data structures
106
+ For each issue: severity, location, and specific fix.`,
107
+ tags: ['performance', 'builtin'],
108
+ builtIn: true,
109
+ },
110
+ 'migration-plan': {
111
+ name: 'migration-plan',
112
+ description: 'Plan a safe migration or refactor of a module',
113
+ prompt: `Create a step-by-step migration plan for changing {target}:
114
+ 1. Current state analysis
115
+ 2. All code that depends on it (imports, usages)
116
+ 3. Proposed new implementation
117
+ 4. Migration steps in safe order (least risk first)
118
+ 5. Rollback strategy
119
+ 6. How to test each step
120
+ Be specific about which files to change and in what order.`,
121
+ tags: ['planning', 'builtin'],
122
+ builtIn: true,
123
+ },
124
+ };
125
+
126
+ class SkillsManager {
127
+ constructor() {
128
+ this._custom = this._load();
129
+ }
130
+
131
+ _load() {
132
+ try {
133
+ if (fs.existsSync(SKILLS_FILE)) return JSON.parse(fs.readFileSync(SKILLS_FILE, 'utf-8'));
134
+ } catch { }
135
+ return {};
136
+ }
137
+
138
+ _save() {
139
+ fs.writeFileSync(SKILLS_FILE, JSON.stringify(this._custom, null, 2));
140
+ }
141
+
142
+ /**
143
+ * Get a skill by name (checks custom first, then built-in)
144
+ */
145
+ get(name) {
146
+ return this._custom[name] || BUILTIN_SKILLS[name] || null;
147
+ }
148
+
149
+ /**
150
+ * List all skills
151
+ */
152
+ list(filter = {}) {
153
+ const all = { ...BUILTIN_SKILLS, ...this._custom };
154
+ let skills = Object.values(all);
155
+ if (filter.tags?.length) {
156
+ skills = skills.filter(s => filter.tags.some(t => s.tags?.includes(t)));
157
+ }
158
+ if (filter.search) {
159
+ const q = filter.search.toLowerCase();
160
+ skills = skills.filter(s => s.name.includes(q) || s.description.includes(q));
161
+ }
162
+ return skills;
163
+ }
164
+
165
+ /**
166
+ * Create a custom skill
167
+ */
168
+ create(name, description, prompt, tags = []) {
169
+ if (!name || !prompt) throw new Error('name and prompt are required');
170
+ if (BUILTIN_SKILLS[name]) throw new Error(`Cannot override built-in skill: ${name}`);
171
+
172
+ const skill = {
173
+ name: name.toLowerCase().replace(/\s+/g, '-'),
174
+ description,
175
+ prompt,
176
+ tags: [...tags, 'custom'],
177
+ builtIn: false,
178
+ createdAt: new Date().toISOString(),
179
+ };
180
+
181
+ this._custom[skill.name] = skill;
182
+ this._save();
183
+ logger.info(`[Skills] Created: ${skill.name}`);
184
+ return skill;
185
+ }
186
+
187
+ /**
188
+ * Delete a custom skill
189
+ */
190
+ delete(name) {
191
+ if (BUILTIN_SKILLS[name]) throw new Error('Cannot delete built-in skills');
192
+ if (!this._custom[name]) throw new Error(`Skill not found: ${name}`);
193
+ delete this._custom[name];
194
+ this._save();
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * Execute a skill against a target
200
+ * @param {string} skillName - Skill to run
201
+ * @param {string} target - What to run it on (function name, file path, etc.)
202
+ * @param {object} opts
203
+ */
204
+ async run(skillName, target, opts = {}) {
205
+ const { sessionId = 'default', extraContext = '' } = opts;
206
+ const skill = this.get(skillName);
207
+ if (!skill) throw new Error(`Unknown skill: ${skillName}. Run /skill list to see available skills.`);
208
+
209
+ logger.info(`[Skills] Running "${skillName}" on "${target}"`);
210
+
211
+ // Build the prompt by substituting {target}
212
+ const prompt = skill.prompt.replace(/\{target\}/g, target);
213
+
214
+ // Retrieve relevant context for the target
215
+ const docs = indexer.search(`${target} ${skillName}`, 8);
216
+ const memories = MemoryManager.getRelevant(`${skillName} ${target}`, 3);
217
+ const memContext = MemoryManager.formatAsContext(memories);
218
+
219
+ const contextStr = docs.length > 0
220
+ ? '\n\n## Codebase Context\n' + docs.map(d =>
221
+ `**${d.filename}** (${d.kind}):\n\`\`\`\n${d.content.slice(0, 800)}\n\`\`\``
222
+ ).join('\n\n---\n\n')
223
+ : '';
224
+
225
+ const systemPrompt = [
226
+ `You are an expert developer executing a specific skill: "${skill.name}".`,
227
+ `Be precise, code-focused, and base your answer entirely on the provided codebase context.`,
228
+ memContext,
229
+ ].filter(Boolean).join('\n\n');
230
+
231
+ const userMessage = `${prompt}${extraContext ? '\n\nAdditional context: ' + extraContext : ''}${contextStr}`;
232
+
233
+ const response = await llm.chat({
234
+ model: llm.model('smart'),
235
+ max_tokens: 2000,
236
+ system: systemPrompt,
237
+ messages: [{ role: 'user', content: userMessage }],
238
+ });
239
+
240
+ costTracker.record({
241
+ model: llm.model('smart'),
242
+ inputTokens: response.usage.input_tokens,
243
+ outputTokens: response.usage.output_tokens,
244
+ sessionId,
245
+ queryType: `skill-${skillName}`,
246
+ });
247
+
248
+ const result = response.content[0].text;
249
+
250
+ // Auto-save useful outcomes to memory
251
+ MemoryManager.extractFromExchange(`${skillName} on ${target}`, result, sessionId).catch(() => { });
252
+
253
+ return {
254
+ skill: skillName,
255
+ target,
256
+ result,
257
+ sourcesUsed: docs.length,
258
+ usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens },
259
+ };
260
+ }
261
+ }
262
+
263
+ module.exports = new SkillsManager();
@@ -7,119 +7,141 @@ const INDEX_FILE = path.join(DATA_DIR, 'index.json');
7
7
  const META_FILE = path.join(DATA_DIR, 'meta.json');
8
8
 
9
9
  class Store {
10
- constructor() {
11
- this._ensureDataDir();
12
- this.index = this._load(INDEX_FILE, []);
13
- this.meta = this._load(META_FILE, {
14
- totalDocs: 0,
15
- totalFiles: 0,
16
- lastIngested: null,
17
- fileTypes: {},
18
- tags: [],
19
- });
10
+ constructor() {
11
+ this._ensureDataDir();
12
+ this.index = this._load(INDEX_FILE, []);
13
+ this.meta = this._load(META_FILE, {
14
+ totalDocs: 0,
15
+ totalFiles: 0,
16
+ lastIngested: null,
17
+ fileTypes: {},
18
+ tags: [],
19
+ });
20
+ }
21
+
22
+ _ensureDataDir() {
23
+ if (!fs.existsSync(DATA_DIR)) {
24
+ fs.mkdirSync(DATA_DIR, { recursive: true });
20
25
  }
21
-
22
- _ensureDataDir() {
23
- if (!fs.existsSync(DATA_DIR)) {
24
- fs.mkdirSync(DATA_DIR, { recursive: true });
25
- }
26
- const logsDir = path.join(process.cwd(), 'logs');
27
- if (!fs.existsSync(logsDir)) {
28
- fs.mkdirSync(logsDir, { recursive: true });
29
- }
30
- }
31
-
32
- _load(file, defaultVal) {
33
- try {
34
- if (fs.existsSync(file)) {
35
- return JSON.parse(fs.readFileSync(file, 'utf-8'));
36
- }
37
- } catch (e) {
38
- logger.warn(`Could not load ${file}: ${e.message}`);
39
- }
40
- return defaultVal;
41
- }
42
-
43
- _save() {
44
- fs.writeFileSync(INDEX_FILE, JSON.stringify(this.index, null, 2));
45
- fs.writeFileSync(META_FILE, JSON.stringify(this.meta, null, 2));
46
- }
47
-
48
- upsertDocs(docs) {
49
- let added = 0;
50
- let updated = 0;
51
-
52
- for (const doc of docs) {
53
- const existingIdx = this.index.findIndex(d => d.id === doc.id);
54
- if (existingIdx >= 0) {
55
- this.index[existingIdx] = doc;
56
- updated++;
57
- } else {
58
- this.index.push(doc);
59
- added++;
60
- }
61
- }
62
-
63
- this._rebuildMeta();
64
- this._save();
65
- return { added, updated };
66
- }
67
-
68
- removeByPath(filePath) {
69
- const before = this.index.length;
70
- this.index = this.index.filter(d => d.filePath !== filePath);
71
- const removed = before - this.index.length;
72
- this._rebuildMeta();
73
- this._save();
74
- return removed;
75
- }
76
-
77
- getAll() {
78
- return this.index;
79
- }
80
-
81
- getByKind(kind) {
82
- return this.index.filter(d => d.kind === kind);
26
+ const logsDir = path.join(process.cwd(), 'logs');
27
+ if (!fs.existsSync(logsDir)) {
28
+ fs.mkdirSync(logsDir, { recursive: true });
83
29
  }
84
-
85
- getIngestedFiles() {
86
- return [...new Set(this.index.map(d => d.filePath))];
30
+ }
31
+
32
+ _load(file, defaultVal) {
33
+ try {
34
+ if (fs.existsSync(file)) {
35
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
36
+ }
37
+ } catch (e) {
38
+ logger.warn(`Could not load ${file}: ${e.message}`);
87
39
  }
88
-
89
- clear() {
90
- this.index = [];
91
- this.meta = {
92
- totalDocs: 0,
93
- totalFiles: 0,
94
- lastIngested: null,
95
- fileTypes: {},
96
- tags: [],
97
- };
98
- this._save();
40
+ return defaultVal;
41
+ }
42
+
43
+ _save() {
44
+ fs.writeFileSync(INDEX_FILE, JSON.stringify(this.index, null, 2));
45
+ fs.writeFileSync(META_FILE, JSON.stringify(this.meta, null, 2));
46
+ }
47
+
48
+ /**
49
+ * Add or update documents in the index
50
+ */
51
+ upsertDocs(docs) {
52
+ let added = 0;
53
+ let updated = 0;
54
+
55
+ for (const doc of docs) {
56
+ const existingIdx = this.index.findIndex(d => d.id === doc.id);
57
+ if (existingIdx >= 0) {
58
+ this.index[existingIdx] = doc;
59
+ updated++;
60
+ } else {
61
+ this.index.push(doc);
62
+ added++;
63
+ }
99
64
  }
100
65
 
101
- getStats() {
102
- return {
103
- ...this.meta,
104
- indexSize: this.index.length,
105
- };
66
+ this._rebuildMeta();
67
+ this._save();
68
+ return { added, updated };
69
+ }
70
+
71
+ /**
72
+ * Remove all documents from a specific file path
73
+ */
74
+ removeByPath(filePath) {
75
+ const before = this.index.length;
76
+ this.index = this.index.filter(d => d.filePath !== filePath);
77
+ const removed = before - this.index.length;
78
+ this._rebuildMeta();
79
+ this._save();
80
+ return removed;
81
+ }
82
+
83
+ /**
84
+ * Get all documents
85
+ */
86
+ getAll() {
87
+ return this.index;
88
+ }
89
+
90
+ /**
91
+ * Get documents by kind
92
+ */
93
+ getByKind(kind) {
94
+ return this.index.filter(d => d.kind === kind);
95
+ }
96
+
97
+ /**
98
+ * Get all unique file paths that have been ingested
99
+ */
100
+ getIngestedFiles() {
101
+ return [...new Set(this.index.map(d => d.filePath))];
102
+ }
103
+
104
+ /**
105
+ * Clear all data
106
+ */
107
+ clear() {
108
+ this.index = [];
109
+ this.meta = {
110
+ totalDocs: 0,
111
+ totalFiles: 0,
112
+ lastIngested: null,
113
+ fileTypes: {},
114
+ tags: [],
115
+ };
116
+ this._save();
117
+ }
118
+
119
+ /**
120
+ * Get stats
121
+ */
122
+ getStats() {
123
+ return {
124
+ ...this.meta,
125
+ indexSize: this.index.length,
126
+ };
127
+ }
128
+
129
+ _rebuildMeta() {
130
+ const files = new Set(this.index.map(d => d.filePath));
131
+ const fileTypes = {};
132
+ for (const doc of this.index) {
133
+ fileTypes[doc.kind] = (fileTypes[doc.kind] || 0) + 1;
106
134
  }
107
135
 
108
- _rebuildMeta() {
109
- const files = new Set(this.index.map(d => d.filePath));
110
- const fileTypes = {};
111
- for (const doc of this.index) {
112
- fileTypes[doc.kind] = (fileTypes[doc.kind] || 0) + 1;
113
- }
114
-
115
- this.meta = {
116
- totalDocs: this.index.length,
117
- totalFiles: files.size,
118
- lastIngested: new Date().toISOString(),
119
- fileTypes,
120
- };
121
- }
136
+ this.meta = {
137
+ totalDocs: this.index.length,
138
+ totalFiles: files.size,
139
+ lastIngested: new Date().toISOString(),
140
+ fileTypes,
141
+ };
142
+ }
122
143
  }
123
144
 
145
+ // Singleton
124
146
  const store = new Store();
125
147
  module.exports = store;