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,212 @@
1
+ /**
2
+ * Full git workflow integration: status, diff, commit, branch, log, review.
3
+ */
4
+
5
+ const BashTool = require('./BashTool');
6
+ const llm = require('../utils/llmClient');
7
+ const logger = require('../utils/logger');
8
+ const costTracker = require('../utils/costTracker');
9
+
10
+ class GitTool {
11
+ /**
12
+ * Check if a directory is inside a git repository
13
+ */
14
+ async isGitRepo(cwd = process.cwd()) {
15
+ const result = await BashTool.execute('git rev-parse --is-inside-work-tree', { cwd, approved: true });
16
+ return result.exitCode === 0;
17
+ }
18
+
19
+ /**
20
+ * Get full git status
21
+ */
22
+ async status(cwd = process.cwd()) {
23
+ const isRepo = await this.isGitRepo(cwd);
24
+ if (!isRepo) return { branch: '(not a git repo)', ahead: 0, behind: 0, staged: [], unstaged: [], untracked: [], recentCommits: [], error: 'Not a git repository' };
25
+
26
+ const [statusResult, branchResult] = await Promise.all([
27
+ BashTool.execute('git status --porcelain=v2 --branch', { cwd, approved: true }),
28
+ BashTool.execute('git log --oneline -5', { cwd, approved: true }),
29
+ ]);
30
+
31
+ return this._parseStatus(statusResult.stdout || '', branchResult.stdout || '');
32
+ }
33
+
34
+ _parseStatus(statusOutput, logOutput) {
35
+ const lines = statusOutput.split('\n').filter(Boolean);
36
+ const result = { branch: '', ahead: 0, behind: 0, staged: [], unstaged: [], untracked: [], recentCommits: [] };
37
+
38
+ for (const line of lines) {
39
+ if (line.startsWith('# branch.oid')) continue;
40
+ if (line.startsWith('# branch.head')) result.branch = line.split(' ')[2];
41
+ if (line.startsWith('# branch.ab')) {
42
+ const m = line.match(/\+(\d+)\s+-(\d+)/);
43
+ if (m) { result.ahead = parseInt(m[1]); result.behind = parseInt(m[2]); }
44
+ }
45
+ if (line.startsWith('1 ') || line.startsWith('2 ')) {
46
+ const xy = line.slice(2, 4);
47
+ const file = line.split('\t').pop();
48
+ if (xy[0] !== '.' && xy[0] !== '?') result.staged.push({ status: xy[0], file });
49
+ if (xy[1] !== '.' && xy[1] !== '?') result.unstaged.push({ status: xy[1], file });
50
+ }
51
+ if (line.startsWith('? ')) result.untracked.push(line.slice(2));
52
+ }
53
+
54
+ result.recentCommits = logOutput.split('\n').filter(Boolean).slice(0, 5);
55
+ return result;
56
+ }
57
+
58
+ /**
59
+ * Get diff (staged, unstaged, or specific file)
60
+ */
61
+ async diff(options = {}) {
62
+ const { cwd = process.cwd(), staged = false, file, stat = false } = options;
63
+ const isRepo = await this.isGitRepo(cwd);
64
+ if (!isRepo) return { diff: '', hasChanges: false, staged, error: 'Not a git repository' };
65
+
66
+ const flags = [
67
+ staged ? '--staged' : '',
68
+ stat ? '--stat' : '',
69
+ '--no-color',
70
+ file ? `-- ${JSON.stringify(file)}` : '',
71
+ ].filter(Boolean).join(' ');
72
+
73
+ const result = await BashTool.execute(`git diff ${flags}`, { cwd, approved: true });
74
+ return {
75
+ diff: result.stdout || '',
76
+ hasChanges: (result.stdout || '').trim().length > 0,
77
+ staged,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Stage files and create a commit with AI-generated message
83
+ */
84
+ async commit(options = {}) {
85
+ const { cwd = process.cwd(), files = ['.'], message, autoMessage = false } = options;
86
+ const isRepo = await this.isGitRepo(cwd);
87
+ if (!isRepo) return { success: false, message: 'Not a git repository' };
88
+
89
+ // Stage files
90
+ const stageCmd = `git add ${files.map(f => JSON.stringify(f)).join(' ')}`;
91
+ const stageResult = await BashTool.execute(stageCmd, { cwd, approved: true });
92
+ if (stageResult.exitCode !== 0) return { success: false, message: stageResult.stderr || 'git add failed' };
93
+
94
+ // Get staged diff for context
95
+ const diffResult = await this.diff({ cwd, staged: true });
96
+ if (!diffResult.hasChanges) return { success: false, message: 'Nothing staged to commit' };
97
+
98
+ let commitMessage = message;
99
+ if (!commitMessage || autoMessage) {
100
+ commitMessage = await this._generateCommitMessage(diffResult.diff);
101
+ }
102
+
103
+ const result = await BashTool.execute(`git commit -m ${JSON.stringify(commitMessage)}`, { cwd, approved: true });
104
+ if (result.exitCode !== 0) return { success: false, message: result.stderr || 'commit failed' };
105
+
106
+ return {
107
+ success: true,
108
+ message: commitMessage,
109
+ output: result.stdout,
110
+ autoGenerated: !message || autoMessage,
111
+ };
112
+ }
113
+
114
+ async _generateCommitMessage(diff) {
115
+ if (!diff || diff.trim().length === 0) return 'chore: update files';
116
+ const truncatedDiff = diff.slice(0, 4000);
117
+ try {
118
+ const response = await llm.chat({
119
+ model: llm.model('fast'),
120
+ max_tokens: 150,
121
+ messages: [{
122
+ role: 'user',
123
+ content: `Generate a concise git commit message in conventional commits format (type: description) for this diff. Reply ONLY with the commit message, no explanation.\n\nDiff:\n${truncatedDiff}`,
124
+ }],
125
+ });
126
+ costTracker.record({
127
+ model: llm.model('fast'),
128
+ inputTokens: response.usage.input_tokens,
129
+ outputTokens: response.usage.output_tokens,
130
+ queryType: 'git-commit',
131
+ });
132
+ return response.content[0].text.trim().replace(/^["']|["']$/g, '');
133
+ } catch {
134
+ return 'chore: update files';
135
+ }
136
+ }
137
+
138
+ /**
139
+ * AI-powered code review of staged changes or a file
140
+ */
141
+ async review(options = {}) {
142
+ const { cwd = process.cwd(), staged = false, file, focus = '' } = options;
143
+ const diffResult = await this.diff({ cwd, staged, file });
144
+ if (!diffResult.hasChanges) return { review: 'No changes to review.', hasIssues: false };
145
+
146
+ const diff = diffResult.diff.slice(0, 6000);
147
+ const focusInstruction = focus
148
+ ? `Focus especially on: ${focus}`
149
+ : 'Focus on: bugs, security issues, performance problems, code style, and maintainability.';
150
+
151
+ try {
152
+ const response = await llm.chat({
153
+ model: llm.model('smart'),
154
+ max_tokens: 1500,
155
+ system: `You are a senior software engineer conducting a code review. Be direct and specific.
156
+ Format your review as:
157
+ ## Summary
158
+ (1-2 sentences)
159
+
160
+ ## Issues Found
161
+ (list any bugs, security problems, or critical issues — be specific with line references)
162
+
163
+ ## Suggestions
164
+ (improvements, not blockers)
165
+
166
+ ## Verdict
167
+ LGTM | Needs Changes | Blocking Issues`,
168
+ messages: [{ role: 'user', content: `Review this diff:\n${focusInstruction}\n\n\`\`\`diff\n${diff}\n\`\`\`` }],
169
+ });
170
+ costTracker.record({ model: llm.model('smart'), inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, queryType: 'git-review' });
171
+ const reviewText = response.content[0].text;
172
+ return { review: reviewText, hasIssues: /blocking issues|needs changes/i.test(reviewText), diff: diffResult.diff };
173
+ } catch (e) {
174
+ return { review: `Review failed: ${e.message}`, hasIssues: false };
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Get commit log
180
+ */
181
+ async log(options = {}) {
182
+ const { cwd = process.cwd(), limit = 10, file, oneline = false } = options;
183
+ const isRepo = await this.isGitRepo(cwd);
184
+ if (!isRepo) return oneline ? [] : [];
185
+
186
+ const format = oneline ? '--oneline' : '--pretty=format:"%H|%an|%ae|%ad|%s" --date=short';
187
+ const fileFlag = file ? `-- ${JSON.stringify(file)}` : '';
188
+ const result = await BashTool.execute(`git log -${limit} ${format} ${fileFlag}`, { cwd, approved: true });
189
+ if (result.exitCode !== 0) return [];
190
+ if (oneline) return result.stdout.split('\n').filter(Boolean);
191
+ return result.stdout.split('\n').filter(Boolean).map(line => {
192
+ const [hash, author, email, date, ...subjectParts] = line.replace(/^"|"$/g, '').split('|');
193
+ return { hash: hash?.slice(0, 8), author, email, date, subject: subjectParts.join('|') };
194
+ });
195
+ }
196
+
197
+ /**
198
+ * List branches
199
+ */
200
+ async branches(cwd = process.cwd()) {
201
+ const isRepo = await this.isGitRepo(cwd);
202
+ if (!isRepo) return [];
203
+ const result = await BashTool.execute('git branch -a --sort=-committerdate', { cwd, approved: true });
204
+ if (result.exitCode !== 0) return [];
205
+ return result.stdout.split('\n').filter(Boolean).map(b => ({
206
+ name: b.replace(/^\*?\s+/, '').trim(),
207
+ current: b.startsWith('*'),
208
+ }));
209
+ }
210
+ }
211
+
212
+ module.exports = new GitTool();
@@ -0,0 +1,199 @@
1
+ /**
2
+ * GrepTool — ripgrep-based content search with fallback.
3
+ * Searches for patterns across the codebase quickly without needing to ingest first.
4
+ */
5
+
6
+ const { exec } = require('child_process');
7
+ const { promisify } = require('util');
8
+ const execAsync = promisify(exec);
9
+ const path = require('path');
10
+ const logger = require('../utils/logger');
11
+
12
+ // Check if ripgrep is available
13
+ let RG_AVAILABLE = null;
14
+ async function checkRg() {
15
+ if (RG_AVAILABLE !== null) return RG_AVAILABLE;
16
+ try {
17
+ await execAsync('rg --version');
18
+ RG_AVAILABLE = true;
19
+ } catch {
20
+ RG_AVAILABLE = false;
21
+ }
22
+ return RG_AVAILABLE;
23
+ }
24
+
25
+ class GrepTool {
26
+ /**
27
+ * Search for a pattern in files
28
+ * @param {string} pattern - regex or literal string
29
+ * @param {object} opts
30
+ * cwd - directory to search in
31
+ * glob - file glob filter (e.g. '*.js')
32
+ * ignoreCase - case-insensitive
33
+ * maxResults - limit results
34
+ * contextLines - lines of context around each match
35
+ * literal - treat pattern as literal string (no regex)
36
+ */
37
+ async search(pattern, opts = {}) {
38
+ const {
39
+ cwd = process.cwd(),
40
+ glob,
41
+ ignoreCase = false,
42
+ maxResults = 50,
43
+ contextLines = 2,
44
+ literal = false,
45
+ } = opts;
46
+
47
+ const useRg = await checkRg();
48
+
49
+ if (useRg) {
50
+ return this._rgSearch(pattern, { cwd, glob, ignoreCase, maxResults, contextLines, literal });
51
+ } else {
52
+ return this._nativeSearch(pattern, { cwd, glob, ignoreCase, maxResults });
53
+ }
54
+ }
55
+
56
+ async _rgSearch(pattern, opts) {
57
+ const { cwd, glob, ignoreCase, maxResults, contextLines, literal } = opts;
58
+
59
+ const flags = [
60
+ '--json',
61
+ `--max-count=${maxResults}`,
62
+ `--context=${contextLines}`,
63
+ ignoreCase ? '--ignore-case' : '',
64
+ literal ? '--fixed-strings' : '',
65
+ glob ? `--glob '${glob}'` : '',
66
+ '--hidden',
67
+ '--no-follow',
68
+ // Standard ignores
69
+ '--glob !node_modules',
70
+ '--glob !.git',
71
+ '--glob !dist',
72
+ '--glob !build',
73
+ '--glob !*.min.js',
74
+ ].filter(Boolean).join(' ');
75
+
76
+ const cmd = `rg ${flags} ${JSON.stringify(pattern)}`;
77
+
78
+ try {
79
+ const { stdout } = await execAsync(cmd, { cwd, maxBuffer: 5 * 1024 * 1024 });
80
+ return this._parseRgJson(stdout, maxResults);
81
+ } catch (err) {
82
+ if (err.code === 1) return { matches: [], total: 0, tool: 'ripgrep', note: 'No matches found' };
83
+ throw new Error(`ripgrep error: ${err.message}`);
84
+ }
85
+ }
86
+
87
+ _parseRgJson(output, maxResults) {
88
+ const lines = output.trim().split('\n').filter(Boolean);
89
+ const matches = [];
90
+ let total = 0;
91
+
92
+ for (const line of lines) {
93
+ try {
94
+ const obj = JSON.parse(line);
95
+ if (obj.type === 'match') {
96
+ total++;
97
+ if (matches.length < maxResults) {
98
+ matches.push({
99
+ file: obj.data.path.text,
100
+ lineNumber: obj.data.line_number,
101
+ line: obj.data.lines.text.trimEnd(),
102
+ submatches: obj.data.submatches?.map(s => s.match?.text) || [],
103
+ });
104
+ }
105
+ }
106
+ } catch { }
107
+ }
108
+
109
+ return { matches, total, tool: 'ripgrep' };
110
+ }
111
+
112
+ async _nativeSearch(pattern, opts) {
113
+ const { cwd, ignoreCase, maxResults } = opts;
114
+ const { glob: globModule } = require('glob');
115
+ const fs = require('fs');
116
+
117
+ const files = await globModule('**/*', {
118
+ cwd,
119
+ absolute: true,
120
+ nodir: true,
121
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
122
+ });
123
+
124
+ const regex = new RegExp(pattern, ignoreCase ? 'gi' : 'g');
125
+ const matches = [];
126
+
127
+ for (const file of files) {
128
+ if (matches.length >= maxResults) break;
129
+ try {
130
+ const content = fs.readFileSync(file, 'utf-8');
131
+ const lines = content.split('\n');
132
+ lines.forEach((line, idx) => {
133
+ if (matches.length < maxResults && regex.test(line)) {
134
+ matches.push({
135
+ file: path.relative(cwd, file),
136
+ lineNumber: idx + 1,
137
+ line: line.trimEnd(),
138
+ submatches: [],
139
+ });
140
+ }
141
+ regex.lastIndex = 0;
142
+ });
143
+ } catch { }
144
+ }
145
+
146
+ return { matches, total: matches.length, tool: 'native-grep' };
147
+ }
148
+
149
+ /**
150
+ * Quick search: find all definitions of a symbol (function, class, const)
151
+ */
152
+ async findDefinitions(symbol, cwd = process.cwd()) {
153
+ const patterns = [
154
+ `function ${symbol}`,
155
+ `class ${symbol}`,
156
+ `const ${symbol}\\s*=`,
157
+ `let ${symbol}\\s*=`,
158
+ `var ${symbol}\\s*=`,
159
+ `${symbol}\\s*\\(`, // method definition
160
+ `exports\\.${symbol}`,
161
+ `module\\.exports.*${symbol}`,
162
+ ];
163
+
164
+ const allMatches = [];
165
+ for (const p of patterns) {
166
+ try {
167
+ const result = await this.search(p, { cwd, maxResults: 10 });
168
+ allMatches.push(...result.matches);
169
+ } catch { }
170
+ }
171
+
172
+ // Deduplicate by file+line
173
+ const seen = new Set();
174
+ return allMatches.filter(m => {
175
+ const key = `${m.file}:${m.lineNumber}`;
176
+ if (seen.has(key)) return false;
177
+ seen.add(key);
178
+ return true;
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Find all imports/requires of a module
184
+ */
185
+ async findImports(moduleName, cwd = process.cwd()) {
186
+ const pattern = `(import|require).*['"]${moduleName}['"]`;
187
+ return this.search(pattern, { cwd, maxResults: 30 });
188
+ }
189
+
190
+ /**
191
+ * Find all TODO/FIXME/HACK/BUG comments
192
+ */
193
+ async findTodos(cwd = process.cwd()) {
194
+ const pattern = '(TODO|FIXME|HACK|BUG|XXX|NOTE)\\s*[:\\-]?';
195
+ return this.search(pattern, { cwd, maxResults: 100, ignoreCase: true });
196
+ }
197
+ }
198
+
199
+ module.exports = new GrepTool();