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/src/index.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * dingdawg-code-review — AI Code Review Agent MCP Server
3
+ * dingdawg-code-review v2 Thin Client MCP Server
4
4
  *
5
- * Governed. Receipted. Production-ready.
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 limit = FREE_LIMITS[tool] ?? 10;
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
- // Receipt & governance helpers
82
+ // API Client calls DingDawg API for paid features
68
83
  // ---------------------------------------------------------------------------
69
84
 
70
- function generateReceiptId(): string {
71
- return `rcpt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
85
+ interface ApiResponse {
86
+ success: boolean;
87
+ data?: Record<string, unknown>;
88
+ error?: string;
72
89
  }
73
90
 
74
- function governanceEnvelope(
91
+ async function callApi(
75
92
  tool: string,
76
- findings: Finding[],
77
- extra: Record<string, unknown> = {},
78
- ): Record<string, unknown> {
79
- const severityCounts: Record<string, number> = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
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
- return {
85
- receipt_id: generateReceiptId(),
86
- timestamp: new Date().toISOString(),
87
- tool,
88
- governance_policy: "dingdawg-code-review/v1.0.0",
89
- rules_checked: [
90
- "security-vulnerabilities",
91
- "code-quality",
92
- "performance",
93
- "best-practices",
94
- ],
95
- findings_count: findings.length,
96
- findings_by_severity: severityCounts,
97
- governed: true,
98
- ...extra,
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
- // Finding types
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: 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: 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
- // --- Security ---
134
- {
135
- id: "SEC-001",
136
- pattern: /eval\s*\(/g,
137
- severity: "critical",
138
- category: "security",
139
- title: "Use of eval()",
140
- description: "eval() executes arbitrary code and is a major injection vector. Attackers can exploit this to run malicious code.",
141
- suggestion: "Replace eval() with JSON.parse() for data, or use a safe expression parser. Never pass user input to eval().",
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 analyzeCode(code: string, language?: string): Finding[] {
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
- // Sort findings: critical first, then high, medium, low, info
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: "1.0.0",
218
+ version: "2.0.0",
572
219
  });
573
220
 
574
221
  // ---------------------------------------------------------------------------
575
- // review_code — FREE: 10 reviews/day
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). Your limit resets automatically.",
595
- upgrade: "Get unlimited access with an API key at dingdawg.com/developers",
596
- ...governanceEnvelope("review_code", []),
597
- }, null, 2),
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
- const findings = analyzeCode(code, language);
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 to get a detailed fix recommendation.",
614
- review_pr: "Run review_pr to review an entire PR diff with breaking change detection.",
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
- ...governanceEnvelope("review_code", findings),
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: 5 PR reviews/day
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). Your limit resets automatically.",
647
- upgrade: "Get unlimited access with an API key at dingdawg.com/developers",
648
- ...governanceEnvelope("review_pr", []),
649
- }, null, 2),
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
- const result = analyzePRDiff(diff, title);
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: result.decision,
662
- summary: result.summary,
663
- breaking_change_risk: result.breaking_change_risk,
664
- test_coverage_gaps: result.test_coverage_gaps,
665
- findings: result.findings,
666
- also_available: {
667
- suggest_fix: "Run suggest_fix with a finding ID to get a detailed fix recommendation.",
668
- review_code: "Run review_code on specific files for deeper analysis.",
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
- ...governanceEnvelope("review_pr", result.findings),
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: 20 suggestions/day
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 detailed fix suggestion with explanation. " +
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, PERF-002)"),
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). Your limit resets automatically.",
698
- upgrade: "Get unlimited access with an API key at dingdawg.com/developers",
699
- ...governanceEnvelope("suggest_fix", []),
700
- }, null, 2),
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
- const fix = generateFix(finding_id, code_context || "");
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
- if (!fix) {
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 finding ID '${finding_id}'. Valid IDs start with SEC-, QUAL-, PERF-, BP-, or BREAK-.`,
714
- available_ids: ANALYSIS_RULES.map((r) => r.id),
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: fix.fix,
740
- explanation: fix.explanation,
741
- also_available: {
742
- review_code: "Run review_code to scan an entire file for issues.",
743
- review_pr: "Run review_pr to review a full PR diff.",
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
- ...governanceEnvelope("suggest_fix", [finding]),
470
+ governed: true,
747
471
  }, null, 2),
748
472
  }],
749
473
  };