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.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * dingdawg-code-review — AI Code Review Agent MCP Server
4
+ * dingdawg-code-review v2 Thin Client MCP Server
5
5
  *
6
- * Governed. Receipted. Production-ready.
6
+ * FREE tier: basic local pattern-matching code review (the hook)
7
+ * PAID tier: LLM-powered deep analysis via DingDawg API
7
8
  *
8
9
  * Install: npx dingdawg-code-review
9
10
  * Claude Code: claude mcp add dingdawg-code-review npx dingdawg-code-review
11
+ *
12
+ * Set DINGDAWG_API_KEY for paid features:
13
+ * export DINGDAWG_API_KEY=your_key
14
+ *
15
+ * Optional: DINGDAWG_MODEL to choose the analysis model (default: gpt-5.4-mini)
10
16
  */
11
17
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
18
  if (k2 === undefined) k2 = k;
@@ -50,19 +56,27 @@ const os = __importStar(require("os"));
50
56
  const path = __importStar(require("path"));
51
57
  const crypto = __importStar(require("crypto"));
52
58
  // ---------------------------------------------------------------------------
59
+ // Configuration
60
+ // ---------------------------------------------------------------------------
61
+ const API_BASE = process.env.DINGDAWG_API_URL || "https://api-production-f951.up.railway.app";
62
+ const API_ENDPOINT = `${API_BASE}/v1/govern/execute`;
63
+ const API_KEY = process.env.DINGDAWG_API_KEY || "";
64
+ const MODEL = process.env.DINGDAWG_MODEL || "gpt-5.4-mini";
65
+ // ---------------------------------------------------------------------------
53
66
  // Persistent rate limiting — survives process restart via filesystem
54
67
  // ---------------------------------------------------------------------------
68
+ const FREE_TIER_LIMIT = 10;
55
69
  const RATE_FILE = path.join(os.homedir(), ".dingdawg", "code-review-usage.json");
56
70
  const MACHINE_ID = crypto.createHash("sha256")
57
71
  .update(`${os.hostname()}-${os.userInfo().username}-${os.platform()}-${os.arch()}`)
58
72
  .digest("hex").slice(0, 16);
59
- const FREE_LIMITS = {
60
- review_code: 10,
61
- review_pr: 5,
62
- suggest_fix: 20,
63
- };
64
73
  function checkFreeRateLimit(tool) {
65
- const limit = FREE_LIMITS[tool] ?? 10;
74
+ const limits = {
75
+ review_code: 10,
76
+ review_pr: 5,
77
+ suggest_fix: 20,
78
+ };
79
+ const limit = limits[tool] ?? FREE_TIER_LIMIT;
66
80
  const key = `${MACHINE_ID}_${tool}`;
67
81
  const now = Date.now();
68
82
  const dayMs = 24 * 60 * 60 * 1000;
@@ -97,301 +111,72 @@ function checkFreeRateLimit(tool) {
97
111
  const current = store[key].count;
98
112
  return { allowed: true, remaining: limit - current };
99
113
  }
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;
114
+ async function callApi(tool, input) {
115
+ if (!API_KEY) {
116
+ return { success: false, error: "no_api_key" };
110
117
  }
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
- };
118
+ try {
119
+ const res = await fetch(API_ENDPOINT, {
120
+ method: "POST",
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ "Authorization": `Bearer ${API_KEY}`,
124
+ },
125
+ body: JSON.stringify({
126
+ agent: "code-review",
127
+ tool,
128
+ input,
129
+ model: MODEL,
130
+ }),
131
+ });
132
+ if (!res.ok) {
133
+ const body = await res.text().catch(() => "");
134
+ return { success: false, error: `API returned ${res.status}: ${body}` };
135
+ }
136
+ const data = await res.json();
137
+ return { success: true, data };
138
+ }
139
+ catch (err) {
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ return { success: false, error: `API request failed: ${message}` };
142
+ }
143
+ }
144
+ function upgradeMessage() {
145
+ return [
146
+ "",
147
+ "━━━ Upgrade to DingDawg Pro ━━━",
148
+ "Get LLM-powered deep code analysis, architecture review,",
149
+ "security audit, and automated fix generation.",
150
+ "",
151
+ " export DINGDAWG_API_KEY=your_key",
152
+ "",
153
+ "Get your key at: https://dingdawg.com/developers",
154
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
155
+ ].join("\n");
127
156
  }
128
157
  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
- },
158
+ { 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." },
159
+ { 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." },
160
+ { 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." },
161
+ { 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." },
162
+ { 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." },
163
+ { 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." },
164
+ { 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." },
165
+ { 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." },
166
+ { 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." },
167
+ { 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(...))." },
168
+ { 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." },
376
169
  ];
377
- function analyzeCode(code, language) {
170
+ function analyzeCodeLocal(code) {
378
171
  const findings = [];
379
- const lines = code.split("\n");
380
172
  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
173
  rule.pattern.lastIndex = 0;
387
174
  let match;
388
175
  const matchedLines = [];
389
176
  while ((match = rule.pattern.exec(code)) !== null) {
390
- // Find the line number
391
177
  const beforeMatch = code.slice(0, match.index);
392
178
  const lineNum = beforeMatch.split("\n").length;
393
179
  matchedLines.push(`line ${lineNum}`);
394
- // Prevent infinite loops on zero-width matches
395
180
  if (match[0].length === 0) {
396
181
  rule.pattern.lastIndex++;
397
182
  }
@@ -408,122 +193,23 @@ function analyzeCode(code, language) {
408
193
  });
409
194
  }
410
195
  }
411
- // Sort findings: critical first, then high, medium, low, info
412
196
  const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
413
197
  findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
414
198
  return findings;
415
199
  }
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
200
  // ---------------------------------------------------------------------------
516
201
  // MCP Server
517
202
  // ---------------------------------------------------------------------------
518
203
  const server = new mcp_js_1.McpServer({
519
204
  name: "dingdawg-code-review",
520
- version: "1.0.0",
205
+ version: "2.0.0",
521
206
  });
522
207
  // ---------------------------------------------------------------------------
523
- // review_code — FREE: 10 reviews/day
208
+ // review_code — FREE local scan + API deep analysis
524
209
  // ---------------------------------------------------------------------------
525
210
  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.", {
211
+ "Returns findings with severity levels, line hints, and fix suggestions. FREE: 10 reviews/day. " +
212
+ "Deep LLM-powered analysis available with API key.", {
527
213
  code: zod_1.z.string().min(1, "Code cannot be empty").describe("The code snippet or file contents to review"),
528
214
  language: zod_1.z.string().optional().describe("Programming language (typescript, javascript, python, etc.)"),
529
215
  filename: zod_1.z.string().optional().describe("Filename for context (e.g., 'auth.ts')"),
@@ -534,39 +220,69 @@ server.tool("review_code", "Scan code for security vulnerabilities, code quality
534
220
  content: [{
535
221
  type: "text",
536
222
  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),
223
+ error: "Free tier limit reached (10 code reviews per 24 hours). Resets automatically.",
224
+ upgrade: "Get unlimited access: export DINGDAWG_API_KEY=your_key https://dingdawg.com/developers",
225
+ governed: true,
226
+ }),
541
227
  }],
542
228
  };
543
229
  }
544
- const findings = analyzeCode(code, language);
230
+ // Local analysis free tier
231
+ const localFindings = analyzeCodeLocal(code);
232
+ // If API key is set, enhance with LLM-powered deep analysis
233
+ if (API_KEY) {
234
+ const apiResult = await callApi("review_code", {
235
+ code,
236
+ language: language || "",
237
+ filename: filename || "",
238
+ });
239
+ if (apiResult.success && apiResult.data) {
240
+ return {
241
+ content: [{
242
+ type: "text",
243
+ text: JSON.stringify({
244
+ mode: "deep_analysis",
245
+ powered_by: "DingDawg Code Review API",
246
+ ...apiResult.data,
247
+ receipt_id: `cr_${Date.now().toString(36)}`,
248
+ governed: true,
249
+ }, null, 2),
250
+ }],
251
+ };
252
+ }
253
+ }
254
+ // Free tier response with teaser
545
255
  return {
546
256
  content: [{
547
257
  type: "text",
548
258
  text: JSON.stringify({
259
+ mode: "local_basic",
549
260
  filename: filename || "inline",
550
261
  language: language || "auto-detected",
551
262
  lines_scanned: code.split("\n").length,
552
- findings,
263
+ findings: localFindings.slice(0, 5),
264
+ total_issues: localFindings.length,
265
+ teaser: localFindings.length > 0
266
+ ? `Found ${localFindings.length} issue(s). Get LLM-powered deep analysis, architecture review, and automated fixes with: export DINGDAWG_API_KEY=your_key`
267
+ : "No issues detected with basic scan. Deep LLM analysis catches 10x more. Get your key at https://dingdawg.com/developers",
268
+ upgrade_url: "https://dingdawg.com/developers",
553
269
  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.",
270
+ suggest_fix: "Run suggest_fix with a finding ID for fix suggestions.",
271
+ review_pr: "Run review_pr to review a PR diff.",
558
272
  },
273
+ receipt_id: `cr_${Date.now().toString(36)}`,
559
274
  free_reviews_remaining: rateCheck.remaining,
560
- ...governanceEnvelope("review_code", findings),
275
+ governed: true,
561
276
  }, null, 2),
562
277
  }],
563
278
  };
564
279
  });
565
280
  // ---------------------------------------------------------------------------
566
- // review_pr — FREE: 5 PR reviews/day
281
+ // review_pr — FREE basic + PAID deep PR review
567
282
  // ---------------------------------------------------------------------------
568
283
  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.", {
284
+ "Returns structured review with approve/request-changes decision. FREE: 5 PR reviews/day. " +
285
+ "Deep LLM-powered review available with API key.", {
570
286
  diff: zod_1.z.string().min(1, "Diff cannot be empty").describe("The unified diff content of the pull request"),
571
287
  title: zod_1.z.string().optional().describe("PR title for context"),
572
288
  description: zod_1.z.string().optional().describe("PR description for context"),
@@ -577,40 +293,70 @@ server.tool("review_pr", "Review a pull request diff. Checks for breaking change
577
293
  content: [{
578
294
  type: "text",
579
295
  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),
296
+ error: "Free tier limit reached (5 PR reviews per 24 hours). Resets automatically.",
297
+ upgrade: "Get unlimited access: export DINGDAWG_API_KEY=your_key https://dingdawg.com/developers",
298
+ governed: true,
299
+ }),
584
300
  }],
585
301
  };
586
302
  }
587
- const result = analyzePRDiff(diff, title);
303
+ // If API key is set, use deep analysis
304
+ if (API_KEY) {
305
+ const apiResult = await callApi("review_pr", {
306
+ diff,
307
+ title: title || "",
308
+ description: description || "",
309
+ });
310
+ if (apiResult.success && apiResult.data) {
311
+ return {
312
+ content: [{
313
+ type: "text",
314
+ text: JSON.stringify({
315
+ mode: "deep_analysis",
316
+ powered_by: "DingDawg Code Review API",
317
+ ...apiResult.data,
318
+ receipt_id: `pr_${Date.now().toString(36)}`,
319
+ governed: true,
320
+ }, null, 2),
321
+ }],
322
+ };
323
+ }
324
+ }
325
+ // Free tier — basic analysis on added lines
326
+ const addedLines = diff.split("\n")
327
+ .filter((line) => line.startsWith("+") && !line.startsWith("+++"))
328
+ .map((line) => line.slice(1))
329
+ .join("\n");
330
+ const findings = analyzeCodeLocal(addedLines);
331
+ const hasCritical = findings.some((f) => f.severity === "critical");
332
+ const highCount = findings.filter((f) => f.severity === "high").length;
333
+ const decision = hasCritical || highCount >= 2 ? "request-changes" : "approve";
588
334
  return {
589
335
  content: [{
590
336
  type: "text",
591
337
  text: JSON.stringify({
338
+ mode: "local_basic",
592
339
  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
- },
340
+ decision,
341
+ findings_count: findings.length,
342
+ top_findings: findings.slice(0, 3),
343
+ teaser: findings.length > 0
344
+ ? `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`
345
+ : "Basic scan clean. LLM-powered review provides breaking change detection, test coverage analysis, and architecture review.",
346
+ upgrade_url: "https://dingdawg.com/developers",
347
+ receipt_id: `pr_${Date.now().toString(36)}`,
602
348
  free_pr_reviews_remaining: rateCheck.remaining,
603
- ...governanceEnvelope("review_pr", result.findings),
349
+ governed: true,
604
350
  }, null, 2),
605
351
  }],
606
352
  };
607
353
  });
608
354
  // ---------------------------------------------------------------------------
609
- // suggest_fix — FREE: 20 suggestions/day
355
+ // suggest_fix — FREE basic + PAID LLM fix generation
610
356
  // ---------------------------------------------------------------------------
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)"),
357
+ server.tool("suggest_fix", "Given a finding ID from review_code or review_pr, generate a fix suggestion. " +
358
+ "FREE: 20 suggestions/day. LLM-powered code generation available with API key.", {
359
+ finding_id: zod_1.z.string().describe("The finding ID from a review (e.g., SEC-001, QUAL-003)"),
614
360
  code_context: zod_1.z.string().optional().describe("The code surrounding the finding for a more targeted fix"),
615
361
  }, async ({ finding_id, code_context }) => {
616
362
  const rateCheck = checkFreeRateLimit("suggest_fix");
@@ -619,52 +365,63 @@ server.tool("suggest_fix", "Given a finding ID from review_code or review_pr, ge
619
365
  content: [{
620
366
  type: "text",
621
367
  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),
368
+ error: "Free tier limit reached (20 fix suggestions per 24 hours). Resets automatically.",
369
+ upgrade: "Get unlimited access: export DINGDAWG_API_KEY=your_key https://dingdawg.com/developers",
370
+ governed: true,
371
+ }),
626
372
  }],
627
373
  };
628
374
  }
629
- const fix = generateFix(finding_id, code_context || "");
630
- if (!fix) {
375
+ // If API key is set, get LLM-powered fix
376
+ if (API_KEY) {
377
+ const apiResult = await callApi("suggest_fix", {
378
+ finding_id,
379
+ code_context: code_context || "",
380
+ });
381
+ if (apiResult.success && apiResult.data) {
382
+ return {
383
+ content: [{
384
+ type: "text",
385
+ text: JSON.stringify({
386
+ mode: "deep_analysis",
387
+ powered_by: "DingDawg Code Review API",
388
+ ...apiResult.data,
389
+ receipt_id: `fix_${Date.now().toString(36)}`,
390
+ governed: true,
391
+ }, null, 2),
392
+ }],
393
+ };
394
+ }
395
+ }
396
+ // Free tier — match against local rules
397
+ const rule = ANALYSIS_RULES.find((r) => r.id === finding_id);
398
+ if (!rule) {
631
399
  return {
632
400
  content: [{
633
401
  type: "text",
634
402
  text: JSON.stringify({
635
403
  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", []),
404
+ error: `No rule found for '${finding_id}'. Valid IDs: ${ANALYSIS_RULES.map((r) => r.id).join(", ")}`,
405
+ governed: true,
639
406
  }, null, 2),
640
407
  }],
641
408
  };
642
409
  }
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
410
  return {
654
411
  content: [{
655
412
  type: "text",
656
413
  text: JSON.stringify({
414
+ mode: "local_basic",
657
415
  finding_id,
658
416
  severity: rule.severity,
659
417
  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
- },
418
+ fix: rule.suggestion,
419
+ explanation: `${rule.title}: ${rule.description}\n\nRecommended: ${rule.suggestion}`,
420
+ teaser: "Get LLM-powered code generation with actual fix diffs using: export DINGDAWG_API_KEY=your_key",
421
+ upgrade_url: "https://dingdawg.com/developers",
422
+ receipt_id: `fix_${Date.now().toString(36)}`,
666
423
  free_suggestions_remaining: rateCheck.remaining,
667
- ...governanceEnvelope("suggest_fix", [finding]),
424
+ governed: true,
668
425
  }, null, 2),
669
426
  }],
670
427
  };