dev-mcp-server 0.0.3 → 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 +134 -6
- 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,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();
|
package/src/storage/store.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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;
|