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