dev-mcp-server 0.0.2
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 +68 -0
- package/README.md +333 -0
- package/cli.js +248 -0
- package/package.json +60 -0
- package/src/api/routes/ingest.js +69 -0
- package/src/api/routes/knowledge.js +65 -0
- package/src/api/routes/query.js +105 -0
- package/src/api/server.js +91 -0
- package/src/core/indexer.js +171 -0
- package/src/core/ingester.js +155 -0
- package/src/core/queryEngine.js +236 -0
- package/src/storage/store.js +125 -0
- package/src/utils/fileParser.js +183 -0
- package/src/utils/llmClient.js +206 -0
- package/src/utils/logger.js +28 -0
package/.env.example
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════
|
|
2
|
+
# Dev MCP Server — environment configuration
|
|
3
|
+
# Copy this file to .env and fill in your values.
|
|
4
|
+
# ═══════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
# ── LLM Provider ───────────────────────────────────────────────
|
|
7
|
+
# Which LLM backend to use.
|
|
8
|
+
# Options: anthropic | ollama | azure
|
|
9
|
+
# Default: anthropic
|
|
10
|
+
LLM_PROVIDER=anthropic
|
|
11
|
+
|
|
12
|
+
# Model / deployment name override.
|
|
13
|
+
# If unset, the default per provider is used:
|
|
14
|
+
# anthropic → claude-opus-4-5
|
|
15
|
+
# ollama → llama3
|
|
16
|
+
# azure → MUST be set (matches your Azure deployment name)
|
|
17
|
+
# LLM_MODEL=claude-opus-4-5
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── Anthropic ──────────────────────────────────────────────────
|
|
21
|
+
# Required when LLM_PROVIDER=anthropic
|
|
22
|
+
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── Ollama ─────────────────────────────────────────────────────
|
|
26
|
+
# Required when LLM_PROVIDER=ollama
|
|
27
|
+
# Ensure Ollama is running: https://ollama.com
|
|
28
|
+
# Pull a model first: ollama pull llama3
|
|
29
|
+
#
|
|
30
|
+
# OLLAMA_BASE_URL=http://localhost:11434
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Azure OpenAI ───────────────────────────────────────────────
|
|
34
|
+
# Required when LLM_PROVIDER=azure
|
|
35
|
+
#
|
|
36
|
+
# Your Azure OpenAI resource endpoint:
|
|
37
|
+
# AZURE_OPENAI_ENDPOINT=https://<your-resource>.openai.azure.com
|
|
38
|
+
#
|
|
39
|
+
# API key from Azure portal → Keys and Endpoint:
|
|
40
|
+
# AZURE_OPENAI_API_KEY=your-azure-key-here
|
|
41
|
+
#
|
|
42
|
+
# The name of the deployment you created in Azure AI Studio
|
|
43
|
+
# (this is also used as the model parameter):
|
|
44
|
+
# AZURE_OPENAI_DEPLOYMENT=gpt-4o
|
|
45
|
+
#
|
|
46
|
+
# API version — default is fine unless you need a specific one:
|
|
47
|
+
# AZURE_OPENAI_API_VERSION=2024-05-01-preview
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Ingestion — ignore rules ────────────────────────────────────
|
|
51
|
+
# Whether to read the project's .gitignore and add its patterns
|
|
52
|
+
# to the ingest ignore list. Set to "false" to disable.
|
|
53
|
+
# Default: true
|
|
54
|
+
# INGEST_USE_GITIGNORE=true
|
|
55
|
+
|
|
56
|
+
# Extra glob patterns to ignore during ingest (comma-separated).
|
|
57
|
+
# Added on top of the built-in baseline and .gitignore patterns.
|
|
58
|
+
# Example: INGEST_EXTRA_IGNORE=**/fixtures/**,**/__snapshots__/**
|
|
59
|
+
# INGEST_EXTRA_IGNORE=
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── Server ─────────────────────────────────────────────────────
|
|
63
|
+
PORT=3000
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Logging ────────────────────────────────────────────────────
|
|
67
|
+
# Options: error | warn | info | debug
|
|
68
|
+
LOG_LEVEL=info
|
package/README.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# 🧠 Dev MCP Server — Model Context Platform
|
|
2
|
+
|
|
3
|
+
> AI that understands **your** codebase, not just the internet.
|
|
4
|
+
|
|
5
|
+
Inspired by *"How I Built an MCP Server That Made Developers Faster and Work Easier"* — a full implementation of the **Model Context Platform** concept: instead of generic AI answers, every response is grounded in your actual code, error logs, API behavior, and bug history.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Problem It Solves
|
|
10
|
+
|
|
11
|
+
Every team has this invisible tax:
|
|
12
|
+
- Debugging code you didn't write, with zero context
|
|
13
|
+
- Explaining things that are already written *somewhere*
|
|
14
|
+
- Digging through 10 files to understand one API
|
|
15
|
+
- Answering the same question for the third time this week
|
|
16
|
+
|
|
17
|
+
The root cause isn't bad code. It's a **context problem** — knowledge scattered across services, logs, configs, and people's heads.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What It Does
|
|
22
|
+
|
|
23
|
+
Before answering any question, the AI looks up your **actual system**. It knows:
|
|
24
|
+
|
|
25
|
+
- Your data models and DTOs
|
|
26
|
+
- Your naming conventions and code patterns
|
|
27
|
+
- Your most common bugs and how you fixed them
|
|
28
|
+
- Your API behaviour — including weird edge cases
|
|
29
|
+
- How your modules connect to each other
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## The 3 Core Queries
|
|
34
|
+
|
|
35
|
+
| Query | Endpoint | Example |
|
|
36
|
+
| ------------------------------------ | ------------------------ | ---------------------------------------------------- |
|
|
37
|
+
| 🐛 **Why is this failing?** | `POST /api/query/debug` | `"Why is ClassCastException thrown in UserService?"` |
|
|
38
|
+
| 🔍 **Where is this used?** | `POST /api/query/usage` | `"Where is getUserById called?"` |
|
|
39
|
+
| 💥 **If I change this, what breaks?** | `POST /api/query/impact` | `"If I change the User model, what breaks?"` |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Option A — via npx (no install required)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# In your project root (where your .env lives):
|
|
49
|
+
npx dev-mcp-server ingest ./src
|
|
50
|
+
npx dev-mcp-server query "Why is getUserById throwing?"
|
|
51
|
+
npx dev-mcp-server query -i # interactive REPL
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> **Note:** `npx` will look for `.env` in the directory you run the command from,
|
|
55
|
+
> so make sure your credentials are there before running.
|
|
56
|
+
|
|
57
|
+
### Option B — local install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone <repo>
|
|
61
|
+
cd dev-mcp-server
|
|
62
|
+
npm install
|
|
63
|
+
cp .env.example .env
|
|
64
|
+
# Edit .env — choose your LLM provider and add credentials
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Ingest your codebase
|
|
69
|
+
node cli.js ingest ./src
|
|
70
|
+
|
|
71
|
+
# Ask questions
|
|
72
|
+
node cli.js query -i # interactive REPL
|
|
73
|
+
node cli.js query "Why is getUserById failing?"
|
|
74
|
+
node cli.js debug "ClassCastException" --stack "at UserService:45"
|
|
75
|
+
node cli.js stats
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Option C — REST API server
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm start
|
|
82
|
+
# Runs at http://localhost:3000
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## LLM Providers
|
|
88
|
+
|
|
89
|
+
The server supports three backends. Switch between them with a single environment variable — no code changes needed.
|
|
90
|
+
|
|
91
|
+
### Anthropic (default)
|
|
92
|
+
|
|
93
|
+
```env
|
|
94
|
+
LLM_PROVIDER=anthropic
|
|
95
|
+
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
|
96
|
+
LLM_MODEL=claude-opus-4-5 # optional, this is the default
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Ollama (local / self-hosted)
|
|
100
|
+
|
|
101
|
+
Run any model locally — no API key needed.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Install Ollama: https://ollama.com
|
|
105
|
+
ollama pull llama3 # or mistral, codellama, phi3, etc.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```env
|
|
109
|
+
LLM_PROVIDER=ollama
|
|
110
|
+
OLLAMA_BASE_URL=http://localhost:11434 # optional, this is the default
|
|
111
|
+
LLM_MODEL=llama3 # optional, this is the default
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Azure OpenAI
|
|
115
|
+
|
|
116
|
+
```env
|
|
117
|
+
LLM_PROVIDER=azure
|
|
118
|
+
AZURE_OPENAI_ENDPOINT=https://<your-resource>.openai.azure.com
|
|
119
|
+
AZURE_OPENAI_API_KEY=your-azure-key-here
|
|
120
|
+
AZURE_OPENAI_DEPLOYMENT=gpt-4o # your deployment name in Azure AI Studio
|
|
121
|
+
AZURE_OPENAI_API_VERSION=2024-05-01-preview # optional, has a sensible default
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> The deployment name is also used as `LLM_MODEL`. If you want to override the model
|
|
125
|
+
> label independently, set `LLM_MODEL` explicitly.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Ingest & Ignore Rules
|
|
130
|
+
|
|
131
|
+
### Default ignore list
|
|
132
|
+
|
|
133
|
+
The following patterns are always excluded, regardless of any other configuration:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
**/node_modules/** **/.git/** **/dist/**
|
|
137
|
+
**/build/** **/coverage/** **/*.min.js
|
|
138
|
+
**/package-lock.json **/yarn.lock
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### .gitignore integration
|
|
142
|
+
|
|
143
|
+
By default the server reads the `.gitignore` in the directory being ingested and adds those patterns on top of the baseline. This means anything your team already ignores in git is also ignored during ingestion — no duplicate config.
|
|
144
|
+
|
|
145
|
+
```env
|
|
146
|
+
# Disable .gitignore integration (enabled by default):
|
|
147
|
+
INGEST_USE_GITIGNORE=false
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Extra ignore patterns
|
|
151
|
+
|
|
152
|
+
Add any additional glob patterns via a comma-separated env var:
|
|
153
|
+
|
|
154
|
+
```env
|
|
155
|
+
INGEST_EXTRA_IGNORE=**/fixtures/**,**/__snapshots__/**,**/test-data/**
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
All three sources (baseline + `.gitignore` + `INGEST_EXTRA_IGNORE`) are merged and deduplicated before each directory ingest. The log output tells you exactly what was applied:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
Ignore sources: baseline, .gitignore (12 patterns), INGEST_EXTRA_IGNORE (2 patterns)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Configuration Reference
|
|
167
|
+
|
|
168
|
+
Copy `.env.example` to `.env` and fill in the relevant section for your chosen provider.
|
|
169
|
+
|
|
170
|
+
| Variable | Default | Description |
|
|
171
|
+
| -------------------------- | ------------------------ | ----------------------------------------------- |
|
|
172
|
+
| `LLM_PROVIDER` | `anthropic` | LLM backend: `anthropic` \| `ollama` \| `azure` |
|
|
173
|
+
| `LLM_MODEL` | *(per provider)* | Model or deployment name override |
|
|
174
|
+
| `ANTHROPIC_API_KEY` | — | Required when `LLM_PROVIDER=anthropic` |
|
|
175
|
+
| `OLLAMA_BASE_URL` | `http://localhost:11434` | Ollama server URL |
|
|
176
|
+
| `AZURE_OPENAI_ENDPOINT` | — | Required when `LLM_PROVIDER=azure` |
|
|
177
|
+
| `AZURE_OPENAI_API_KEY` | — | Required when `LLM_PROVIDER=azure` |
|
|
178
|
+
| `AZURE_OPENAI_DEPLOYMENT` | — | Required when `LLM_PROVIDER=azure` |
|
|
179
|
+
| `AZURE_OPENAI_API_VERSION` | `2024-05-01-preview` | Azure API version |
|
|
180
|
+
| `INGEST_USE_GITIGNORE` | `true` | Read `.gitignore` during ingest |
|
|
181
|
+
| `INGEST_EXTRA_IGNORE` | — | Comma-separated extra glob patterns to ignore |
|
|
182
|
+
| `PORT` | `3000` | HTTP server port |
|
|
183
|
+
| `LOG_LEVEL` | `info` | `error` \| `warn` \| `info` \| `debug` |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## API Reference
|
|
188
|
+
|
|
189
|
+
### Ingest
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# Ingest a file
|
|
193
|
+
curl -X POST http://localhost:3000/api/ingest/file \
|
|
194
|
+
-H "Content-Type: application/json" \
|
|
195
|
+
-d '{"filePath": "./src/services/UserService.js"}'
|
|
196
|
+
|
|
197
|
+
# Ingest a directory
|
|
198
|
+
curl -X POST http://localhost:3000/api/ingest/directory \
|
|
199
|
+
-H "Content-Type: application/json" \
|
|
200
|
+
-d '{"dirPath": "./src"}'
|
|
201
|
+
|
|
202
|
+
# Ingest raw text (paste an error log, bug description, etc.)
|
|
203
|
+
curl -X POST http://localhost:3000/api/ingest/raw \
|
|
204
|
+
-H "Content-Type: application/json" \
|
|
205
|
+
-d '{
|
|
206
|
+
"content": "ClassCastException at UserService line 45: Mongoose doc passed to UserDTO. Fix: call .toObject() first.",
|
|
207
|
+
"kind": "log",
|
|
208
|
+
"label": "production-bug-2024-03-15"
|
|
209
|
+
}'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Query
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# General question — auto-detects debug / usage / impact mode
|
|
216
|
+
curl -X POST http://localhost:3000/api/query \
|
|
217
|
+
-H "Content-Type: application/json" \
|
|
218
|
+
-d '{"question": "Why does getUserById sometimes throw ClassCastException?"}'
|
|
219
|
+
|
|
220
|
+
# Force debug mode
|
|
221
|
+
curl -X POST http://localhost:3000/api/query/debug \
|
|
222
|
+
-H "Content-Type: application/json" \
|
|
223
|
+
-d '{"error": "ClassCastException", "stackTrace": "at UserService.getUserById:45"}'
|
|
224
|
+
|
|
225
|
+
# Usage search
|
|
226
|
+
curl -X POST http://localhost:3000/api/query/usage \
|
|
227
|
+
-H "Content-Type: application/json" \
|
|
228
|
+
-d '{"symbol": "getUserById"}'
|
|
229
|
+
|
|
230
|
+
# Impact analysis
|
|
231
|
+
curl -X POST http://localhost:3000/api/query/impact \
|
|
232
|
+
-H "Content-Type: application/json" \
|
|
233
|
+
-d '{"target": "UserDTO", "changeDescription": "add a new required field"}'
|
|
234
|
+
|
|
235
|
+
# Streaming (Server-Sent Events)
|
|
236
|
+
curl -X POST http://localhost:3000/api/query/stream \
|
|
237
|
+
-H "Content-Type: application/json" \
|
|
238
|
+
-d '{"question": "How does user status update work end to end?"}'
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Knowledge Base
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
curl http://localhost:3000/api/knowledge/stats
|
|
245
|
+
curl "http://localhost:3000/api/knowledge/search?q=ClassCastException&topK=5"
|
|
246
|
+
curl http://localhost:3000/api/knowledge/files
|
|
247
|
+
curl -X POST http://localhost:3000/api/knowledge/rebuild
|
|
248
|
+
curl -X DELETE http://localhost:3000/api/ingest/clear
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Supported File Types
|
|
254
|
+
|
|
255
|
+
| Category | Extensions |
|
|
256
|
+
| -------- | ------------------------------------------------------------------------------------------------ |
|
|
257
|
+
| Code | `.js` `.ts` `.jsx` `.tsx` `.mjs` `.cjs` `.py` `.java` `.go` `.rb` `.php` `.cs` `.cpp` `.c` `.rs` |
|
|
258
|
+
| Config | `.json` `.yaml` `.yml` `.env` `.toml` `.xml` |
|
|
259
|
+
| Docs | `.md` `.txt` |
|
|
260
|
+
| Logs | `.log` |
|
|
261
|
+
| Schema | `.sql` `.graphql` `.gql` |
|
|
262
|
+
| Scripts | `.sh` `.bash` |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## What to Ingest
|
|
267
|
+
|
|
268
|
+
The key insight: **ingest real stuff, not clean summaries**.
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
node cli.js ingest ./src # actual source code
|
|
272
|
+
node cli.js ingest ./logs # real error logs — the ugly ones
|
|
273
|
+
node cli.js ingest ./config # environment configs and schemas
|
|
274
|
+
node cli.js ingest ./docs # ADRs, runbooks, onboarding notes
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Paste knowledge directly in the interactive REPL:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
❯ node cli.js query -i
|
|
281
|
+
❯ We fixed a bug last week where the Mongoose document wasn't being converted
|
|
282
|
+
to a plain object before passing to UserDTO. Always call .toObject() first.
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
> *"Docs lie. Or rather, docs go stale. Code doesn't."*
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Architecture
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
293
|
+
│ Dev MCP Server │
|
|
294
|
+
│ │
|
|
295
|
+
│ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │
|
|
296
|
+
│ │ Ingester │───▶│ Store │──▶│ Indexer │ │
|
|
297
|
+
│ │ │ │ (JSON) │ │ (TF-IDF Search) │ │
|
|
298
|
+
│ └──────────┘ └──────────┘ └────────────────────────┘ │
|
|
299
|
+
│ │ │ │
|
|
300
|
+
│ ▼ ▼ │
|
|
301
|
+
│ ┌──────────┐ ┌───────────────────────────┐ │
|
|
302
|
+
│ │ CLI │ │ Query Engine │ │
|
|
303
|
+
│ │ (REPL) │ │ Retrieval + LLM Client │ │
|
|
304
|
+
│ └──────────┘ └───────────────────────────┘ │
|
|
305
|
+
│ │ │
|
|
306
|
+
│ ┌┴──────────────┐│
|
|
307
|
+
│ │ Anthropic / ││
|
|
308
|
+
│ │ Ollama / ││
|
|
309
|
+
│ │ Azure OpenAI ││
|
|
310
|
+
│ └───────────────┘│
|
|
311
|
+
│ │
|
|
312
|
+
│ ┌────────────────────────────────────────────────────────┐ │
|
|
313
|
+
│ │ Express REST API │ │
|
|
314
|
+
│ └────────────────────────────────────────────────────────┘ │
|
|
315
|
+
└─────────────────────────────────────────────────────────────┘
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**How it works:**
|
|
319
|
+
1. **Ingest** — Feed your codebase in (files, directories, raw text)
|
|
320
|
+
2. **Index** — TF-IDF search index built over all chunks
|
|
321
|
+
3. **Query** — Question arrives → relevant context retrieved → LLM answers based on *your* code
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Key Design Decisions
|
|
326
|
+
|
|
327
|
+
**Data quality beats model quality.** The retrieval step (TF-IDF over your actual files) matters more than which AI model you use. A focused, well-curated knowledge base with a smaller model will outperform a bloated one with GPT-4.
|
|
328
|
+
|
|
329
|
+
**No embeddings, no vector DB.** TF-IDF is deterministic, fast, and requires zero infrastructure. For most codebases (< 50k files) it's entirely sufficient.
|
|
330
|
+
|
|
331
|
+
**Provider-agnostic by design.** The `llmClient` abstraction means you can switch from Anthropic to a local Ollama model to Azure OpenAI by changing one line in `.env` — useful for cost control, data residency requirements, or offline usage.
|
|
332
|
+
|
|
333
|
+
**Ingest real artefacts.** Error logs, not summaries of error logs. Actual API responses, not docs about API responses. The messier the better — the system is built to handle it.
|
package/cli.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
|
|
5
|
+
|
|
6
|
+
const { Command } = require('commander');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
|
|
11
|
+
const ingester = require('./src/core/ingester');
|
|
12
|
+
const indexer = require('./src/core/indexer');
|
|
13
|
+
const { QueryEngine, detectMode, QUERY_MODES } = require('./src/core/queryEngine');
|
|
14
|
+
const store = require('./src/storage/store');
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
const banner = chalk.cyan(`
|
|
19
|
+
╔══════════════════════════════════════════════════════╗
|
|
20
|
+
║ Dev MCP Server — Model Context Platform ║
|
|
21
|
+
║ AI that understands YOUR codebase ║
|
|
22
|
+
╚══════════════════════════════════════════════════════╝
|
|
23
|
+
`);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('ingest <path>')
|
|
27
|
+
.description('Ingest a file or directory into the knowledge base')
|
|
28
|
+
.option('-t, --type <type>', 'Force type: code | config | documentation | log | schema')
|
|
29
|
+
.action(async (inputPath, opts) => {
|
|
30
|
+
console.log(banner);
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const stat = fs.statSync(inputPath);
|
|
33
|
+
const spinner = ora();
|
|
34
|
+
|
|
35
|
+
if (stat.isDirectory()) {
|
|
36
|
+
spinner.start(chalk.blue(`Scanning directory: ${inputPath}`));
|
|
37
|
+
try {
|
|
38
|
+
const result = await ingester.ingestDirectory(inputPath);
|
|
39
|
+
spinner.succeed(chalk.green('Ingestion complete'));
|
|
40
|
+
console.log('\n' + chalk.bold('Results:'));
|
|
41
|
+
console.log(` ${chalk.green('✓')} Ingested: ${result.ingested} files`);
|
|
42
|
+
console.log(` ${chalk.yellow('⚠')} Skipped: ${result.skipped} files`);
|
|
43
|
+
console.log(` ${chalk.red('✗')} Failed: ${result.failed} files`);
|
|
44
|
+
console.log(` ${chalk.cyan('◈')} Chunks: ${result.totalChunks} total`);
|
|
45
|
+
if (result.errors.length > 0) {
|
|
46
|
+
console.log('\n' + chalk.red('Errors:'));
|
|
47
|
+
result.errors.slice(0, 5).forEach(e =>
|
|
48
|
+
console.log(` ${e.file}: ${e.error}`)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
spinner.start(chalk.blue(`Ingesting file: ${inputPath}`));
|
|
57
|
+
try {
|
|
58
|
+
const result = await ingester.ingestFile(inputPath);
|
|
59
|
+
indexer.build();
|
|
60
|
+
spinner.succeed(chalk.green(`Ingested: ${result.chunks} chunks`));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command('query [question]')
|
|
70
|
+
.description('Ask a question about your codebase')
|
|
71
|
+
.option('-m, --mode <mode>', 'Force mode: debug | usage | impact | general')
|
|
72
|
+
.option('-k, --top-k <n>', 'Number of context chunks', '8')
|
|
73
|
+
.option('-i, --interactive', 'Start interactive REPL session')
|
|
74
|
+
.action(async (question, opts) => {
|
|
75
|
+
console.log(banner);
|
|
76
|
+
|
|
77
|
+
const stats = store.getStats();
|
|
78
|
+
if (stats.totalDocs === 0) {
|
|
79
|
+
console.log(chalk.yellow('⚠ Knowledge base is empty!'));
|
|
80
|
+
console.log(chalk.gray(' Run: node cli.js ingest <path>'));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(chalk.gray(`📚 Knowledge base: ${stats.totalDocs} docs from ${stats.totalFiles} files\n`));
|
|
85
|
+
|
|
86
|
+
if (opts.interactive || !question) {
|
|
87
|
+
await startRepl();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await askQuestion(question, opts);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
async function askQuestion(question, opts = {}) {
|
|
95
|
+
const mode = opts.mode || detectMode(question);
|
|
96
|
+
const topK = parseInt(opts.topK || 8);
|
|
97
|
+
|
|
98
|
+
const modeColors = {
|
|
99
|
+
[QUERY_MODES.DEBUG]: chalk.red,
|
|
100
|
+
[QUERY_MODES.USAGE]: chalk.blue,
|
|
101
|
+
[QUERY_MODES.IMPACT]: chalk.yellow,
|
|
102
|
+
[QUERY_MODES.GENERAL]: chalk.cyan,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const modeEmoji = {
|
|
106
|
+
[QUERY_MODES.DEBUG]: '🐛',
|
|
107
|
+
[QUERY_MODES.USAGE]: '🔍',
|
|
108
|
+
[QUERY_MODES.IMPACT]: '💥',
|
|
109
|
+
[QUERY_MODES.GENERAL]: '💬',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const colorFn = modeColors[mode] || chalk.cyan;
|
|
113
|
+
console.log(colorFn(`${modeEmoji[mode]} Mode: ${mode.toUpperCase()}`));
|
|
114
|
+
console.log(chalk.bold(`\nQ: ${question}\n`));
|
|
115
|
+
|
|
116
|
+
const spinner = ora('Retrieving context and thinking...').start();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const result = await QueryEngine.query(question, { mode, topK });
|
|
120
|
+
spinner.stop();
|
|
121
|
+
|
|
122
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
123
|
+
console.log(chalk.gray('Sources used:'));
|
|
124
|
+
result.sources.forEach((s, i) => {
|
|
125
|
+
console.log(chalk.gray(` [${i + 1}] ${s.file} (${s.kind}) — score: ${s.relevanceScore}`));
|
|
126
|
+
});
|
|
127
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
128
|
+
|
|
129
|
+
console.log('\n' + chalk.bold('Answer:\n'));
|
|
130
|
+
console.log(result.answer);
|
|
131
|
+
console.log(chalk.gray(`\n[Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out]`));
|
|
132
|
+
|
|
133
|
+
} catch (err) {
|
|
134
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function startRepl() {
|
|
139
|
+
console.log(chalk.cyan('Starting interactive session. Type "exit" to quit, "help" for tips.\n'));
|
|
140
|
+
|
|
141
|
+
const rl = readline.createInterface({
|
|
142
|
+
input: process.stdin,
|
|
143
|
+
output: process.stdout,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const prompt = () => {
|
|
147
|
+
rl.question(chalk.bold.cyan('\n❯ '), async (input) => {
|
|
148
|
+
const trimmed = input.trim();
|
|
149
|
+
|
|
150
|
+
if (!trimmed) {
|
|
151
|
+
prompt();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
|
|
156
|
+
console.log(chalk.cyan('\nGoodbye!\n'));
|
|
157
|
+
rl.close();
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (trimmed.toLowerCase() === 'help') {
|
|
162
|
+
console.log(chalk.cyan(`
|
|
163
|
+
Tips:
|
|
164
|
+
🐛 Debug: "Why is ClassCastException happening in UserService?"
|
|
165
|
+
🔍 Usage: "Where is getUserById used?"
|
|
166
|
+
💥 Impact: "If I change the User model, what breaks?"
|
|
167
|
+
💬 General: Any question about your codebase
|
|
168
|
+
`));
|
|
169
|
+
prompt();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (trimmed.toLowerCase() === 'stats') {
|
|
174
|
+
const stats = store.getStats();
|
|
175
|
+
console.log(chalk.cyan(JSON.stringify(stats, null, 2)));
|
|
176
|
+
prompt();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await askQuestion(trimmed);
|
|
181
|
+
prompt();
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
prompt();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
program
|
|
189
|
+
.command('stats')
|
|
190
|
+
.description('Show knowledge base statistics')
|
|
191
|
+
.action(() => {
|
|
192
|
+
const stats = store.getStats();
|
|
193
|
+
const files = store.getIngestedFiles();
|
|
194
|
+
|
|
195
|
+
console.log(banner);
|
|
196
|
+
console.log(chalk.bold('Knowledge Base Stats:'));
|
|
197
|
+
console.log(` Total documents: ${chalk.green(stats.totalDocs)}`);
|
|
198
|
+
console.log(` Total files: ${chalk.green(stats.totalFiles)}`);
|
|
199
|
+
console.log(` Last ingested: ${chalk.gray(stats.lastIngested || 'Never')}`);
|
|
200
|
+
console.log('\n' + chalk.bold('By type:'));
|
|
201
|
+
Object.entries(stats.fileTypes || {}).forEach(([type, count]) => {
|
|
202
|
+
console.log(` ${type.padEnd(15)} ${chalk.cyan(count)} docs`);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (files.length > 0) {
|
|
206
|
+
console.log('\n' + chalk.bold(`Ingested files (${files.length}):`));
|
|
207
|
+
files.slice(0, 20).forEach(f => console.log(` ${chalk.gray(f)}`));
|
|
208
|
+
if (files.length > 20) {
|
|
209
|
+
console.log(chalk.gray(` ... and ${files.length - 20} more`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
program
|
|
215
|
+
.command('clear')
|
|
216
|
+
.description('Clear the entire knowledge base')
|
|
217
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
218
|
+
.action(async (opts) => {
|
|
219
|
+
if (!opts.yes) {
|
|
220
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
221
|
+
rl.question(chalk.red('⚠ This will delete all indexed data. Continue? (y/N) '), (answer) => {
|
|
222
|
+
rl.close();
|
|
223
|
+
if (answer.toLowerCase() === 'y') {
|
|
224
|
+
store.clear();
|
|
225
|
+
indexer.invalidate();
|
|
226
|
+
console.log(chalk.green('✓ Knowledge base cleared'));
|
|
227
|
+
} else {
|
|
228
|
+
console.log('Cancelled.');
|
|
229
|
+
}
|
|
230
|
+
process.exit(0);
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
store.clear();
|
|
234
|
+
indexer.invalidate();
|
|
235
|
+
console.log(chalk.green('✓ Knowledge base cleared'));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
program
|
|
240
|
+
.command('debug <error>')
|
|
241
|
+
.description('Quick debug: explain an error in context of your codebase')
|
|
242
|
+
.option('-s, --stack <trace>', 'Stack trace')
|
|
243
|
+
.action(async (error, opts) => {
|
|
244
|
+
console.log(banner);
|
|
245
|
+
await askQuestion(`Why is this error happening and how do I fix it?\nError: ${error}${opts.stack ? '\nStack:\n' + opts.stack : ''}`, { mode: QUERY_MODES.DEBUG });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
program.parse(process.argv);
|