dingdawg-code-review 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/README.md +95 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +679 -0
- package/package.json +44 -0
- package/src/index.ts +762 -0
- package/tsconfig.json +14 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* dingdawg-code-review — AI Code Review Agent MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Governed. Receipted. Production-ready.
|
|
7
|
+
*
|
|
8
|
+
* Install: npx dingdawg-code-review
|
|
9
|
+
* Claude Code: claude mcp add dingdawg-code-review npx dingdawg-code-review
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
46
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
47
|
+
const zod_1 = require("zod");
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const crypto = __importStar(require("crypto"));
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Persistent rate limiting — survives process restart via filesystem
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
const RATE_FILE = path.join(os.homedir(), ".dingdawg", "code-review-usage.json");
|
|
56
|
+
const MACHINE_ID = crypto.createHash("sha256")
|
|
57
|
+
.update(`${os.hostname()}-${os.userInfo().username}-${os.platform()}-${os.arch()}`)
|
|
58
|
+
.digest("hex").slice(0, 16);
|
|
59
|
+
const FREE_LIMITS = {
|
|
60
|
+
review_code: 10,
|
|
61
|
+
review_pr: 5,
|
|
62
|
+
suggest_fix: 20,
|
|
63
|
+
};
|
|
64
|
+
function checkFreeRateLimit(tool) {
|
|
65
|
+
const limit = FREE_LIMITS[tool] ?? 10;
|
|
66
|
+
const key = `${MACHINE_ID}_${tool}`;
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
69
|
+
let store = {};
|
|
70
|
+
try {
|
|
71
|
+
const dir = path.dirname(RATE_FILE);
|
|
72
|
+
if (!fs.existsSync(dir))
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
if (fs.existsSync(RATE_FILE)) {
|
|
75
|
+
store = JSON.parse(fs.readFileSync(RATE_FILE, "utf-8"));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* fresh start */ }
|
|
79
|
+
const entry = store[key];
|
|
80
|
+
if (!entry || now > entry.resetAt) {
|
|
81
|
+
store[key] = { count: 1, resetAt: now + dayMs };
|
|
82
|
+
}
|
|
83
|
+
else if (entry.count >= limit) {
|
|
84
|
+
try {
|
|
85
|
+
fs.writeFileSync(RATE_FILE, JSON.stringify(store));
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
return { allowed: false, remaining: 0 };
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
store[key].count++;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(RATE_FILE, JSON.stringify(store));
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
const current = store[key].count;
|
|
98
|
+
return { allowed: true, remaining: limit - current };
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Receipt & governance helpers
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
function generateReceiptId() {
|
|
104
|
+
return `rcpt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
105
|
+
}
|
|
106
|
+
function governanceEnvelope(tool, findings, extra = {}) {
|
|
107
|
+
const severityCounts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
108
|
+
for (const f of findings) {
|
|
109
|
+
severityCounts[f.severity] = (severityCounts[f.severity] || 0) + 1;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
receipt_id: generateReceiptId(),
|
|
113
|
+
timestamp: new Date().toISOString(),
|
|
114
|
+
tool,
|
|
115
|
+
governance_policy: "dingdawg-code-review/v1.0.0",
|
|
116
|
+
rules_checked: [
|
|
117
|
+
"security-vulnerabilities",
|
|
118
|
+
"code-quality",
|
|
119
|
+
"performance",
|
|
120
|
+
"best-practices",
|
|
121
|
+
],
|
|
122
|
+
findings_count: findings.length,
|
|
123
|
+
findings_by_severity: severityCounts,
|
|
124
|
+
governed: true,
|
|
125
|
+
...extra,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const ANALYSIS_RULES = [
|
|
129
|
+
// --- Security ---
|
|
130
|
+
{
|
|
131
|
+
id: "SEC-001",
|
|
132
|
+
pattern: /eval\s*\(/g,
|
|
133
|
+
severity: "critical",
|
|
134
|
+
category: "security",
|
|
135
|
+
title: "Use of eval()",
|
|
136
|
+
description: "eval() executes arbitrary code and is a major injection vector. Attackers can exploit this to run malicious code.",
|
|
137
|
+
suggestion: "Replace eval() with JSON.parse() for data, or use a safe expression parser. Never pass user input to eval().",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "SEC-002",
|
|
141
|
+
pattern: /exec\s*\(\s*(['"`]|[^)]*\+)/g,
|
|
142
|
+
severity: "critical",
|
|
143
|
+
category: "security",
|
|
144
|
+
title: "Command injection risk",
|
|
145
|
+
description: "String concatenation in exec/spawn calls can allow command injection if user input is included.",
|
|
146
|
+
suggestion: "Use parameterized commands with execFile() or spawn() with an argument array instead of shell strings.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "SEC-003",
|
|
150
|
+
pattern: /innerHTML\s*=\s*[^'"`]/g,
|
|
151
|
+
severity: "high",
|
|
152
|
+
category: "security",
|
|
153
|
+
title: "Potential XSS via innerHTML",
|
|
154
|
+
description: "Setting innerHTML with dynamic content can introduce cross-site scripting vulnerabilities.",
|
|
155
|
+
suggestion: "Use textContent for plain text, or sanitize HTML with DOMPurify before assignment.",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "SEC-004",
|
|
159
|
+
pattern: /(?:password|secret|api_?key|token|private_?key)\s*[:=]\s*['"`][^'"`]{3,}/gi,
|
|
160
|
+
severity: "critical",
|
|
161
|
+
category: "security",
|
|
162
|
+
title: "Hardcoded secret detected",
|
|
163
|
+
description: "Credentials or API keys appear to be hardcoded in source. These will be exposed in version control.",
|
|
164
|
+
suggestion: "Move secrets to environment variables or a secrets manager. Never commit credentials to source code.",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "SEC-005",
|
|
168
|
+
pattern: /\bhttp:\/\/(?!localhost|127\.0\.0\.1)/g,
|
|
169
|
+
severity: "medium",
|
|
170
|
+
category: "security",
|
|
171
|
+
title: "Insecure HTTP URL",
|
|
172
|
+
description: "Non-localhost HTTP URLs transmit data in plaintext, vulnerable to man-in-the-middle attacks.",
|
|
173
|
+
suggestion: "Use HTTPS for all external URLs. Enforce TLS in production.",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "SEC-006",
|
|
177
|
+
pattern: /(?:SELECT|INSERT|UPDATE|DELETE)\s+.*\+\s*(?:req\.|params\.|query\.|body\.|input|user)/gi,
|
|
178
|
+
severity: "critical",
|
|
179
|
+
category: "security",
|
|
180
|
+
title: "SQL injection risk",
|
|
181
|
+
description: "SQL query built with string concatenation using user input. This is a classic SQL injection vector.",
|
|
182
|
+
suggestion: "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "SEC-007",
|
|
186
|
+
pattern: /dangerouslySetInnerHTML/g,
|
|
187
|
+
severity: "high",
|
|
188
|
+
category: "security",
|
|
189
|
+
title: "React dangerouslySetInnerHTML",
|
|
190
|
+
description: "dangerouslySetInnerHTML bypasses React's XSS protections. If the content includes user input, XSS is possible.",
|
|
191
|
+
suggestion: "Sanitize content with DOMPurify before passing to dangerouslySetInnerHTML, or restructure to avoid it.",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "SEC-008",
|
|
195
|
+
pattern: /new\s+Function\s*\(/g,
|
|
196
|
+
severity: "high",
|
|
197
|
+
category: "security",
|
|
198
|
+
title: "Dynamic function constructor",
|
|
199
|
+
description: "new Function() is equivalent to eval() and poses the same code injection risks.",
|
|
200
|
+
suggestion: "Refactor to avoid dynamic code generation. Use lookup tables or strategy patterns instead.",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "SEC-009",
|
|
204
|
+
pattern: /(?:cors|Access-Control-Allow-Origin)\s*[:=]\s*['"`]\*['"`]/gi,
|
|
205
|
+
severity: "medium",
|
|
206
|
+
category: "security",
|
|
207
|
+
title: "Wildcard CORS policy",
|
|
208
|
+
description: "Allowing all origins (\\*) in CORS can expose your API to cross-origin attacks from malicious sites.",
|
|
209
|
+
suggestion: "Restrict CORS to specific trusted origins in production.",
|
|
210
|
+
},
|
|
211
|
+
// --- Code Quality ---
|
|
212
|
+
{
|
|
213
|
+
id: "QUAL-001",
|
|
214
|
+
pattern: /\bcatch\s*\(\s*\w*\s*\)\s*\{\s*\}/g,
|
|
215
|
+
severity: "medium",
|
|
216
|
+
category: "quality",
|
|
217
|
+
title: "Empty catch block",
|
|
218
|
+
description: "Swallowing exceptions silently hides errors and makes debugging extremely difficult.",
|
|
219
|
+
suggestion: "Log the error, rethrow it, or handle it explicitly. At minimum add a comment explaining why it's empty.",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "QUAL-002",
|
|
223
|
+
pattern: /\/\/\s*TODO(?::|\s)/gi,
|
|
224
|
+
severity: "low",
|
|
225
|
+
category: "quality",
|
|
226
|
+
title: "TODO comment found",
|
|
227
|
+
description: "TODO comments indicate unfinished work that may be forgotten.",
|
|
228
|
+
suggestion: "Track TODOs in your issue tracker. Resolve before merging to main.",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "QUAL-003",
|
|
232
|
+
pattern: /console\.(log|warn|error|debug|info)\s*\(/g,
|
|
233
|
+
severity: "low",
|
|
234
|
+
category: "quality",
|
|
235
|
+
title: "Console statement in code",
|
|
236
|
+
description: "Console statements should not be in production code. They leak information and clutter output.",
|
|
237
|
+
suggestion: "Use a structured logging library (winston, pino) or remove debug statements before committing.",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "QUAL-004",
|
|
241
|
+
pattern: /any(?:\s*[;,)\]}]|\s*$)/gm,
|
|
242
|
+
severity: "medium",
|
|
243
|
+
category: "quality",
|
|
244
|
+
title: "TypeScript 'any' type usage",
|
|
245
|
+
description: "Using 'any' disables type checking, defeating the purpose of TypeScript and hiding potential bugs.",
|
|
246
|
+
suggestion: "Replace 'any' with a specific type, 'unknown', or a generic. Use type narrowing for dynamic data.",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "QUAL-005",
|
|
250
|
+
pattern: /(?:^|\n).{200,}/g,
|
|
251
|
+
severity: "low",
|
|
252
|
+
category: "quality",
|
|
253
|
+
title: "Overly long line",
|
|
254
|
+
description: "Lines exceeding 200 characters hurt readability and make code reviews harder.",
|
|
255
|
+
suggestion: "Break long lines into multiple shorter lines. Configure your formatter to enforce a max line length.",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "QUAL-006",
|
|
259
|
+
pattern: /\bvar\s+/g,
|
|
260
|
+
severity: "medium",
|
|
261
|
+
category: "quality",
|
|
262
|
+
title: "Use of 'var' keyword",
|
|
263
|
+
description: "'var' has function scope and hoisting behavior that leads to subtle bugs.",
|
|
264
|
+
suggestion: "Use 'const' for values that don't change, 'let' for values that do. Never use 'var'.",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: "QUAL-007",
|
|
268
|
+
pattern: /==(?!=)/g,
|
|
269
|
+
severity: "low",
|
|
270
|
+
category: "quality",
|
|
271
|
+
title: "Loose equality operator",
|
|
272
|
+
description: "== performs type coercion which can produce unexpected results (e.g., '' == 0 is true).",
|
|
273
|
+
suggestion: "Use === (strict equality) to avoid type coercion surprises.",
|
|
274
|
+
},
|
|
275
|
+
// --- Performance ---
|
|
276
|
+
{
|
|
277
|
+
id: "PERF-001",
|
|
278
|
+
pattern: /new\s+RegExp\s*\(/g,
|
|
279
|
+
severity: "low",
|
|
280
|
+
category: "performance",
|
|
281
|
+
title: "Regex compiled inside function/loop",
|
|
282
|
+
description: "Creating RegExp objects inside frequently-called code recompiles the regex on every call.",
|
|
283
|
+
suggestion: "Move the RegExp to a module-level constant so it's compiled once.",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "PERF-002",
|
|
287
|
+
pattern: /JSON\.parse\s*\(\s*JSON\.stringify\s*\(/g,
|
|
288
|
+
severity: "medium",
|
|
289
|
+
category: "performance",
|
|
290
|
+
title: "Deep clone via JSON round-trip",
|
|
291
|
+
description: "JSON.parse(JSON.stringify(obj)) is slow for large objects and drops functions, Dates, undefined, etc.",
|
|
292
|
+
suggestion: "Use structuredClone() (Node 17+/modern browsers) or a dedicated deep-clone library.",
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: "PERF-003",
|
|
296
|
+
pattern: /\.forEach\s*\(\s*async/g,
|
|
297
|
+
severity: "high",
|
|
298
|
+
category: "performance",
|
|
299
|
+
title: "Async callback in forEach",
|
|
300
|
+
description: "Array.forEach does not await async callbacks. All iterations fire simultaneously with no error handling.",
|
|
301
|
+
suggestion: "Use a for...of loop with await, or Promise.all(arr.map(async ...)) for controlled concurrency.",
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: "PERF-004",
|
|
305
|
+
pattern: /await\s+.*\bfor\s*\(/g,
|
|
306
|
+
severity: "medium",
|
|
307
|
+
category: "performance",
|
|
308
|
+
title: "Await inside loop",
|
|
309
|
+
description: "Awaiting inside a loop runs operations sequentially when they could run in parallel.",
|
|
310
|
+
suggestion: "Collect promises and use Promise.all() for parallel execution, or use for await...of for async iterables.",
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: "PERF-005",
|
|
314
|
+
pattern: /document\.querySelector(?:All)?\s*\([^)]+\)/g,
|
|
315
|
+
severity: "low",
|
|
316
|
+
category: "performance",
|
|
317
|
+
title: "DOM query in potential hot path",
|
|
318
|
+
description: "Repeated DOM queries are expensive. If this runs in a loop or event handler, cache the result.",
|
|
319
|
+
suggestion: "Cache the DOM reference in a variable outside the loop/handler.",
|
|
320
|
+
},
|
|
321
|
+
// --- Best Practices ---
|
|
322
|
+
{
|
|
323
|
+
id: "BP-001",
|
|
324
|
+
pattern: /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\([^)]{100,}\)/g,
|
|
325
|
+
severity: "medium",
|
|
326
|
+
category: "best-practice",
|
|
327
|
+
title: "Function with too many parameters",
|
|
328
|
+
description: "Functions with many parameters are hard to call correctly and maintain.",
|
|
329
|
+
suggestion: "Use an options/config object parameter instead. Destructure for clarity.",
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "BP-002",
|
|
333
|
+
pattern: /catch\s*\(\s*\w+\s*\)\s*\{[^}]*throw\s+\w+\s*;?\s*\}/g,
|
|
334
|
+
severity: "low",
|
|
335
|
+
category: "best-practice",
|
|
336
|
+
title: "Catch and rethrow without modification",
|
|
337
|
+
description: "Catching an error only to rethrow it adds noise without value.",
|
|
338
|
+
suggestion: "Remove the try/catch if you're not modifying the error, or wrap it with additional context.",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: "BP-003",
|
|
342
|
+
pattern: /(?:^|\n)\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)/g,
|
|
343
|
+
severity: "low",
|
|
344
|
+
category: "best-practice",
|
|
345
|
+
title: "Excessive consecutive comments",
|
|
346
|
+
description: "Large blocks of comments may indicate commented-out code or over-documentation.",
|
|
347
|
+
suggestion: "Remove commented-out code. Use clear naming and small functions instead of excessive comments.",
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: "BP-004",
|
|
351
|
+
pattern: /(?:process\.exit|os\._exit|sys\.exit)\s*\(\s*[^)]*\)/g,
|
|
352
|
+
severity: "medium",
|
|
353
|
+
category: "best-practice",
|
|
354
|
+
title: "Hard process exit",
|
|
355
|
+
description: "Calling process.exit() skips cleanup, open handles, and graceful shutdown procedures.",
|
|
356
|
+
suggestion: "Throw an error or return an error code to let the application shut down gracefully.",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: "BP-005",
|
|
360
|
+
pattern: /(?:\.then\s*\([^)]*\)\s*){3,}/g,
|
|
361
|
+
severity: "medium",
|
|
362
|
+
category: "best-practice",
|
|
363
|
+
title: "Deeply chained promises",
|
|
364
|
+
description: "Long .then() chains are hard to read and debug compared to async/await.",
|
|
365
|
+
suggestion: "Refactor to async/await for better readability and error handling.",
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: "BP-006",
|
|
369
|
+
pattern: /(?:^|\n)(?:.*\n){300,}/g,
|
|
370
|
+
severity: "medium",
|
|
371
|
+
category: "best-practice",
|
|
372
|
+
title: "File exceeds 300 lines",
|
|
373
|
+
description: "Large files are harder to navigate, test, and maintain.",
|
|
374
|
+
suggestion: "Consider splitting into smaller, focused modules with clear responsibilities.",
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
function analyzeCode(code, language) {
|
|
378
|
+
const findings = [];
|
|
379
|
+
const lines = code.split("\n");
|
|
380
|
+
for (const rule of ANALYSIS_RULES) {
|
|
381
|
+
// Skip DOM-specific rules for non-frontend code
|
|
382
|
+
if (rule.id === "PERF-005" && language && !["javascript", "typescript", "jsx", "tsx"].includes(language)) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
// Reset regex state
|
|
386
|
+
rule.pattern.lastIndex = 0;
|
|
387
|
+
let match;
|
|
388
|
+
const matchedLines = [];
|
|
389
|
+
while ((match = rule.pattern.exec(code)) !== null) {
|
|
390
|
+
// Find the line number
|
|
391
|
+
const beforeMatch = code.slice(0, match.index);
|
|
392
|
+
const lineNum = beforeMatch.split("\n").length;
|
|
393
|
+
matchedLines.push(`line ${lineNum}`);
|
|
394
|
+
// Prevent infinite loops on zero-width matches
|
|
395
|
+
if (match[0].length === 0) {
|
|
396
|
+
rule.pattern.lastIndex++;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (matchedLines.length > 0) {
|
|
400
|
+
findings.push({
|
|
401
|
+
id: rule.id,
|
|
402
|
+
severity: rule.severity,
|
|
403
|
+
category: rule.category,
|
|
404
|
+
title: rule.title,
|
|
405
|
+
description: rule.description,
|
|
406
|
+
line_hint: matchedLines.slice(0, 5).join(", ") + (matchedLines.length > 5 ? ` (+${matchedLines.length - 5} more)` : ""),
|
|
407
|
+
suggestion: rule.suggestion,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Sort findings: critical first, then high, medium, low, info
|
|
412
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
413
|
+
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
414
|
+
return findings;
|
|
415
|
+
}
|
|
416
|
+
function analyzePRDiff(diff, title) {
|
|
417
|
+
const findings = [];
|
|
418
|
+
const testCoverageGaps = [];
|
|
419
|
+
// Extract only added lines for analysis (lines starting with +, not ++)
|
|
420
|
+
const addedLines = diff.split("\n")
|
|
421
|
+
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
|
422
|
+
.map((line) => line.slice(1))
|
|
423
|
+
.join("\n");
|
|
424
|
+
// Run the standard code analysis on added lines
|
|
425
|
+
const codeFindings = analyzeCode(addedLines);
|
|
426
|
+
findings.push(...codeFindings);
|
|
427
|
+
// Check for breaking change patterns in the diff
|
|
428
|
+
let breakingRisk = "none";
|
|
429
|
+
const breakingPatterns = [
|
|
430
|
+
{ pattern: /^-\s*export\s+/gm, risk: "high", desc: "Exported symbol removed" },
|
|
431
|
+
{ pattern: /^-\s*(?:public|protected)\s+/gm, risk: "medium", desc: "Public/protected API removed" },
|
|
432
|
+
{ pattern: /^-.*(?:interface|type)\s+\w+/gm, risk: "medium", desc: "Type definition removed" },
|
|
433
|
+
{ pattern: /^-\s*(?:app|router)\.\s*(?:get|post|put|delete|patch)\s*\(/gm, risk: "high", desc: "API endpoint removed" },
|
|
434
|
+
{ pattern: /(?:DROP\s+TABLE|ALTER\s+TABLE.*DROP\s+COLUMN)/gi, risk: "high", desc: "Database schema destructive change" },
|
|
435
|
+
];
|
|
436
|
+
const riskOrder = { none: 0, low: 1, medium: 2, high: 3 };
|
|
437
|
+
for (const bp of breakingPatterns) {
|
|
438
|
+
bp.pattern.lastIndex = 0;
|
|
439
|
+
if (bp.pattern.test(diff)) {
|
|
440
|
+
if (riskOrder[bp.risk] > riskOrder[breakingRisk]) {
|
|
441
|
+
breakingRisk = bp.risk;
|
|
442
|
+
}
|
|
443
|
+
findings.push({
|
|
444
|
+
id: `BREAK-${findings.length + 1}`,
|
|
445
|
+
severity: bp.risk === "high" ? "high" : "medium",
|
|
446
|
+
category: "quality",
|
|
447
|
+
title: `Breaking change: ${bp.desc}`,
|
|
448
|
+
description: `This diff appears to ${bp.desc.toLowerCase()}. This could break downstream consumers.`,
|
|
449
|
+
suggestion: "Ensure backward compatibility or document the breaking change in release notes.",
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Test coverage gap detection
|
|
454
|
+
const addedFiles = diff.match(/^\+\+\+\s+b\/(.+)/gm)?.map((l) => l.replace(/^\+\+\+\s+b\//, "")) ?? [];
|
|
455
|
+
const hasTestFiles = addedFiles.some((f) => f.includes("test") || f.includes("spec") || f.includes("__tests__"));
|
|
456
|
+
const hasSourceChanges = addedFiles.some((f) => !f.includes("test") && !f.includes("spec") && !f.includes("__tests__") &&
|
|
457
|
+
(f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".py") || f.endsWith(".go") || f.endsWith(".rs")));
|
|
458
|
+
if (hasSourceChanges && !hasTestFiles) {
|
|
459
|
+
testCoverageGaps.push("Source files modified but no test files included in this PR.");
|
|
460
|
+
}
|
|
461
|
+
// Check for new functions without corresponding tests
|
|
462
|
+
const newFunctions = addedLines.match(/(?:function|const|let)\s+(\w+)\s*(?:=\s*(?:async\s*)?\(|\()/g);
|
|
463
|
+
if (newFunctions && newFunctions.length > 3 && !hasTestFiles) {
|
|
464
|
+
testCoverageGaps.push(`${newFunctions.length} new functions added without test coverage.`);
|
|
465
|
+
}
|
|
466
|
+
// Check for new API endpoints without tests
|
|
467
|
+
const newEndpoints = addedLines.match(/(?:app|router)\.\s*(?:get|post|put|delete|patch)\s*\(/g);
|
|
468
|
+
if (newEndpoints && !hasTestFiles) {
|
|
469
|
+
testCoverageGaps.push(`${newEndpoints.length} new API endpoint(s) added without test coverage.`);
|
|
470
|
+
}
|
|
471
|
+
// Decision logic
|
|
472
|
+
const hasCritical = findings.some((f) => f.severity === "critical");
|
|
473
|
+
const hasHigh = findings.some((f) => f.severity === "high");
|
|
474
|
+
const decision = hasCritical || (hasHigh && findings.filter((f) => f.severity === "high").length >= 2)
|
|
475
|
+
? "request-changes"
|
|
476
|
+
: "approve";
|
|
477
|
+
// Summary
|
|
478
|
+
const categoryCount = {};
|
|
479
|
+
for (const f of findings) {
|
|
480
|
+
categoryCount[f.category] = (categoryCount[f.category] || 0) + 1;
|
|
481
|
+
}
|
|
482
|
+
const summaryParts = [];
|
|
483
|
+
if (findings.length === 0) {
|
|
484
|
+
summaryParts.push("Clean PR. No issues detected.");
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
summaryParts.push(`Found ${findings.length} issue(s):`);
|
|
488
|
+
for (const [cat, count] of Object.entries(categoryCount)) {
|
|
489
|
+
summaryParts.push(`${count} ${cat}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (testCoverageGaps.length > 0) {
|
|
493
|
+
summaryParts.push(`Test coverage gaps: ${testCoverageGaps.length}`);
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
decision,
|
|
497
|
+
findings,
|
|
498
|
+
summary: summaryParts.join(" "),
|
|
499
|
+
breaking_change_risk: breakingRisk,
|
|
500
|
+
test_coverage_gaps: testCoverageGaps,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Fix suggestion engine
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
function generateFix(findingId, code) {
|
|
507
|
+
const rule = ANALYSIS_RULES.find((r) => r.id === findingId);
|
|
508
|
+
if (!rule)
|
|
509
|
+
return null;
|
|
510
|
+
return {
|
|
511
|
+
fix: rule.suggestion,
|
|
512
|
+
explanation: `${rule.title}: ${rule.description}\n\nRecommended approach: ${rule.suggestion}`,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// MCP Server
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
const server = new mcp_js_1.McpServer({
|
|
519
|
+
name: "dingdawg-code-review",
|
|
520
|
+
version: "1.0.0",
|
|
521
|
+
});
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// review_code — FREE: 10 reviews/day
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
server.tool("review_code", "Scan code for security vulnerabilities, code quality issues, performance problems, and best practice violations. " +
|
|
526
|
+
"Returns findings with severity levels, line hints, and fix suggestions. FREE: 10 reviews/day.", {
|
|
527
|
+
code: zod_1.z.string().min(1, "Code cannot be empty").describe("The code snippet or file contents to review"),
|
|
528
|
+
language: zod_1.z.string().optional().describe("Programming language (typescript, javascript, python, etc.)"),
|
|
529
|
+
filename: zod_1.z.string().optional().describe("Filename for context (e.g., 'auth.ts')"),
|
|
530
|
+
}, async ({ code, language, filename }) => {
|
|
531
|
+
const rateCheck = checkFreeRateLimit("review_code");
|
|
532
|
+
if (!rateCheck.allowed) {
|
|
533
|
+
return {
|
|
534
|
+
content: [{
|
|
535
|
+
type: "text",
|
|
536
|
+
text: JSON.stringify({
|
|
537
|
+
error: "Free tier limit reached (10 code reviews per 24 hours). Your limit resets automatically.",
|
|
538
|
+
upgrade: "Get unlimited access with an API key at dingdawg.com/developers",
|
|
539
|
+
...governanceEnvelope("review_code", []),
|
|
540
|
+
}, null, 2),
|
|
541
|
+
}],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const findings = analyzeCode(code, language);
|
|
545
|
+
return {
|
|
546
|
+
content: [{
|
|
547
|
+
type: "text",
|
|
548
|
+
text: JSON.stringify({
|
|
549
|
+
filename: filename || "inline",
|
|
550
|
+
language: language || "auto-detected",
|
|
551
|
+
lines_scanned: code.split("\n").length,
|
|
552
|
+
findings,
|
|
553
|
+
also_available: {
|
|
554
|
+
suggest_fix: "Run suggest_fix with a finding ID to get a detailed fix recommendation.",
|
|
555
|
+
review_pr: "Run review_pr to review an entire PR diff with breaking change detection.",
|
|
556
|
+
compliance: "Run npx dingdawg-compliance for AI compliance reports.",
|
|
557
|
+
shield: "Run npx dingdawg-shield for AI security scanning.",
|
|
558
|
+
},
|
|
559
|
+
free_reviews_remaining: rateCheck.remaining,
|
|
560
|
+
...governanceEnvelope("review_code", findings),
|
|
561
|
+
}, null, 2),
|
|
562
|
+
}],
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// review_pr — FREE: 5 PR reviews/day
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
server.tool("review_pr", "Review a pull request diff. Checks for breaking changes, security issues, test coverage gaps. " +
|
|
569
|
+
"Returns structured review with approve/request-changes decision. FREE: 5 PR reviews/day.", {
|
|
570
|
+
diff: zod_1.z.string().min(1, "Diff cannot be empty").describe("The unified diff content of the pull request"),
|
|
571
|
+
title: zod_1.z.string().optional().describe("PR title for context"),
|
|
572
|
+
description: zod_1.z.string().optional().describe("PR description for context"),
|
|
573
|
+
}, async ({ diff, title, description }) => {
|
|
574
|
+
const rateCheck = checkFreeRateLimit("review_pr");
|
|
575
|
+
if (!rateCheck.allowed) {
|
|
576
|
+
return {
|
|
577
|
+
content: [{
|
|
578
|
+
type: "text",
|
|
579
|
+
text: JSON.stringify({
|
|
580
|
+
error: "Free tier limit reached (5 PR reviews per 24 hours). Your limit resets automatically.",
|
|
581
|
+
upgrade: "Get unlimited access with an API key at dingdawg.com/developers",
|
|
582
|
+
...governanceEnvelope("review_pr", []),
|
|
583
|
+
}, null, 2),
|
|
584
|
+
}],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const result = analyzePRDiff(diff, title);
|
|
588
|
+
return {
|
|
589
|
+
content: [{
|
|
590
|
+
type: "text",
|
|
591
|
+
text: JSON.stringify({
|
|
592
|
+
pr_title: title || "untitled",
|
|
593
|
+
decision: result.decision,
|
|
594
|
+
summary: result.summary,
|
|
595
|
+
breaking_change_risk: result.breaking_change_risk,
|
|
596
|
+
test_coverage_gaps: result.test_coverage_gaps,
|
|
597
|
+
findings: result.findings,
|
|
598
|
+
also_available: {
|
|
599
|
+
suggest_fix: "Run suggest_fix with a finding ID to get a detailed fix recommendation.",
|
|
600
|
+
review_code: "Run review_code on specific files for deeper analysis.",
|
|
601
|
+
},
|
|
602
|
+
free_pr_reviews_remaining: rateCheck.remaining,
|
|
603
|
+
...governanceEnvelope("review_pr", result.findings),
|
|
604
|
+
}, null, 2),
|
|
605
|
+
}],
|
|
606
|
+
};
|
|
607
|
+
});
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
// suggest_fix — FREE: 20 suggestions/day
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
server.tool("suggest_fix", "Given a finding ID from review_code or review_pr, generate a detailed fix suggestion with explanation. " +
|
|
612
|
+
"FREE: 20 suggestions/day.", {
|
|
613
|
+
finding_id: zod_1.z.string().describe("The finding ID from a review (e.g., SEC-001, QUAL-003, PERF-002)"),
|
|
614
|
+
code_context: zod_1.z.string().optional().describe("The code surrounding the finding for a more targeted fix"),
|
|
615
|
+
}, async ({ finding_id, code_context }) => {
|
|
616
|
+
const rateCheck = checkFreeRateLimit("suggest_fix");
|
|
617
|
+
if (!rateCheck.allowed) {
|
|
618
|
+
return {
|
|
619
|
+
content: [{
|
|
620
|
+
type: "text",
|
|
621
|
+
text: JSON.stringify({
|
|
622
|
+
error: "Free tier limit reached (20 fix suggestions per 24 hours). Your limit resets automatically.",
|
|
623
|
+
upgrade: "Get unlimited access with an API key at dingdawg.com/developers",
|
|
624
|
+
...governanceEnvelope("suggest_fix", []),
|
|
625
|
+
}, null, 2),
|
|
626
|
+
}],
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const fix = generateFix(finding_id, code_context || "");
|
|
630
|
+
if (!fix) {
|
|
631
|
+
return {
|
|
632
|
+
content: [{
|
|
633
|
+
type: "text",
|
|
634
|
+
text: JSON.stringify({
|
|
635
|
+
finding_id,
|
|
636
|
+
error: `No rule found for finding ID '${finding_id}'. Valid IDs start with SEC-, QUAL-, PERF-, BP-, or BREAK-.`,
|
|
637
|
+
available_ids: ANALYSIS_RULES.map((r) => r.id),
|
|
638
|
+
...governanceEnvelope("suggest_fix", []),
|
|
639
|
+
}, null, 2),
|
|
640
|
+
}],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
// Build a contextual finding for governance
|
|
644
|
+
const rule = ANALYSIS_RULES.find((r) => r.id === finding_id);
|
|
645
|
+
const finding = {
|
|
646
|
+
id: rule.id,
|
|
647
|
+
severity: rule.severity,
|
|
648
|
+
category: rule.category,
|
|
649
|
+
title: rule.title,
|
|
650
|
+
description: rule.description,
|
|
651
|
+
suggestion: rule.suggestion,
|
|
652
|
+
};
|
|
653
|
+
return {
|
|
654
|
+
content: [{
|
|
655
|
+
type: "text",
|
|
656
|
+
text: JSON.stringify({
|
|
657
|
+
finding_id,
|
|
658
|
+
severity: rule.severity,
|
|
659
|
+
category: rule.category,
|
|
660
|
+
fix: fix.fix,
|
|
661
|
+
explanation: fix.explanation,
|
|
662
|
+
also_available: {
|
|
663
|
+
review_code: "Run review_code to scan an entire file for issues.",
|
|
664
|
+
review_pr: "Run review_pr to review a full PR diff.",
|
|
665
|
+
},
|
|
666
|
+
free_suggestions_remaining: rateCheck.remaining,
|
|
667
|
+
...governanceEnvelope("suggest_fix", [finding]),
|
|
668
|
+
}, null, 2),
|
|
669
|
+
}],
|
|
670
|
+
};
|
|
671
|
+
});
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// Start
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
async function main() {
|
|
676
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
677
|
+
await server.connect(transport);
|
|
678
|
+
}
|
|
679
|
+
main().catch((err) => { console.error("Server failed:", err); process.exit(1); });
|