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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode: Before executing a complex change, generate a step-by-step plan
|
|
3
|
+
* and get user approval before proceeding.
|
|
4
|
+
*
|
|
5
|
+
* Compact: Compress long conversation history into a dense summary
|
|
6
|
+
* to stay within context limits.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const llm = require('../utils/llmClient');
|
|
10
|
+
const costTracker = require('../utils/costTracker');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
|
|
13
|
+
class PlannerEngine {
|
|
14
|
+
/**
|
|
15
|
+
* Generate an execution plan for a complex task.
|
|
16
|
+
* This is the "think before you act" pattern.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} task - What the user wants to do
|
|
19
|
+
* @param {Array} context - Retrieved codebase chunks
|
|
20
|
+
* @param {string} sessionId
|
|
21
|
+
*/
|
|
22
|
+
async generatePlan(task, context = [], sessionId = 'default') {
|
|
23
|
+
const contextStr = context.length > 0
|
|
24
|
+
? `\n\n## Relevant Codebase Context\n${context.map(c => `**${c.filename}**:\n${c.content.slice(0, 800)}`).join('\n\n---\n\n')}`
|
|
25
|
+
: '';
|
|
26
|
+
|
|
27
|
+
const response = await llm.chat({
|
|
28
|
+
model: llm.model('smart'),
|
|
29
|
+
max_tokens: 1500,
|
|
30
|
+
system: `You are a senior developer creating an execution plan. Be specific, step-by-step, and honest about risks.
|
|
31
|
+
Format the plan as:
|
|
32
|
+
|
|
33
|
+
## Understanding
|
|
34
|
+
(What you understand about the task and codebase context)
|
|
35
|
+
|
|
36
|
+
## Plan
|
|
37
|
+
Step 1: [action] — [file/component affected] — [risk: low|medium|high]
|
|
38
|
+
Step 2: ...
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
## Prerequisites
|
|
42
|
+
(Anything that needs to happen before starting)
|
|
43
|
+
|
|
44
|
+
## Risks & Rollback
|
|
45
|
+
(What could go wrong and how to undo it)
|
|
46
|
+
|
|
47
|
+
## Estimated Effort
|
|
48
|
+
(Quick estimate: minutes/hours)`,
|
|
49
|
+
messages: [{
|
|
50
|
+
role: 'user',
|
|
51
|
+
content: `Create an execution plan for this task:\n\n${task}${contextStr}`,
|
|
52
|
+
}],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
costTracker.record({
|
|
56
|
+
model: llm.model('smart'),
|
|
57
|
+
inputTokens: response.usage.input_tokens,
|
|
58
|
+
outputTokens: response.usage.output_tokens,
|
|
59
|
+
sessionId,
|
|
60
|
+
queryType: 'plan',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
task,
|
|
65
|
+
plan: response.content[0].text,
|
|
66
|
+
generatedAt: new Date().toISOString(),
|
|
67
|
+
approved: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Compact a conversation history into a dense summary.
|
|
73
|
+
* Reduces token usage by replacing old messages with a compressed summary.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array} messages - Array of { role, content } message objects
|
|
76
|
+
* @param {string} sessionId
|
|
77
|
+
*/
|
|
78
|
+
async compact(messages, sessionId = 'default') {
|
|
79
|
+
if (!messages || messages.length < 4) {
|
|
80
|
+
return { compacted: false, reason: 'Too few messages to compact', messages };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Keep last 2 messages verbatim, compact the rest
|
|
84
|
+
const toCompact = messages.slice(0, -2);
|
|
85
|
+
const toKeep = messages.slice(-2);
|
|
86
|
+
|
|
87
|
+
const historyText = toCompact.map(m =>
|
|
88
|
+
`${m.role.toUpperCase()}: ${m.content?.slice(0, 500) || ''}`
|
|
89
|
+
).join('\n\n');
|
|
90
|
+
|
|
91
|
+
const response = await llm.chat({
|
|
92
|
+
model: llm.model('fast'),
|
|
93
|
+
max_tokens: 800,
|
|
94
|
+
messages: [{
|
|
95
|
+
role: 'user',
|
|
96
|
+
content: `Compress this conversation history into a dense, information-preserving summary that captures:
|
|
97
|
+
1. What was asked and answered
|
|
98
|
+
2. Key facts discovered about the codebase
|
|
99
|
+
3. Any decisions made or actions taken
|
|
100
|
+
4. Open questions or unresolved issues
|
|
101
|
+
|
|
102
|
+
Keep it under 400 words. Be specific (include file names, function names, error types).
|
|
103
|
+
|
|
104
|
+
History to compress:
|
|
105
|
+
${historyText}`,
|
|
106
|
+
}],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
costTracker.record({
|
|
110
|
+
model: llm.model('fast'),
|
|
111
|
+
inputTokens: response.usage.input_tokens,
|
|
112
|
+
outputTokens: response.usage.output_tokens,
|
|
113
|
+
sessionId,
|
|
114
|
+
queryType: 'compact',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const summary = response.content[0].text;
|
|
118
|
+
const compactedMessage = {
|
|
119
|
+
role: 'system',
|
|
120
|
+
content: `[Compacted conversation summary]\n${summary}`,
|
|
121
|
+
compacted: true,
|
|
122
|
+
originalCount: toCompact.length,
|
|
123
|
+
compactedAt: new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const compactedMessages = [compactedMessage, ...toKeep];
|
|
127
|
+
|
|
128
|
+
logger.info(`[Compact] ${messages.length} messages → ${compactedMessages.length} (${toCompact.length} compacted)`);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
compacted: true,
|
|
132
|
+
originalCount: messages.length,
|
|
133
|
+
newCount: compactedMessages.length,
|
|
134
|
+
savedMessages: messages.length - compactedMessages.length,
|
|
135
|
+
messages: compactedMessages,
|
|
136
|
+
summary,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Doctor — check environment health
|
|
142
|
+
*/
|
|
143
|
+
async doctor() {
|
|
144
|
+
const checks = [];
|
|
145
|
+
const BashTool = require('../tools/BashTool');
|
|
146
|
+
|
|
147
|
+
const run = async (name, cmd, parse) => {
|
|
148
|
+
try {
|
|
149
|
+
const r = await BashTool.executeOrThrow(cmd, {});
|
|
150
|
+
checks.push({ name, status: 'ok', detail: parse ? parse(r.stdout) : r.stdout.trim() });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
checks.push({ name, status: 'fail', detail: err.message.slice(0, 100) });
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await run('Node.js', 'node --version', v => v.trim());
|
|
157
|
+
await run('npm', 'npm --version', v => `v${v.trim()}`);
|
|
158
|
+
await run('git', 'git --version', v => v.trim());
|
|
159
|
+
await run('ripgrep', 'rg --version', v => v.split('\n')[0]);
|
|
160
|
+
|
|
161
|
+
// Check API key
|
|
162
|
+
// checks.push({
|
|
163
|
+
// name: 'ANTHROPIC_API_KEY',
|
|
164
|
+
// status: process.env.ANTHROPIC_API_KEY ? 'ok' : 'fail',
|
|
165
|
+
// detail: process.env.ANTHROPIC_API_KEY
|
|
166
|
+
// ? `Set (${process.env.ANTHROPIC_API_KEY.slice(0, 10)}...)`
|
|
167
|
+
// : 'Not set — add to .env',
|
|
168
|
+
// });
|
|
169
|
+
|
|
170
|
+
// Check data directory
|
|
171
|
+
const fs = require('fs');
|
|
172
|
+
const dataDir = require('path').join(process.cwd(), 'data');
|
|
173
|
+
checks.push({
|
|
174
|
+
name: 'data/ directory',
|
|
175
|
+
status: fs.existsSync(dataDir) ? 'ok' : 'warn',
|
|
176
|
+
detail: fs.existsSync(dataDir) ? 'Exists' : 'Will be created on first ingest',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Knowledge base
|
|
180
|
+
const store = require('../storage/store');
|
|
181
|
+
const stats = store.getStats();
|
|
182
|
+
checks.push({
|
|
183
|
+
name: 'Knowledge base',
|
|
184
|
+
status: stats.totalDocs > 0 ? 'ok' : 'warn',
|
|
185
|
+
detail: stats.totalDocs > 0
|
|
186
|
+
? `${stats.totalDocs} docs from ${stats.totalFiles} files`
|
|
187
|
+
: 'Empty — run: node cli.js ingest <path>',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const passed = checks.filter(c => c.status === 'ok').length;
|
|
191
|
+
const failed = checks.filter(c => c.status === 'fail').length;
|
|
192
|
+
const warned = checks.filter(c => c.status === 'warn').length;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
checks,
|
|
196
|
+
summary: { passed, failed, warned, total: checks.length },
|
|
197
|
+
healthy: failed === 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = new PlannerEngine();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in plugin: stats-plugin
|
|
4
|
+
* Adds /api/stats/overview — a single endpoint that aggregates everything.
|
|
5
|
+
*/
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: 'stats-plugin',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
description: 'Adds /api/stats/overview endpoint aggregating all system stats',
|
|
10
|
+
async register(app, { store, toolRegistry }) {
|
|
11
|
+
app.get('/api/stats/overview', async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const kbStats = store ? store.getStats() : {};
|
|
14
|
+
const { MemoryManager } = require('../../memory/memoryManager');
|
|
15
|
+
const { TaskManager } = require('../../tasks/taskManager');
|
|
16
|
+
const costTracker = require('../../utils/costTracker');
|
|
17
|
+
res.json({
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
knowledgeBase: kbStats,
|
|
20
|
+
memory: MemoryManager.getStats(),
|
|
21
|
+
tasks: TaskManager.getStats(),
|
|
22
|
+
cost: costTracker.getSummary(),
|
|
23
|
+
tools: toolRegistry ? toolRegistry.count : 0,
|
|
24
|
+
uptime: process.uptime(),
|
|
25
|
+
});
|
|
26
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* A plugin is a plain Node module that exports:
|
|
4
|
+
* { name, version, description, register(app, registry) }
|
|
5
|
+
*
|
|
6
|
+
* register() receives:
|
|
7
|
+
* - app : Express app (add routes)
|
|
8
|
+
* - toolRegistry : add custom tools
|
|
9
|
+
* - app._mcpCtx : shared context (store, indexer, memory, etc.)
|
|
10
|
+
*
|
|
11
|
+
* Built-in plugins ship in src/plugins/builtin/.
|
|
12
|
+
* User plugins drop into <cwd>/plugins/.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const logger = require('../utils/logger');
|
|
18
|
+
|
|
19
|
+
const BUILTIN_DIR = path.join(__dirname, 'builtin');
|
|
20
|
+
const USER_DIR = path.join(process.cwd(), 'plugins');
|
|
21
|
+
const STATE_FILE = path.join(process.cwd(), 'data', 'plugins-state.json');
|
|
22
|
+
|
|
23
|
+
class PluginManager {
|
|
24
|
+
constructor() {
|
|
25
|
+
this._plugins = new Map(); // name → { meta, enabled, instance }
|
|
26
|
+
this._state = this._loadState();
|
|
27
|
+
this._app = null;
|
|
28
|
+
this._ctx = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_loadState() {
|
|
32
|
+
try { if (fs.existsSync(STATE_FILE)) return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')); } catch { }
|
|
33
|
+
return { disabled: [] };
|
|
34
|
+
}
|
|
35
|
+
_saveState() { fs.writeFileSync(STATE_FILE, JSON.stringify(this._state, null, 2)); }
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load and register all plugins. Called once during server boot.
|
|
39
|
+
* @param {Express} app
|
|
40
|
+
* @param {object} ctx — { toolRegistry, store, indexer, memoryManager, ... }
|
|
41
|
+
*/
|
|
42
|
+
async loadAll(app, ctx = {}) {
|
|
43
|
+
this._app = app;
|
|
44
|
+
this._ctx = ctx;
|
|
45
|
+
|
|
46
|
+
// Load built-in plugins first
|
|
47
|
+
if (fs.existsSync(BUILTIN_DIR)) {
|
|
48
|
+
for (const file of fs.readdirSync(BUILTIN_DIR).filter(f => f.endsWith('.js'))) {
|
|
49
|
+
await this._load(path.join(BUILTIN_DIR, file), true);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load user plugins
|
|
54
|
+
if (fs.existsSync(USER_DIR)) {
|
|
55
|
+
for (const file of fs.readdirSync(USER_DIR).filter(f => f.endsWith('.js'))) {
|
|
56
|
+
await this._load(path.join(USER_DIR, file), false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger.info(`[Plugins] Loaded ${this._plugins.size} plugin(s): ${[...this._plugins.keys()].join(', ') || 'none'}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _load(filePath, builtIn = false) {
|
|
64
|
+
try {
|
|
65
|
+
const mod = require(filePath);
|
|
66
|
+
const name = mod.name || path.basename(filePath, '.js');
|
|
67
|
+
const enabled = !this._state.disabled.includes(name);
|
|
68
|
+
|
|
69
|
+
const entry = {
|
|
70
|
+
name,
|
|
71
|
+
version: mod.version || '1.0.0',
|
|
72
|
+
description: mod.description || '',
|
|
73
|
+
filePath,
|
|
74
|
+
builtIn,
|
|
75
|
+
enabled,
|
|
76
|
+
loadedAt: new Date().toISOString(),
|
|
77
|
+
error: null,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (enabled && typeof mod.register === 'function') {
|
|
81
|
+
try {
|
|
82
|
+
await mod.register(this._app, {
|
|
83
|
+
toolRegistry: this._ctx.toolRegistry,
|
|
84
|
+
...this._ctx,
|
|
85
|
+
});
|
|
86
|
+
logger.info(`[Plugins] ✓ ${name}${builtIn ? ' (built-in)' : ''}`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
entry.error = err.message;
|
|
89
|
+
logger.error(`[Plugins] ✗ ${name}: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this._plugins.set(name, entry);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
logger.warn(`[Plugins] Could not load ${filePath}: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Load a single plugin by file path (hot-load at runtime)
|
|
101
|
+
*/
|
|
102
|
+
async loadPlugin(filePath) {
|
|
103
|
+
delete require.cache[require.resolve(filePath)];
|
|
104
|
+
await this._load(filePath);
|
|
105
|
+
return this.get(path.basename(filePath, '.js'));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
enable(name) {
|
|
109
|
+
const p = this._plugins.get(name);
|
|
110
|
+
if (!p) throw new Error(`Plugin not found: ${name}`);
|
|
111
|
+
this._state.disabled = this._state.disabled.filter(n => n !== name);
|
|
112
|
+
p.enabled = true;
|
|
113
|
+
this._saveState();
|
|
114
|
+
return p;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
disable(name) {
|
|
118
|
+
const p = this._plugins.get(name);
|
|
119
|
+
if (!p) throw new Error(`Plugin not found: ${name}`);
|
|
120
|
+
if (!this._state.disabled.includes(name)) this._state.disabled.push(name);
|
|
121
|
+
p.enabled = false;
|
|
122
|
+
this._saveState();
|
|
123
|
+
logger.info(`[Plugins] Disabled: ${name} (restart to fully unload)`);
|
|
124
|
+
return p;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
list() {
|
|
128
|
+
return [...this._plugins.values()].map(({ filePath, ...rest }) => rest);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get(name) {
|
|
132
|
+
const p = this._plugins.get(name);
|
|
133
|
+
if (!p) return null;
|
|
134
|
+
const { filePath, ...rest } = p;
|
|
135
|
+
return rest;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getStats() {
|
|
139
|
+
const all = this.list();
|
|
140
|
+
return { total: all.length, enabled: all.filter(p => p.enabled).length, disabled: all.filter(p => !p.enabled).length, errors: all.filter(p => p.error).length };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = new PluginManager();
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* A dedicated prompt engineering system.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Analyse a prompt for weaknesses
|
|
7
|
+
* - Improve a prompt automatically
|
|
8
|
+
* - Generate prompts from a task description
|
|
9
|
+
* - A/B test two prompts against a benchmark
|
|
10
|
+
* - Prompt template library with variables
|
|
11
|
+
* - Chain-of-thought injection
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const llm = require('../utils/llmClient');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const costTracker = require('../utils/costTracker');
|
|
18
|
+
const logger = require('../utils/logger');
|
|
19
|
+
|
|
20
|
+
const LIBRARY_FILE = path.join(process.cwd(), 'data', 'prompt-library.json');
|
|
21
|
+
|
|
22
|
+
// ── Built-in prompt templates ─────────────────────────────────────────────────
|
|
23
|
+
const BUILTIN_TEMPLATES = {
|
|
24
|
+
'chain-of-thought': {
|
|
25
|
+
name: 'chain-of-thought',
|
|
26
|
+
description: 'Adds structured reasoning steps before the answer',
|
|
27
|
+
template: `{prompt}
|
|
28
|
+
|
|
29
|
+
Think through this step by step:
|
|
30
|
+
1. What is being asked?
|
|
31
|
+
2. What information do I have?
|
|
32
|
+
3. What approach should I take?
|
|
33
|
+
4. Work through the solution
|
|
34
|
+
5. Verify the answer
|
|
35
|
+
|
|
36
|
+
Answer:`,
|
|
37
|
+
variables: ['prompt'],
|
|
38
|
+
},
|
|
39
|
+
'role-expert': {
|
|
40
|
+
name: 'role-expert',
|
|
41
|
+
description: 'Frame Anu as a domain expert',
|
|
42
|
+
template: `You are a world-class {domain} expert with 20+ years of experience.
|
|
43
|
+
You give precise, evidence-based answers grounded in real-world practice.
|
|
44
|
+
You never guess — if you are uncertain, you say so clearly.
|
|
45
|
+
|
|
46
|
+
Task: {task}`,
|
|
47
|
+
variables: ['domain', 'task'],
|
|
48
|
+
},
|
|
49
|
+
'few-shot': {
|
|
50
|
+
name: 'few-shot',
|
|
51
|
+
description: 'Provide examples before the actual task',
|
|
52
|
+
template: `Here are examples of the task:
|
|
53
|
+
|
|
54
|
+
Example 1:
|
|
55
|
+
Input: {example1_input}
|
|
56
|
+
Output: {example1_output}
|
|
57
|
+
|
|
58
|
+
Example 2:
|
|
59
|
+
Input: {example2_input}
|
|
60
|
+
Output: {example2_output}
|
|
61
|
+
|
|
62
|
+
Now do the same for:
|
|
63
|
+
Input: {actual_input}
|
|
64
|
+
Output:`,
|
|
65
|
+
variables: ['example1_input', 'example1_output', 'example2_input', 'example2_output', 'actual_input'],
|
|
66
|
+
},
|
|
67
|
+
'structured-output': {
|
|
68
|
+
name: 'structured-output',
|
|
69
|
+
description: 'Force structured JSON output',
|
|
70
|
+
template: `{task}
|
|
71
|
+
|
|
72
|
+
Respond ONLY with a JSON object in this exact format (no markdown, no explanation):
|
|
73
|
+
{schema}`,
|
|
74
|
+
variables: ['task', 'schema'],
|
|
75
|
+
},
|
|
76
|
+
'critique-and-revise': {
|
|
77
|
+
name: 'critique-and-revise',
|
|
78
|
+
description: 'Self-critique and improve an answer',
|
|
79
|
+
template: `{task}
|
|
80
|
+
|
|
81
|
+
First, provide your initial answer.
|
|
82
|
+
Then, critique it: what's missing, what could be wrong, what could be clearer?
|
|
83
|
+
Finally, provide a revised, improved answer incorporating your critique.
|
|
84
|
+
|
|
85
|
+
Initial answer:
|
|
86
|
+
[your first attempt]
|
|
87
|
+
|
|
88
|
+
Critique:
|
|
89
|
+
[what you'd improve]
|
|
90
|
+
|
|
91
|
+
Revised answer:
|
|
92
|
+
[your improved answer]`,
|
|
93
|
+
variables: ['task'],
|
|
94
|
+
},
|
|
95
|
+
'least-to-most': {
|
|
96
|
+
name: 'least-to-most',
|
|
97
|
+
description: 'Break down into simpler sub-problems first',
|
|
98
|
+
template: `To solve: {problem}
|
|
99
|
+
|
|
100
|
+
First, identify the simpler sub-problems that lead to the solution.
|
|
101
|
+
Solve each sub-problem in order, using each answer to help with the next.
|
|
102
|
+
|
|
103
|
+
Sub-problems:
|
|
104
|
+
1. [simplest piece]
|
|
105
|
+
2. [next piece]
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
Now solve each:`,
|
|
109
|
+
variables: ['problem'],
|
|
110
|
+
},
|
|
111
|
+
'react-agent': {
|
|
112
|
+
name: 'react-agent',
|
|
113
|
+
description: 'ReAct (Reason + Act) prompting pattern',
|
|
114
|
+
template: `Task: {task}
|
|
115
|
+
|
|
116
|
+
Use this format:
|
|
117
|
+
Thought: [your reasoning about the current situation]
|
|
118
|
+
Action: [action to take from: {available_actions}]
|
|
119
|
+
Observation: [result of the action]
|
|
120
|
+
... (repeat until done)
|
|
121
|
+
Thought: I now have enough information to answer
|
|
122
|
+
Answer: [final answer]`,
|
|
123
|
+
variables: ['task', 'available_actions'],
|
|
124
|
+
},
|
|
125
|
+
'socratic': {
|
|
126
|
+
name: 'socratic',
|
|
127
|
+
description: 'Lead the user to the answer through questions',
|
|
128
|
+
template: `Instead of directly answering "{question}", help the user discover the answer themselves.
|
|
129
|
+
|
|
130
|
+
Ask 2-3 probing questions that guide them toward understanding.
|
|
131
|
+
After they respond, lead them closer to the answer.
|
|
132
|
+
Only reveal the full answer if they are completely stuck.`,
|
|
133
|
+
variables: ['question'],
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
class PromptEngineer {
|
|
138
|
+
constructor() {
|
|
139
|
+
this._library = this._loadLibrary();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_loadLibrary() {
|
|
143
|
+
try { if (fs.existsSync(LIBRARY_FILE)) return JSON.parse(fs.readFileSync(LIBRARY_FILE, 'utf-8')); } catch { }
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
_saveLibrary() { fs.writeFileSync(LIBRARY_FILE, JSON.stringify(this._library, null, 2)); }
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Analyse a prompt and identify its weaknesses
|
|
150
|
+
*/
|
|
151
|
+
async analyse(prompt, opts = {}) {
|
|
152
|
+
logger.info(`[PromptEngineer] Analysing prompt (${prompt.length} chars)`);
|
|
153
|
+
const response = await llm.chat({
|
|
154
|
+
model: llm.model('smart'), max_tokens: 800,
|
|
155
|
+
system: `You are a prompt engineering expert. Analyse prompts with precision and suggest concrete improvements.`,
|
|
156
|
+
messages: [{ role: 'user', content: `Analyse this prompt and score it 1-10 on each dimension:\n\n---\n${prompt}\n---\n\nReturn JSON:\n{"scores":{"clarity":0,"specificity":0,"context":0,"output_format":0,"role_framing":0,"examples":0},"weaknesses":["..."],"strengths":["..."],"overall_score":0}` }],
|
|
157
|
+
});
|
|
158
|
+
costTracker.record({ model: llm.model('smart'), inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, sessionId: opts.sessionId || 'prompt-engineer', queryType: 'prompt-analyse' });
|
|
159
|
+
try { return JSON.parse(response.content[0].text.replace(/```json\n?|\n?```/g, '').trim()); }
|
|
160
|
+
catch { return { raw: response.content[0].text }; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Automatically improve a prompt
|
|
165
|
+
*/
|
|
166
|
+
async improve(prompt, opts = {}) {
|
|
167
|
+
const { goal = '', style = 'concise' } = opts;
|
|
168
|
+
logger.info(`[PromptEngineer] Improving prompt`);
|
|
169
|
+
|
|
170
|
+
const analysis = await this.analyse(prompt, opts);
|
|
171
|
+
|
|
172
|
+
const response = await llm.chat({
|
|
173
|
+
model: llm.model('smart'), max_tokens: 1200,
|
|
174
|
+
system: `You are an expert prompt engineer. Rewrite prompts to be clearer, more specific, and more effective.`,
|
|
175
|
+
messages: [{ role: 'user', content: `Original prompt:\n---\n${prompt}\n---\n\nWeaknesses identified: ${JSON.stringify(analysis.weaknesses || [])}\n\n${goal ? `Goal: ${goal}` : ''}\nStyle: ${style}\n\nRewrite the prompt to fix all weaknesses. Return:\n{"improved_prompt":"...","changes_made":["..."],"expected_improvement":"..."}` }],
|
|
176
|
+
});
|
|
177
|
+
costTracker.record({ model: llm.model('smart'), inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, sessionId: opts.sessionId || 'prompt-engineer', queryType: 'prompt-improve' });
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const result = JSON.parse(response.content[0].text.replace(/```json\n?|\n?```/g, '').trim());
|
|
181
|
+
return { ...result, original: prompt, analysis };
|
|
182
|
+
} catch {
|
|
183
|
+
return { improved_prompt: response.content[0].text, original: prompt };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate a prompt from a task description
|
|
189
|
+
*/
|
|
190
|
+
async generate(taskDescription, opts = {}) {
|
|
191
|
+
const { type = 'instruction', model = 'smart', includeExamples = false } = opts;
|
|
192
|
+
logger.info(`[PromptEngineer] Generating prompt for: ${taskDescription.slice(0, 60)}`);
|
|
193
|
+
|
|
194
|
+
const response = await llm.chat({
|
|
195
|
+
model: llm.model('smart'), max_tokens: 1000,
|
|
196
|
+
messages: [{ role: 'user', content: `Generate an optimal ${type} prompt for this task:\n"${taskDescription}"\n\nTarget model: ${model}\nInclude examples: ${includeExamples}\n\nReturn:\n{"system_prompt":"...","user_prompt_template":"...","key_techniques":["..."],"variables":["..."]}` }],
|
|
197
|
+
});
|
|
198
|
+
costTracker.record({ model: llm.model('smart'), inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, sessionId: opts.sessionId || 'prompt-engineer', queryType: 'prompt-generate' });
|
|
199
|
+
try { return JSON.parse(response.content[0].text.replace(/```json\n?|\n?```/g, '').trim()); }
|
|
200
|
+
catch { return { user_prompt_template: response.content[0].text }; }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A/B test two prompts against a test case
|
|
205
|
+
*/
|
|
206
|
+
async abTest(promptA, promptB, testInput, opts = {}) {
|
|
207
|
+
logger.info(`[PromptEngineer] A/B testing two prompts`);
|
|
208
|
+
|
|
209
|
+
const [resA, resB] = await Promise.all([
|
|
210
|
+
llm.chat({ model: llm.model('fast'), max_tokens: 500, messages: [{ role: 'user', content: `${promptA}\n\nInput: ${testInput}` }] }),
|
|
211
|
+
llm.chat({ model: llm.model('fast'), max_tokens: 500, messages: [{ role: 'user', content: `${promptB}\n\nInput: ${testInput}` }] }),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const outputA = resA.content[0].text;
|
|
215
|
+
const outputB = resB.content[0].text;
|
|
216
|
+
|
|
217
|
+
// Judge which is better
|
|
218
|
+
const judge = await llm.chat({
|
|
219
|
+
model: llm.model('fast'), max_tokens: 300,
|
|
220
|
+
messages: [{ role: 'user', content: `Compare these two AI outputs for the task: "${testInput}"\n\nOutput A:\n${outputA}\n\nOutput B:\n${outputB}\n\nWhich is better and why? Return JSON:\n{"winner":"A|B|tie","score_a":0,"score_b":0,"reasoning":"...","key_difference":"..."}` }],
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
for (const r of [resA, resB, judge]) {
|
|
224
|
+
costTracker.record({ model: llm.model('fast'), inputTokens: r.usage.input_tokens, outputTokens: r.usage.output_tokens, sessionId: opts.sessionId || 'prompt-engineer', queryType: 'prompt-abtest' });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const verdict = JSON.parse(judge.content[0].text.replace(/```json\n?|\n?```/g, '').trim());
|
|
229
|
+
return { promptA, promptB, testInput, outputA, outputB, verdict };
|
|
230
|
+
} catch {
|
|
231
|
+
return { promptA, promptB, outputA, outputB, verdict: { raw: judge.content[0].text } };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Apply a prompt template with variable substitution
|
|
237
|
+
*/
|
|
238
|
+
applyTemplate(templateName, variables = {}) {
|
|
239
|
+
const t = this._library[templateName] || BUILTIN_TEMPLATES[templateName];
|
|
240
|
+
if (!t) throw new Error(`Template not found: ${templateName}. Available: ${this.listTemplates().map(t => t.name).join(', ')}`);
|
|
241
|
+
let result = t.template;
|
|
242
|
+
for (const [k, v] of Object.entries(variables)) {
|
|
243
|
+
result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
|
|
244
|
+
}
|
|
245
|
+
const missing = (result.match(/\{(\w+)\}/g) || []);
|
|
246
|
+
if (missing.length) logger.warn(`[PromptEngineer] Unfilled variables: ${missing.join(', ')}`);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Inject chain-of-thought reasoning into any prompt
|
|
252
|
+
*/
|
|
253
|
+
injectCoT(prompt, style = 'standard') {
|
|
254
|
+
const injections = {
|
|
255
|
+
standard: `\n\nThink through this carefully, step by step, before giving your final answer.`,
|
|
256
|
+
detailed: `\n\nBefore answering:\n1. Understand what's being asked\n2. Identify relevant information\n3. Consider edge cases\n4. Work through the solution\n5. Verify your answer\n\nNow answer:`,
|
|
257
|
+
socratic: `\n\nAsk yourself: What do I know? What don't I know? What's the simplest path? Then answer:`,
|
|
258
|
+
zero_shot: `\n\nLet's think about this step by step:`,
|
|
259
|
+
};
|
|
260
|
+
return prompt + (injections[style] || injections.standard);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Save a custom template to the library */
|
|
264
|
+
saveTemplate(name, description, template, variables = []) {
|
|
265
|
+
if (BUILTIN_TEMPLATES[name]) throw new Error(`Cannot override built-in template: ${name}`);
|
|
266
|
+
this._library[name] = { name, description, template, variables, custom: true, createdAt: new Date().toISOString() };
|
|
267
|
+
this._saveLibrary();
|
|
268
|
+
return this._library[name];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
deleteTemplate(name) {
|
|
272
|
+
if (BUILTIN_TEMPLATES[name]) throw new Error('Cannot delete built-in templates');
|
|
273
|
+
if (!this._library[name]) throw new Error(`Template not found: ${name}`);
|
|
274
|
+
delete this._library[name]; this._saveLibrary(); return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
listTemplates() {
|
|
278
|
+
return [
|
|
279
|
+
...Object.values(BUILTIN_TEMPLATES).map(t => ({ ...t, builtIn: true })),
|
|
280
|
+
...Object.values(this._library).map(t => ({ ...t, builtIn: false })),
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getTemplate(name) {
|
|
285
|
+
return this._library[name] || BUILTIN_TEMPLATES[name] || null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = new PromptEngineer();
|