vibecop 0.1.2 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +133 -28
  2. package/dist/cli.js +1188 -700
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -44,6 +44,7 @@ var __export = (target, all) => {
44
44
  set: __exportSetter.bind(all, name)
45
45
  });
46
46
  };
47
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
48
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
48
49
 
49
50
  // node_modules/commander/lib/error.js
@@ -9044,9 +9045,274 @@ var require_dist = __commonJS((exports) => {
9044
9045
  exports.visitAsync = visit.visitAsync;
9045
9046
  });
9046
9047
 
9047
- // src/cli.ts
9048
+ // src/init.ts
9049
+ var exports_init = {};
9050
+ __export(exports_init, {
9051
+ runInit: () => runInit
9052
+ });
9048
9053
  import { execSync } from "node:child_process";
9049
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
9054
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
9055
+ import { join as join5 } from "node:path";
9056
+ function detectTools(cwd) {
9057
+ const tools = [];
9058
+ tools.push({
9059
+ name: "Claude Code",
9060
+ detected: existsSync4(join5(cwd, ".claude")),
9061
+ reason: existsSync4(join5(cwd, ".claude")) ? ".claude/ directory found" : "not found"
9062
+ });
9063
+ tools.push({
9064
+ name: "Cursor",
9065
+ detected: existsSync4(join5(cwd, ".cursor")),
9066
+ reason: existsSync4(join5(cwd, ".cursor")) ? ".cursor/ directory found" : "not found"
9067
+ });
9068
+ tools.push({
9069
+ name: "Codex CLI",
9070
+ detected: existsSync4(join5(cwd, ".codex")),
9071
+ reason: existsSync4(join5(cwd, ".codex")) ? ".codex/ directory found" : "not found"
9072
+ });
9073
+ let aiderInstalled = false;
9074
+ try {
9075
+ execSync("which aider", { stdio: "pipe" });
9076
+ aiderInstalled = true;
9077
+ } catch {
9078
+ aiderInstalled = false;
9079
+ }
9080
+ tools.push({
9081
+ name: "Aider",
9082
+ detected: aiderInstalled,
9083
+ reason: aiderInstalled ? "aider installed" : "not found"
9084
+ });
9085
+ tools.push({
9086
+ name: "Windsurf",
9087
+ detected: existsSync4(join5(cwd, ".windsurf")),
9088
+ reason: existsSync4(join5(cwd, ".windsurf")) ? ".windsurf/ directory found" : "not found"
9089
+ });
9090
+ tools.push({
9091
+ name: "GitHub Copilot",
9092
+ detected: existsSync4(join5(cwd, ".github")),
9093
+ reason: existsSync4(join5(cwd, ".github")) ? ".github/ directory found" : "not found"
9094
+ });
9095
+ const clineDetected = existsSync4(join5(cwd, ".cline")) || existsSync4(join5(cwd, ".clinerules"));
9096
+ tools.push({
9097
+ name: "Cline",
9098
+ detected: clineDetected,
9099
+ reason: clineDetected ? existsSync4(join5(cwd, ".cline")) ? ".cline/ directory found" : ".clinerules found" : "not found"
9100
+ });
9101
+ return tools;
9102
+ }
9103
+ function generateConfigs(cwd, tools) {
9104
+ const generated = [];
9105
+ for (const tool of tools) {
9106
+ if (!tool.detected)
9107
+ continue;
9108
+ switch (tool.name) {
9109
+ case "Claude Code": {
9110
+ const settingsPath = join5(cwd, ".claude", "settings.json");
9111
+ if (existsSync4(settingsPath)) {
9112
+ generated.push({
9113
+ path: ".claude/settings.json",
9114
+ description: "already exists, skipped"
9115
+ });
9116
+ } else {
9117
+ mkdirSync(join5(cwd, ".claude"), { recursive: true });
9118
+ const settings = {
9119
+ hooks: {
9120
+ PostToolUse: [
9121
+ {
9122
+ matcher: "Edit|Write|MultiEdit",
9123
+ hooks: [
9124
+ {
9125
+ type: "command",
9126
+ command: SCAN_CMD
9127
+ }
9128
+ ]
9129
+ }
9130
+ ]
9131
+ }
9132
+ };
9133
+ writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
9134
+ `);
9135
+ generated.push({
9136
+ path: ".claude/settings.json",
9137
+ description: "PostToolUse hook (blocks on findings)"
9138
+ });
9139
+ }
9140
+ break;
9141
+ }
9142
+ case "Cursor": {
9143
+ mkdirSync(join5(cwd, ".cursor"), { recursive: true });
9144
+ const hooksPath = join5(cwd, ".cursor", "hooks.json");
9145
+ const hooks = {
9146
+ hooks: {
9147
+ afterFileEdit: [{ command: SCAN_CMD }]
9148
+ }
9149
+ };
9150
+ writeFileSync(hooksPath, `${JSON.stringify(hooks, null, 2)}
9151
+ `);
9152
+ generated.push({
9153
+ path: ".cursor/hooks.json",
9154
+ description: "afterFileEdit hook"
9155
+ });
9156
+ mkdirSync(join5(cwd, ".cursor", "rules"), { recursive: true });
9157
+ const rulesPath = join5(cwd, ".cursor", "rules", "vibecop.md");
9158
+ const rulesContent = `---
9159
+ trigger: always_on
9160
+ ---
9161
+
9162
+ After every code edit, review vibecop findings and fix issues before proceeding.
9163
+ Run: ${SCAN_CMD}
9164
+ `;
9165
+ writeFileSync(rulesPath, rulesContent);
9166
+ generated.push({
9167
+ path: ".cursor/rules/vibecop.md",
9168
+ description: "always-on lint rule"
9169
+ });
9170
+ break;
9171
+ }
9172
+ case "Codex CLI": {
9173
+ mkdirSync(join5(cwd, ".codex"), { recursive: true });
9174
+ const codexPath = join5(cwd, ".codex", "hooks.json");
9175
+ const codexHooks = {
9176
+ hooks: {
9177
+ PostToolUse: [
9178
+ {
9179
+ matcher: "Edit|Write|MultiEdit",
9180
+ hooks: [
9181
+ {
9182
+ type: "command",
9183
+ command: SCAN_CMD
9184
+ }
9185
+ ]
9186
+ }
9187
+ ]
9188
+ }
9189
+ };
9190
+ writeFileSync(codexPath, `${JSON.stringify(codexHooks, null, 2)}
9191
+ `);
9192
+ generated.push({
9193
+ path: ".codex/hooks.json",
9194
+ description: "PostToolUse hook (blocks on findings)"
9195
+ });
9196
+ break;
9197
+ }
9198
+ case "Aider": {
9199
+ const aiderPath = join5(cwd, ".aider.conf.yml");
9200
+ const aiderContent = `lint-cmd:
9201
+ - "typescript: npx vibecop scan --format text"
9202
+ - "javascript: npx vibecop scan --format text"
9203
+ - "python: npx vibecop scan --format text"
9204
+ auto-lint: true
9205
+ `;
9206
+ writeFileSync(aiderPath, aiderContent);
9207
+ generated.push({
9208
+ path: ".aider.conf.yml",
9209
+ description: "lint-cmd per language"
9210
+ });
9211
+ break;
9212
+ }
9213
+ case "Windsurf": {
9214
+ mkdirSync(join5(cwd, ".windsurf", "rules"), { recursive: true });
9215
+ const windsurfPath = join5(cwd, ".windsurf", "rules", "vibecop.md");
9216
+ const windsurfContent = `---
9217
+ trigger: always_on
9218
+ ---
9219
+
9220
+ After every code edit, review vibecop findings and fix issues before proceeding.
9221
+ Run: ${SCAN_CMD}
9222
+ `;
9223
+ writeFileSync(windsurfPath, windsurfContent);
9224
+ generated.push({
9225
+ path: ".windsurf/rules/vibecop.md",
9226
+ description: "always-on lint rule"
9227
+ });
9228
+ break;
9229
+ }
9230
+ case "GitHub Copilot": {
9231
+ mkdirSync(join5(cwd, ".github"), { recursive: true });
9232
+ const copilotPath = join5(cwd, ".github", "copilot-instructions.md");
9233
+ const vibecopSection = `
9234
+ ## vibecop
9235
+
9236
+ After every code edit, run \`${SCAN_CMD}\` and fix any findings before proceeding.
9237
+ `;
9238
+ if (existsSync4(copilotPath)) {
9239
+ const existing = readFileSync6(copilotPath, "utf-8");
9240
+ if (existing.includes("vibecop")) {
9241
+ generated.push({
9242
+ path: ".github/copilot-instructions.md",
9243
+ description: "already contains vibecop, skipped"
9244
+ });
9245
+ } else {
9246
+ writeFileSync(copilotPath, existing + vibecopSection);
9247
+ generated.push({
9248
+ path: ".github/copilot-instructions.md",
9249
+ description: "appended vibecop instructions"
9250
+ });
9251
+ }
9252
+ } else {
9253
+ writeFileSync(copilotPath, vibecopSection);
9254
+ generated.push({
9255
+ path: ".github/copilot-instructions.md",
9256
+ description: "copilot instructions"
9257
+ });
9258
+ }
9259
+ break;
9260
+ }
9261
+ case "Cline": {
9262
+ const clinePath = join5(cwd, ".clinerules");
9263
+ const clineContent = `After every code edit, run \`${SCAN_CMD}\` and fix any findings before proceeding.
9264
+ `;
9265
+ writeFileSync(clinePath, clineContent);
9266
+ generated.push({
9267
+ path: ".clinerules",
9268
+ description: "always-on lint rule"
9269
+ });
9270
+ break;
9271
+ }
9272
+ }
9273
+ }
9274
+ return generated;
9275
+ }
9276
+ function padEnd(str, len) {
9277
+ return str + " ".repeat(Math.max(0, len - str.length));
9278
+ }
9279
+ async function runInit(cwd) {
9280
+ const root = cwd ?? process.cwd();
9281
+ console.log("");
9282
+ console.log(" vibecop — agent integration setup");
9283
+ console.log("");
9284
+ const tools = detectTools(root);
9285
+ const anyDetected = tools.some((t) => t.detected);
9286
+ if (!anyDetected) {
9287
+ console.log(" No supported AI coding tools detected.");
9288
+ console.log(" See docs/agent-integration.md for manual setup.");
9289
+ console.log("");
9290
+ return;
9291
+ }
9292
+ console.log(" Detected tools:");
9293
+ for (const tool of tools) {
9294
+ const icon = tool.detected ? "✓" : "✗";
9295
+ console.log(` ${icon} ${tool.name} (${tool.reason})`);
9296
+ }
9297
+ console.log("");
9298
+ const generated = generateConfigs(root, tools);
9299
+ if (generated.length > 0) {
9300
+ const maxPath = Math.max(...generated.map((g) => g.path.length));
9301
+ console.log(" Generated:");
9302
+ for (const file of generated) {
9303
+ console.log(` ${padEnd(file.path, maxPath)} — ${file.description}`);
9304
+ }
9305
+ console.log("");
9306
+ }
9307
+ console.log(" Done! vibecop will now run automatically in your agent workflow.");
9308
+ console.log("");
9309
+ }
9310
+ var SCAN_CMD = "npx vibecop scan --diff HEAD --format agent";
9311
+ var init_init = () => {};
9312
+
9313
+ // src/cli.ts
9314
+ import { execSync as execSync2 } from "node:child_process";
9315
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
9050
9316
  import { extname as extname2, relative as relative2, resolve as resolve4 } from "node:path";
9051
9317
 
9052
9318
  // node_modules/commander/esm.mjs
@@ -13110,6 +13376,35 @@ function isNodeError(err) {
13110
13376
  return err instanceof Error && "code" in err;
13111
13377
  }
13112
13378
 
13379
+ // src/detectors/utils.ts
13380
+ function makeFinding(detectorId, ctx, node, message, severity, suggestion) {
13381
+ const range = node.range();
13382
+ return {
13383
+ detectorId,
13384
+ message,
13385
+ severity,
13386
+ file: ctx.file.path,
13387
+ line: range.start.line + 1,
13388
+ column: range.start.column + 1,
13389
+ endLine: range.end.line + 1,
13390
+ endColumn: range.end.column + 1,
13391
+ ...suggestion != null && { suggestion }
13392
+ };
13393
+ }
13394
+ function makeLineFinding(detectorId, ctx, line, column, message, severity, suggestion, endLine, endColumn) {
13395
+ return {
13396
+ detectorId,
13397
+ message,
13398
+ severity,
13399
+ file: ctx.file.path,
13400
+ line,
13401
+ column,
13402
+ ...suggestion != null && { suggestion },
13403
+ ...endLine != null && { endLine },
13404
+ ...endColumn != null && { endColumn }
13405
+ };
13406
+ }
13407
+
13113
13408
  // src/detectors/empty-error-handler.ts
13114
13409
  var CONSOLE_METHODS = new Set(["console.log", "console.error", "console.warn"]);
13115
13410
  var PYTHON_LOG_FUNCTIONS = new Set(["print", "logging.debug", "logging.info", "logging.warning", "logging.error"]);
@@ -13140,19 +13435,8 @@ function detectJavaScriptCatchBlocks(ctx) {
13140
13435
  const hasComment = body.children().some((ch) => ch.kind() === "comment");
13141
13436
  if (hasComment)
13142
13437
  continue;
13143
- const range = catchNode.range();
13144
13438
  if (bodyChildren.length === 0) {
13145
- findings.push({
13146
- detectorId: "empty-error-handler",
13147
- message: "Empty catch block silently swallows errors",
13148
- severity: "warning",
13149
- file: ctx.file.path,
13150
- line: range.start.line + 1,
13151
- column: range.start.column + 1,
13152
- endLine: range.end.line + 1,
13153
- endColumn: range.end.column + 1,
13154
- suggestion: "Add error handling, re-throw the error, or add a comment explaining why the error is intentionally ignored"
13155
- });
13439
+ findings.push(makeFinding("empty-error-handler", ctx, catchNode, "Empty catch block silently swallows errors", "warning", "Add error handling, re-throw the error, or add a comment explaining why the error is intentionally ignored"));
13156
13440
  continue;
13157
13441
  }
13158
13442
  if (bodyChildren.length === 1) {
@@ -13160,17 +13444,7 @@ function detectJavaScriptCatchBlocks(ctx) {
13160
13444
  if (stmt.kind() === "expression_statement") {
13161
13445
  const stmtText = stmt.text().replace(/;$/, "").trim();
13162
13446
  if (isLogOnlyCall(stmtText)) {
13163
- findings.push({
13164
- detectorId: "empty-error-handler",
13165
- message: "Catch block only logs the error without handling it",
13166
- severity: "warning",
13167
- file: ctx.file.path,
13168
- line: range.start.line + 1,
13169
- column: range.start.column + 1,
13170
- endLine: range.end.line + 1,
13171
- endColumn: range.end.column + 1,
13172
- suggestion: "Add proper error handling: re-throw, return a fallback value, or propagate the error"
13173
- });
13447
+ findings.push(makeFinding("empty-error-handler", ctx, catchNode, "Catch block only logs the error without handling it", "warning", "Add proper error handling: re-throw, return a fallback value, or propagate the error"));
13174
13448
  }
13175
13449
  }
13176
13450
  }
@@ -13187,38 +13461,17 @@ function detectPythonExceptBlocks(ctx) {
13187
13461
  if (!block)
13188
13462
  continue;
13189
13463
  const blockChildren = block.children();
13190
- const range = exceptNode.range();
13191
13464
  const exceptText = exceptNode.text();
13192
13465
  if (exceptText.includes("#"))
13193
13466
  continue;
13194
13467
  if (blockChildren.length === 1 && blockChildren[0].kind() === "pass_statement") {
13195
- findings.push({
13196
- detectorId: "empty-error-handler",
13197
- message: "Except block with only 'pass' silently swallows errors",
13198
- severity: "warning",
13199
- file: ctx.file.path,
13200
- line: range.start.line + 1,
13201
- column: range.start.column + 1,
13202
- endLine: range.end.line + 1,
13203
- endColumn: range.end.column + 1,
13204
- suggestion: "Add error handling, re-raise the exception, or add a comment explaining why the error is intentionally ignored"
13205
- });
13468
+ findings.push(makeFinding("empty-error-handler", ctx, exceptNode, "Except block with only 'pass' silently swallows errors", "warning", "Add error handling, re-raise the exception, or add a comment explaining why the error is intentionally ignored"));
13206
13469
  continue;
13207
13470
  }
13208
13471
  if (blockChildren.length === 1 && blockChildren[0].kind() === "expression_statement") {
13209
13472
  const stmtText = blockChildren[0].text().trim();
13210
13473
  if (isPythonLogOnlyCall(stmtText)) {
13211
- findings.push({
13212
- detectorId: "empty-error-handler",
13213
- message: "Except block only logs the error without handling it",
13214
- severity: "warning",
13215
- file: ctx.file.path,
13216
- line: range.start.line + 1,
13217
- column: range.start.column + 1,
13218
- endLine: range.end.line + 1,
13219
- endColumn: range.end.column + 1,
13220
- suggestion: "Add proper error handling: re-raise, return a fallback value, or propagate the error"
13221
- });
13474
+ findings.push(makeFinding("empty-error-handler", ctx, exceptNode, "Except block only logs the error without handling it", "warning", "Add proper error handling: re-raise, return a fallback value, or propagate the error"));
13222
13475
  }
13223
13476
  }
13224
13477
  }
@@ -13297,33 +13550,12 @@ function detectJavaScriptTrivialAssertions(ctx) {
13297
13550
  if (!LITERAL_KINDS_JS.has(expectArg.kind()))
13298
13551
  continue;
13299
13552
  const methodName = property.text();
13300
- const range = call.range();
13301
13553
  if (methodName === "toBeTruthy" && expectArg.text() === "true") {
13302
- findings.push({
13303
- detectorId: "trivial-assertion",
13304
- message: "Trivial assertion: expect(true).toBeTruthy() always passes",
13305
- severity: "warning",
13306
- file: ctx.file.path,
13307
- line: range.start.line + 1,
13308
- column: range.start.column + 1,
13309
- endLine: range.end.line + 1,
13310
- endColumn: range.end.column + 1,
13311
- suggestion: "Replace with a meaningful assertion that tests actual behavior"
13312
- });
13554
+ findings.push(makeFinding("trivial-assertion", ctx, call, "Trivial assertion: expect(true).toBeTruthy() always passes", "warning", "Replace with a meaningful assertion that tests actual behavior"));
13313
13555
  continue;
13314
13556
  }
13315
13557
  if (methodName === "toBeFalsy" && expectArg.text() === "false") {
13316
- findings.push({
13317
- detectorId: "trivial-assertion",
13318
- message: "Trivial assertion: expect(false).toBeFalsy() always passes",
13319
- severity: "warning",
13320
- file: ctx.file.path,
13321
- line: range.start.line + 1,
13322
- column: range.start.column + 1,
13323
- endLine: range.end.line + 1,
13324
- endColumn: range.end.column + 1,
13325
- suggestion: "Replace with a meaningful assertion that tests actual behavior"
13326
- });
13558
+ findings.push(makeFinding("trivial-assertion", ctx, call, "Trivial assertion: expect(false).toBeFalsy() always passes", "warning", "Replace with a meaningful assertion that tests actual behavior"));
13327
13559
  continue;
13328
13560
  }
13329
13561
  if (methodName !== "toBe" && methodName !== "toEqual")
@@ -13341,17 +13573,7 @@ function detectJavaScriptTrivialAssertions(ctx) {
13341
13573
  areSame = expectArg.text() === matcherArg.text();
13342
13574
  }
13343
13575
  if (areSame) {
13344
- findings.push({
13345
- detectorId: "trivial-assertion",
13346
- message: `Trivial assertion: expect(${expectArg.text()}).${methodName}(${matcherArg.text()}) always passes`,
13347
- severity: "warning",
13348
- file: ctx.file.path,
13349
- line: range.start.line + 1,
13350
- column: range.start.column + 1,
13351
- endLine: range.end.line + 1,
13352
- endColumn: range.end.column + 1,
13353
- suggestion: "Replace with a meaningful assertion that tests actual behavior"
13354
- });
13576
+ findings.push(makeFinding("trivial-assertion", ctx, call, `Trivial assertion: expect(${expectArg.text()}).${methodName}(${matcherArg.text()}) always passes`, "warning", "Replace with a meaningful assertion that tests actual behavior"));
13355
13577
  }
13356
13578
  }
13357
13579
  }
@@ -13366,19 +13588,8 @@ function detectPythonTrivialAssertions(ctx) {
13366
13588
  if (children.length < 2)
13367
13589
  continue;
13368
13590
  const expr = children[1];
13369
- const range = assertNode.range();
13370
13591
  if (expr.kind() === "true" || expr.kind() === "false") {
13371
- findings.push({
13372
- detectorId: "trivial-assertion",
13373
- message: `Trivial assertion: assert ${expr.text()} always ${expr.kind() === "true" ? "passes" : "fails"}`,
13374
- severity: "warning",
13375
- file: ctx.file.path,
13376
- line: range.start.line + 1,
13377
- column: range.start.column + 1,
13378
- endLine: range.end.line + 1,
13379
- endColumn: range.end.column + 1,
13380
- suggestion: "Replace with a meaningful assertion that tests actual behavior"
13381
- });
13592
+ findings.push(makeFinding("trivial-assertion", ctx, assertNode, `Trivial assertion: assert ${expr.text()} always ${expr.kind() === "true" ? "passes" : "fails"}`, "warning", "Replace with a meaningful assertion that tests actual behavior"));
13382
13593
  continue;
13383
13594
  }
13384
13595
  if (expr.kind() === "comparison_operator") {
@@ -13394,17 +13605,7 @@ function detectPythonTrivialAssertions(ctx) {
13394
13605
  areSame = left.text() === right.text();
13395
13606
  }
13396
13607
  if (areSame) {
13397
- findings.push({
13398
- detectorId: "trivial-assertion",
13399
- message: `Trivial assertion: assert ${left.text()} == ${right.text()} always passes`,
13400
- severity: "warning",
13401
- file: ctx.file.path,
13402
- line: range.start.line + 1,
13403
- column: range.start.column + 1,
13404
- endLine: range.end.line + 1,
13405
- endColumn: range.end.column + 1,
13406
- suggestion: "Replace with a meaningful assertion that tests actual behavior"
13407
- });
13608
+ findings.push(makeFinding("trivial-assertion", ctx, assertNode, `Trivial assertion: assert ${left.text()} == ${right.text()} always passes`, "warning", "Replace with a meaningful assertion that tests actual behavior"));
13408
13609
  }
13409
13610
  }
13410
13611
  }
@@ -13465,18 +13666,7 @@ function detectRejectUnauthorized(root, ctx, findings) {
13465
13666
  const key = children.find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string");
13466
13667
  const value = children.find((ch) => ch.kind() === "false");
13467
13668
  if (key && value && key.text().replace(/["']/g, "") === "rejectUnauthorized") {
13468
- const range = pair.range();
13469
- findings.push({
13470
- detectorId: "insecure-defaults",
13471
- message: "TLS certificate verification is disabled (rejectUnauthorized: false)",
13472
- severity: "error",
13473
- file: ctx.file.path,
13474
- line: range.start.line + 1,
13475
- column: range.start.column + 1,
13476
- endLine: range.end.line + 1,
13477
- endColumn: range.end.column + 1,
13478
- suggestion: "Remove rejectUnauthorized: false to enable TLS certificate verification"
13479
- });
13669
+ findings.push(makeFinding("insecure-defaults", ctx, pair, "TLS certificate verification is disabled (rejectUnauthorized: false)", "error", "Remove rejectUnauthorized: false to enable TLS certificate verification"));
13480
13670
  }
13481
13671
  }
13482
13672
  }
@@ -13486,18 +13676,7 @@ function detectEvalUsage(root, ctx, findings) {
13486
13676
  const children = call.children();
13487
13677
  const fn = children[0];
13488
13678
  if (fn && fn.kind() === "identifier" && fn.text() === "eval") {
13489
- const range = call.range();
13490
- findings.push({
13491
- detectorId: "insecure-defaults",
13492
- message: "eval() executes arbitrary code and is a security risk",
13493
- severity: "error",
13494
- file: ctx.file.path,
13495
- line: range.start.line + 1,
13496
- column: range.start.column + 1,
13497
- endLine: range.end.line + 1,
13498
- endColumn: range.end.column + 1,
13499
- suggestion: "Avoid eval(). Use JSON.parse() for data, or refactor to avoid dynamic code execution"
13500
- });
13679
+ findings.push(makeFinding("insecure-defaults", ctx, call, "eval() executes arbitrary code and is a security risk", "error", "Avoid eval(). Use JSON.parse() for data, or refactor to avoid dynamic code execution"));
13501
13680
  }
13502
13681
  }
13503
13682
  }
@@ -13507,18 +13686,7 @@ function detectNewFunction(root, ctx, findings) {
13507
13686
  const children = newExpr.children();
13508
13687
  const constructorNode = children.find((ch) => ch.kind() === "identifier");
13509
13688
  if (constructorNode && constructorNode.text() === "Function") {
13510
- const range = newExpr.range();
13511
- findings.push({
13512
- detectorId: "insecure-defaults",
13513
- message: "new Function() creates functions from strings and is a security risk",
13514
- severity: "error",
13515
- file: ctx.file.path,
13516
- line: range.start.line + 1,
13517
- column: range.start.column + 1,
13518
- endLine: range.end.line + 1,
13519
- endColumn: range.end.column + 1,
13520
- suggestion: "Avoid new Function(). Refactor to use static function definitions"
13521
- });
13689
+ findings.push(makeFinding("insecure-defaults", ctx, newExpr, "new Function() creates functions from strings and is a security risk", "error", "Avoid new Function(). Refactor to use static function definitions"));
13522
13690
  }
13523
13691
  }
13524
13692
  }
@@ -13546,18 +13714,7 @@ function detectHardcodedCredentialsJS(root, ctx, findings) {
13546
13714
  continue;
13547
13715
  if (looksLikeNonCredential(strContent))
13548
13716
  continue;
13549
- const range = pair.range();
13550
- findings.push({
13551
- detectorId: "insecure-defaults",
13552
- message: `Hardcoded credential detected in property '${keyName}'`,
13553
- severity: "error",
13554
- file: ctx.file.path,
13555
- line: range.start.line + 1,
13556
- column: range.start.column + 1,
13557
- endLine: range.end.line + 1,
13558
- endColumn: range.end.column + 1,
13559
- suggestion: "Use environment variables or a secrets manager instead of hardcoding credentials"
13560
- });
13717
+ findings.push(makeFinding("insecure-defaults", ctx, pair, `Hardcoded credential detected in property '${keyName}'`, "error", "Use environment variables or a secrets manager instead of hardcoding credentials"));
13561
13718
  }
13562
13719
  }
13563
13720
  function checkCredentialAssignment(node, ctx, findings) {
@@ -13574,18 +13731,7 @@ function checkCredentialAssignment(node, ctx, findings) {
13574
13731
  return;
13575
13732
  if (looksLikeNonCredential(strContent))
13576
13733
  return;
13577
- const range = node.range();
13578
- findings.push({
13579
- detectorId: "insecure-defaults",
13580
- message: `Hardcoded credential detected in variable '${varName}'`,
13581
- severity: "error",
13582
- file: ctx.file.path,
13583
- line: range.start.line + 1,
13584
- column: range.start.column + 1,
13585
- endLine: range.end.line + 1,
13586
- endColumn: range.end.column + 1,
13587
- suggestion: "Use environment variables or a secrets manager instead of hardcoding credentials"
13588
- });
13734
+ findings.push(makeFinding("insecure-defaults", ctx, node, `Hardcoded credential detected in variable '${varName}'`, "error", "Use environment variables or a secrets manager instead of hardcoding credentials"));
13589
13735
  }
13590
13736
  function detectWeakCiphers(root, ctx, findings) {
13591
13737
  const callExprs = root.findAll({ rule: { kind: "call_expression" } });
@@ -13607,18 +13753,7 @@ function detectWeakCiphers(root, ctx, findings) {
13607
13753
  if (firstArg.kind() === "string") {
13608
13754
  const cipher = firstArg.text().slice(1, -1).toLowerCase();
13609
13755
  if (WEAK_CIPHERS.has(cipher)) {
13610
- const range = call.range();
13611
- findings.push({
13612
- detectorId: "insecure-defaults",
13613
- message: `Weak cipher algorithm '${cipher}' detected`,
13614
- severity: "error",
13615
- file: ctx.file.path,
13616
- line: range.start.line + 1,
13617
- column: range.start.column + 1,
13618
- endLine: range.end.line + 1,
13619
- endColumn: range.end.column + 1,
13620
- suggestion: "Use a strong cipher algorithm like 'aes-256-gcm' instead"
13621
- });
13756
+ findings.push(makeFinding("insecure-defaults", ctx, call, `Weak cipher algorithm '${cipher}' detected`, "error", "Use a strong cipher algorithm like 'aes-256-gcm' instead"));
13622
13757
  }
13623
13758
  }
13624
13759
  }
@@ -13641,18 +13776,7 @@ function detectPythonVerifyFalse(root, ctx, findings) {
13641
13776
  const key = children.find((ch) => ch.kind() === "identifier");
13642
13777
  const value = children.find((ch) => ch.kind() === "false");
13643
13778
  if (key && value && key.text() === "verify") {
13644
- const range = kwarg.range();
13645
- findings.push({
13646
- detectorId: "insecure-defaults",
13647
- message: "TLS certificate verification is disabled (verify=False)",
13648
- severity: "error",
13649
- file: ctx.file.path,
13650
- line: range.start.line + 1,
13651
- column: range.start.column + 1,
13652
- endLine: range.end.line + 1,
13653
- endColumn: range.end.column + 1,
13654
- suggestion: "Remove verify=False to enable TLS certificate verification"
13655
- });
13779
+ findings.push(makeFinding("insecure-defaults", ctx, kwarg, "TLS certificate verification is disabled (verify=False)", "error", "Remove verify=False to enable TLS certificate verification"));
13656
13780
  }
13657
13781
  }
13658
13782
  }
@@ -13663,18 +13787,7 @@ function detectPythonShellTrue(root, ctx, findings) {
13663
13787
  const key = children.find((ch) => ch.kind() === "identifier");
13664
13788
  const value = children.find((ch) => ch.kind() === "true");
13665
13789
  if (key && value && key.text() === "shell") {
13666
- const range = kwarg.range();
13667
- findings.push({
13668
- detectorId: "insecure-defaults",
13669
- message: "shell=True in subprocess call allows shell injection attacks",
13670
- severity: "error",
13671
- file: ctx.file.path,
13672
- line: range.start.line + 1,
13673
- column: range.start.column + 1,
13674
- endLine: range.end.line + 1,
13675
- endColumn: range.end.column + 1,
13676
- suggestion: "Use shell=False (the default) and pass arguments as a list"
13677
- });
13790
+ findings.push(makeFinding("insecure-defaults", ctx, kwarg, "shell=True in subprocess call allows shell injection attacks", "error", "Use shell=False (the default) and pass arguments as a list"));
13678
13791
  }
13679
13792
  }
13680
13793
  }
@@ -13684,18 +13797,7 @@ function detectPythonEval(root, ctx, findings) {
13684
13797
  const children = call.children();
13685
13798
  const fn = children[0];
13686
13799
  if (fn && fn.kind() === "identifier" && fn.text() === "eval") {
13687
- const range = call.range();
13688
- findings.push({
13689
- detectorId: "insecure-defaults",
13690
- message: "eval() executes arbitrary code and is a security risk",
13691
- severity: "error",
13692
- file: ctx.file.path,
13693
- line: range.start.line + 1,
13694
- column: range.start.column + 1,
13695
- endLine: range.end.line + 1,
13696
- endColumn: range.end.column + 1,
13697
- suggestion: "Avoid eval(). Use ast.literal_eval() for safe expression evaluation"
13698
- });
13800
+ findings.push(makeFinding("insecure-defaults", ctx, call, "eval() executes arbitrary code and is a security risk", "error", "Avoid eval(). Use ast.literal_eval() for safe expression evaluation"));
13699
13801
  }
13700
13802
  }
13701
13803
  }
@@ -13721,18 +13823,7 @@ function detectPythonHardcodedCredentials(root, ctx, findings) {
13721
13823
  }
13722
13824
  if (strContent.length === 0)
13723
13825
  continue;
13724
- const range = assign.range();
13725
- findings.push({
13726
- detectorId: "insecure-defaults",
13727
- message: `Hardcoded credential detected in variable '${varName}'`,
13728
- severity: "error",
13729
- file: ctx.file.path,
13730
- line: range.start.line + 1,
13731
- column: range.start.column + 1,
13732
- endLine: range.end.line + 1,
13733
- endColumn: range.end.column + 1,
13734
- suggestion: "Use environment variables or a secrets manager instead of hardcoding credentials"
13735
- });
13826
+ findings.push(makeFinding("insecure-defaults", ctx, assign, `Hardcoded credential detected in variable '${varName}'`, "error", "Use environment variables or a secrets manager instead of hardcoding credentials"));
13736
13827
  }
13737
13828
  }
13738
13829
  var insecureDefaults = {
@@ -14248,18 +14339,7 @@ function detectJavaScriptUndeclaredImports(ctx) {
14248
14339
  const nearestDeps = findNearestJsDependencies(ctx.file.absolutePath, scanRoot);
14249
14340
  if (nearestDeps.has(packageName))
14250
14341
  continue;
14251
- const range = importNode.range();
14252
- findings.push({
14253
- detectorId: "undeclared-import",
14254
- message: `Import '${packageName}' is not declared in project dependencies`,
14255
- severity: "error",
14256
- file: ctx.file.path,
14257
- line: range.start.line + 1,
14258
- column: range.start.column + 1,
14259
- endLine: range.end.line + 1,
14260
- endColumn: range.end.column + 1,
14261
- suggestion: `Add '${packageName}' to your package.json dependencies`
14262
- });
14342
+ findings.push(makeFinding("undeclared-import", ctx, importNode, `Import '${packageName}' is not declared in project dependencies`, "error", `Add '${packageName}' to your package.json dependencies`));
14263
14343
  }
14264
14344
  const callExprs = root.findAll({ rule: { kind: "call_expression" } });
14265
14345
  for (const call of callExprs) {
@@ -14295,18 +14375,7 @@ function detectJavaScriptUndeclaredImports(ctx) {
14295
14375
  const nearestDeps = findNearestJsDependencies(ctx.file.absolutePath, scanRoot);
14296
14376
  if (nearestDeps.has(packageName))
14297
14377
  continue;
14298
- const range = call.range();
14299
- findings.push({
14300
- detectorId: "undeclared-import",
14301
- message: `Import '${packageName}' is not declared in project dependencies`,
14302
- severity: "error",
14303
- file: ctx.file.path,
14304
- line: range.start.line + 1,
14305
- column: range.start.column + 1,
14306
- endLine: range.end.line + 1,
14307
- endColumn: range.end.column + 1,
14308
- suggestion: `Add '${packageName}' to your package.json dependencies`
14309
- });
14378
+ findings.push(makeFinding("undeclared-import", ctx, call, `Import '${packageName}' is not declared in project dependencies`, "error", `Add '${packageName}' to your package.json dependencies`));
14310
14379
  }
14311
14380
  return findings;
14312
14381
  }
@@ -14330,18 +14399,7 @@ function detectPythonUndeclaredImports(ctx) {
14330
14399
  const topLevel = fullName.split(".")[0];
14331
14400
  if (isPythonImportDeclared(topLevel, ctx, scanRoot))
14332
14401
  continue;
14333
- const range = importNode.range();
14334
- findings.push({
14335
- detectorId: "undeclared-import",
14336
- message: `Import '${topLevel}' is not declared in project dependencies`,
14337
- severity: "error",
14338
- file: ctx.file.path,
14339
- line: range.start.line + 1,
14340
- column: range.start.column + 1,
14341
- endLine: range.end.line + 1,
14342
- endColumn: range.end.column + 1,
14343
- suggestion: `Add '${topLevel}' to your requirements.txt or pyproject.toml`
14344
- });
14402
+ findings.push(makeFinding("undeclared-import", ctx, importNode, `Import '${topLevel}' is not declared in project dependencies`, "error", `Add '${topLevel}' to your requirements.txt or pyproject.toml`));
14345
14403
  }
14346
14404
  const fromImports = root.findAll({ rule: { kind: "import_from_statement" } });
14347
14405
  for (const importNode of fromImports) {
@@ -14356,18 +14414,7 @@ function detectPythonUndeclaredImports(ctx) {
14356
14414
  const topLevel = fullName.split(".")[0];
14357
14415
  if (isPythonImportDeclared(topLevel, ctx, scanRoot))
14358
14416
  continue;
14359
- const range = importNode.range();
14360
- findings.push({
14361
- detectorId: "undeclared-import",
14362
- message: `Import '${topLevel}' is not declared in project dependencies`,
14363
- severity: "error",
14364
- file: ctx.file.path,
14365
- line: range.start.line + 1,
14366
- column: range.start.column + 1,
14367
- endLine: range.end.line + 1,
14368
- endColumn: range.end.column + 1,
14369
- suggestion: `Add '${topLevel}' to your requirements.txt or pyproject.toml`
14370
- });
14417
+ findings.push(makeFinding("undeclared-import", ctx, importNode, `Import '${topLevel}' is not declared in project dependencies`, "error", `Add '${topLevel}' to your requirements.txt or pyproject.toml`));
14371
14418
  }
14372
14419
  return findings;
14373
14420
  }
@@ -14460,33 +14507,11 @@ function detectRedundantNullChecks(ctx) {
14460
14507
  if (leftCheck.variable !== rightCheck.variable)
14461
14508
  continue;
14462
14509
  if (!leftCheck.isLoose && !rightCheck.isLoose && (leftCheck.checksNull && rightCheck.checksUndefined || leftCheck.checksUndefined && rightCheck.checksNull)) {
14463
- const range = expr.range();
14464
- findings.push({
14465
- detectorId: "over-defensive-coding",
14466
- message: `Redundant null+undefined check on '${leftCheck.variable}'. Use '${leftCheck.variable} != null' to check both.`,
14467
- severity: "info",
14468
- file: ctx.file.path,
14469
- line: range.start.line + 1,
14470
- column: range.start.column + 1,
14471
- endLine: range.end.line + 1,
14472
- endColumn: range.end.column + 1,
14473
- suggestion: `Replace with '${leftCheck.variable} != null' which checks both null and undefined`
14474
- });
14510
+ findings.push(makeFinding("over-defensive-coding", ctx, expr, `Redundant null+undefined check on '${leftCheck.variable}'. Use '${leftCheck.variable} != null' to check both.`, "info", `Replace with '${leftCheck.variable} != null' which checks both null and undefined`));
14475
14511
  continue;
14476
14512
  }
14477
14513
  if (leftCheck.isLoose && rightCheck.isLoose && (leftCheck.checksNull && rightCheck.checksUndefined || leftCheck.checksUndefined && rightCheck.checksNull)) {
14478
- const range = expr.range();
14479
- findings.push({
14480
- detectorId: "over-defensive-coding",
14481
- message: `Redundant check: '${leftCheck.variable} != null' already checks both null and undefined`,
14482
- severity: "info",
14483
- file: ctx.file.path,
14484
- line: range.start.line + 1,
14485
- column: range.start.column + 1,
14486
- endLine: range.end.line + 1,
14487
- endColumn: range.end.column + 1,
14488
- suggestion: `Use just '${leftCheck.variable} != null' — it checks both null and undefined`
14489
- });
14514
+ findings.push(makeFinding("over-defensive-coding", ctx, expr, `Redundant check: '${leftCheck.variable} != null' already checks both null and undefined`, "info", `Use just '${leftCheck.variable} != null' — it checks both null and undefined`));
14490
14515
  }
14491
14516
  }
14492
14517
  return findings;
@@ -14519,18 +14544,7 @@ function detectJsonParseLiteralTryCatch(ctx) {
14519
14544
  const arg = argNodes[0];
14520
14545
  if (arg.kind() !== "string")
14521
14546
  continue;
14522
- const range = tryNode.range();
14523
- findings.push({
14524
- detectorId: "over-defensive-coding",
14525
- message: "Unnecessary try/catch around JSON.parse() with a string literal argument that cannot fail",
14526
- severity: "info",
14527
- file: ctx.file.path,
14528
- line: range.start.line + 1,
14529
- column: range.start.column + 1,
14530
- endLine: range.end.line + 1,
14531
- endColumn: range.end.column + 1,
14532
- suggestion: "Remove the try/catch — JSON.parse with a valid string literal will never throw"
14533
- });
14547
+ findings.push(makeFinding("over-defensive-coding", ctx, tryNode, "Unnecessary try/catch around JSON.parse() with a string literal argument that cannot fail", "info", "Remove the try/catch — JSON.parse with a valid string literal will never throw"));
14534
14548
  }
14535
14549
  }
14536
14550
  }
@@ -14627,15 +14641,7 @@ var excessiveCommentRatio = {
14627
14641
  if (ratio > threshold) {
14628
14642
  const pct = Math.round(ratio * 100);
14629
14643
  return [
14630
- {
14631
- detectorId: "excessive-comment-ratio",
14632
- message: `File has ${pct}% comment lines (${counts.commentLines} comments, ${counts.codeLines} code lines). Threshold: ${Math.round(threshold * 100)}%`,
14633
- severity: "info",
14634
- file: ctx.file.path,
14635
- line: 1,
14636
- column: 1,
14637
- suggestion: "Reduce excessive comments. Good code should be self-documenting with comments reserved for explaining 'why', not 'what'."
14638
- }
14644
+ makeLineFinding("excessive-comment-ratio", ctx, 1, 1, `File has ${pct}% comment lines (${counts.commentLines} comments, ${counts.codeLines} code lines). Threshold: ${Math.round(threshold * 100)}%`, "info", "Reduce excessive comments. Good code should be self-documenting with comments reserved for explaining 'why', not 'what'.")
14639
14645
  ];
14640
14646
  }
14641
14647
  return [];
@@ -14706,15 +14712,7 @@ function detectJsMocking(ctx) {
14706
14712
  const ratio = ctx.config.ratio ?? DEFAULT_RATIO;
14707
14713
  if (mockCount > 0 && mockCount > assertionCount * ratio) {
14708
14714
  return [
14709
- {
14710
- detectorId: "over-mocking",
14711
- message: `Test file has more mocks (${mockCount}) than assertions (${assertionCount}). Tests that over-mock may not verify real behavior.`,
14712
- severity: "warning",
14713
- file: ctx.file.path,
14714
- line: 1,
14715
- column: 1,
14716
- suggestion: "Reduce mocking and add more assertions. Consider integration tests for heavily-mocked code."
14717
- }
14715
+ makeLineFinding("over-mocking", ctx, 1, 1, `Test file has more mocks (${mockCount}) than assertions (${assertionCount}). Tests that over-mock may not verify real behavior.`, "warning", "Reduce mocking and add more assertions. Consider integration tests for heavily-mocked code.")
14718
14716
  ];
14719
14717
  }
14720
14718
  return [];
@@ -14725,15 +14723,7 @@ function detectPythonMocking(ctx) {
14725
14723
  const ratio = ctx.config.ratio ?? DEFAULT_RATIO;
14726
14724
  if (mockCount > 0 && mockCount > assertionCount * ratio) {
14727
14725
  return [
14728
- {
14729
- detectorId: "over-mocking",
14730
- message: `Test file has more mocks (${mockCount}) than assertions (${assertionCount}). Tests that over-mock may not verify real behavior.`,
14731
- severity: "warning",
14732
- file: ctx.file.path,
14733
- line: 1,
14734
- column: 1,
14735
- suggestion: "Reduce mocking and add more assertions. Consider integration tests for heavily-mocked code."
14736
- }
14726
+ makeLineFinding("over-mocking", ctx, 1, 1, `Test file has more mocks (${mockCount}) than assertions (${assertionCount}). Tests that over-mock may not verify real behavior.`, "warning", "Reduce mocking and add more assertions. Consider integration tests for heavily-mocked code.")
14737
14727
  ];
14738
14728
  }
14739
14729
  return [];
@@ -14866,18 +14856,7 @@ function detectJavaScript(ctx) {
14866
14856
  }
14867
14857
  if (!isDbCall)
14868
14858
  continue;
14869
- const range = awaitExpr.range();
14870
- findings.push({
14871
- detectorId: "n-plus-one-query",
14872
- message: "Database or API call inside a loop — potential N+1 query",
14873
- severity: "warning",
14874
- file: ctx.file.path,
14875
- line: range.start.line + 1,
14876
- column: range.start.column + 1,
14877
- endLine: range.end.line + 1,
14878
- endColumn: range.end.column + 1,
14879
- suggestion: "Batch the operation outside the loop (e.g., use WHERE IN, Promise.all, or bulk API endpoint)"
14880
- });
14859
+ findings.push(makeFinding("n-plus-one-query", ctx, awaitExpr, "Database or API call inside a loop — potential N+1 query", "warning", "Batch the operation outside the loop (e.g., use WHERE IN, Promise.all, or bulk API endpoint)"));
14881
14860
  }
14882
14861
  const arrowFns = root.findAll({ rule: { kind: "arrow_function" } });
14883
14862
  for (const arrow of arrowFns) {
@@ -14917,18 +14896,7 @@ function detectJavaScript(ctx) {
14917
14896
  }
14918
14897
  if (!isDbCall)
14919
14898
  continue;
14920
- const range = innerAwait.range();
14921
- findings.push({
14922
- detectorId: "n-plus-one-query",
14923
- message: "Database or API call inside .map(async ...) — potential N+1 query",
14924
- severity: "warning",
14925
- file: ctx.file.path,
14926
- line: range.start.line + 1,
14927
- column: range.start.column + 1,
14928
- endLine: range.end.line + 1,
14929
- endColumn: range.end.column + 1,
14930
- suggestion: "Batch the operation (e.g., use WHERE IN or a single bulk query) instead of per-item async calls"
14931
- });
14899
+ findings.push(makeFinding("n-plus-one-query", ctx, innerAwait, "Database or API call inside .map(async ...) — potential N+1 query", "warning", "Batch the operation (e.g., use WHERE IN or a single bulk query) instead of per-item async calls"));
14932
14900
  }
14933
14901
  }
14934
14902
  const callExprs = root.findAll({ rule: { kind: "call_expression" } });
@@ -14963,18 +14931,7 @@ function detectJavaScript(ctx) {
14963
14931
  }
14964
14932
  if (!isDbCall)
14965
14933
  continue;
14966
- const range = call.range();
14967
- findings.push({
14968
- detectorId: "n-plus-one-query",
14969
- message: "Database or API call inside a loop — potential N+1 query",
14970
- severity: "warning",
14971
- file: ctx.file.path,
14972
- line: range.start.line + 1,
14973
- column: range.start.column + 1,
14974
- endLine: range.end.line + 1,
14975
- endColumn: range.end.column + 1,
14976
- suggestion: "Batch the operation outside the loop (e.g., use WHERE IN, Promise.all, or bulk API endpoint)"
14977
- });
14934
+ findings.push(makeFinding("n-plus-one-query", ctx, call, "Database or API call inside a loop — potential N+1 query", "warning", "Batch the operation outside the loop (e.g., use WHERE IN, Promise.all, or bulk API endpoint)"));
14978
14935
  }
14979
14936
  return findings;
14980
14937
  }
@@ -14999,17 +14956,7 @@ function detectPython(ctx) {
14999
14956
  continue;
15000
14957
  const range = awaitExpr.range();
15001
14958
  reported.add(range.start.line);
15002
- findings.push({
15003
- detectorId: "n-plus-one-query",
15004
- message: "Database call inside a loop — potential N+1 query",
15005
- severity: "warning",
15006
- file: ctx.file.path,
15007
- line: range.start.line + 1,
15008
- column: range.start.column + 1,
15009
- endLine: range.end.line + 1,
15010
- endColumn: range.end.column + 1,
15011
- suggestion: "Batch the operation outside the loop (e.g., use WHERE IN or bulk query)"
15012
- });
14959
+ findings.push(makeFinding("n-plus-one-query", ctx, awaitExpr, "Database call inside a loop — potential N+1 query", "warning", "Batch the operation outside the loop (e.g., use WHERE IN or bulk query)"));
15013
14960
  }
15014
14961
  const callExprs = root.findAll({ rule: { kind: "call" } });
15015
14962
  for (const call of callExprs) {
@@ -15022,17 +14969,7 @@ function detectPython(ctx) {
15022
14969
  const callText = call.text();
15023
14970
  if (!isDbCallPython(callText))
15024
14971
  continue;
15025
- findings.push({
15026
- detectorId: "n-plus-one-query",
15027
- message: "Database call inside a loop — potential N+1 query",
15028
- severity: "warning",
15029
- file: ctx.file.path,
15030
- line: range.start.line + 1,
15031
- column: range.start.column + 1,
15032
- endLine: range.end.line + 1,
15033
- endColumn: range.end.column + 1,
15034
- suggestion: "Batch the operation outside the loop (e.g., use WHERE IN or bulk query)"
15035
- });
14972
+ findings.push(makeFinding("n-plus-one-query", ctx, call, "Database call inside a loop — potential N+1 query", "warning", "Batch the operation outside the loop (e.g., use WHERE IN or bulk query)"));
15036
14973
  }
15037
14974
  return findings;
15038
14975
  }
@@ -15130,18 +15067,7 @@ function detectJavaScript2(ctx) {
15130
15067
  continue;
15131
15068
  if (!looksLikeDbCall(callText))
15132
15069
  continue;
15133
- const range = stmt.range();
15134
- findings.push({
15135
- detectorId: "unchecked-db-result",
15136
- message: "Database mutation result is not checked — errors will be silently ignored",
15137
- severity: "warning",
15138
- file: ctx.file.path,
15139
- line: range.start.line + 1,
15140
- column: range.start.column + 1,
15141
- endLine: range.end.line + 1,
15142
- endColumn: range.end.column + 1,
15143
- suggestion: "Store the result and check for errors: const result = await db.insert(...)"
15144
- });
15070
+ findings.push(makeFinding("unchecked-db-result", ctx, stmt, "Database mutation result is not checked — errors will be silently ignored", "warning", "Store the result and check for errors: const result = await db.insert(...)"));
15145
15071
  }
15146
15072
  return findings;
15147
15073
  }
@@ -15189,18 +15115,7 @@ function detectPython2(ctx) {
15189
15115
  }
15190
15116
  if (!isMutation)
15191
15117
  continue;
15192
- const range = stmt.range();
15193
- findings.push({
15194
- detectorId: "unchecked-db-result",
15195
- message: "Database mutation result is not checked — errors may be silently ignored",
15196
- severity: "warning",
15197
- file: ctx.file.path,
15198
- line: range.start.line + 1,
15199
- column: range.start.column + 1,
15200
- endLine: range.end.line + 1,
15201
- endColumn: range.end.column + 1,
15202
- suggestion: "Store the result and verify the operation succeeded"
15203
- });
15118
+ findings.push(makeFinding("unchecked-db-result", ctx, stmt, "Database mutation result is not checked — errors may be silently ignored", "warning", "Store the result and verify the operation succeeded"));
15204
15119
  }
15205
15120
  return findings;
15206
15121
  }
@@ -15236,18 +15151,7 @@ function detectJavaScript3(ctx) {
15236
15151
  const ifText = consequent.text().replace(/\s+/g, " ").trim();
15237
15152
  const elseText = elseBody.text().replace(/\s+/g, " ").trim();
15238
15153
  if (ifText === elseText && ifText.length > 4) {
15239
- const range = ifStmt.range();
15240
- findings.push({
15241
- detectorId: "dead-code-path",
15242
- message: "if and else branches are identical — condition has no effect",
15243
- severity: "warning",
15244
- file: ctx.file.path,
15245
- line: range.start.line + 1,
15246
- column: range.start.column + 1,
15247
- endLine: range.end.line + 1,
15248
- endColumn: range.end.column + 1,
15249
- suggestion: "Remove the conditional and keep only the body, or fix the branch logic"
15250
- });
15154
+ findings.push(makeFinding("dead-code-path", ctx, ifStmt, "if and else branches are identical — condition has no effect", "warning", "Remove the conditional and keep only the body, or fix the branch logic"));
15251
15155
  }
15252
15156
  }
15253
15157
  const blocks = root.findAll({ rule: { kind: "statement_block" } });
@@ -15256,18 +15160,7 @@ function detectJavaScript3(ctx) {
15256
15160
  let foundTerminator = false;
15257
15161
  for (const stmt of stmts) {
15258
15162
  if (foundTerminator) {
15259
- const range = stmt.range();
15260
- findings.push({
15261
- detectorId: "dead-code-path",
15262
- message: "Unreachable code after return/throw statement",
15263
- severity: "warning",
15264
- file: ctx.file.path,
15265
- line: range.start.line + 1,
15266
- column: range.start.column + 1,
15267
- endLine: range.end.line + 1,
15268
- endColumn: range.end.column + 1,
15269
- suggestion: "Remove unreachable code or fix the control flow"
15270
- });
15163
+ findings.push(makeFinding("dead-code-path", ctx, stmt, "Unreachable code after return/throw statement", "warning", "Remove unreachable code or fix the control flow"));
15271
15164
  break;
15272
15165
  }
15273
15166
  if (stmt.kind() === "return_statement" || stmt.kind() === "throw_statement") {
@@ -15291,18 +15184,7 @@ function detectPython3(ctx) {
15291
15184
  const ifText = blocks[0].text().replace(/\s+/g, " ").trim();
15292
15185
  const elseText = elseBlock.text().replace(/\s+/g, " ").trim();
15293
15186
  if (ifText === elseText && ifText.length > 4) {
15294
- const range = ifStmt.range();
15295
- findings.push({
15296
- detectorId: "dead-code-path",
15297
- message: "if and else branches are identical — condition has no effect",
15298
- severity: "warning",
15299
- file: ctx.file.path,
15300
- line: range.start.line + 1,
15301
- column: range.start.column + 1,
15302
- endLine: range.end.line + 1,
15303
- endColumn: range.end.column + 1,
15304
- suggestion: "Remove the conditional and keep only the body, or fix the branch logic"
15305
- });
15187
+ findings.push(makeFinding("dead-code-path", ctx, ifStmt, "if and else branches are identical — condition has no effect", "warning", "Remove the conditional and keep only the body, or fix the branch logic"));
15306
15188
  }
15307
15189
  }
15308
15190
  }
@@ -15344,15 +15226,7 @@ function detect(ctx) {
15344
15226
  const match = line.match(doubleAssertRe) || line.match(doubleAssertRe2);
15345
15227
  if (!match)
15346
15228
  continue;
15347
- findings.push({
15348
- detectorId: "double-type-assertion",
15349
- message: "Double type assertion (as unknown as X) bypasses TypeScript's type safety",
15350
- severity: "warning",
15351
- file: ctx.file.path,
15352
- line: i + 1,
15353
- column: (match.index ?? 0) + 1,
15354
- suggestion: "Fix the underlying type mismatch instead of using double assertion. Add a proper type guard or fix the type definition."
15355
- });
15229
+ findings.push(makeLineFinding("double-type-assertion", ctx, i + 1, (match.index ?? 0) + 1, "Double type assertion (as unknown as X) bypasses TypeScript's type safety", "warning", "Fix the underlying type mismatch instead of using double assertion. Add a proper type guard or fix the type definition."));
15356
15230
  }
15357
15231
  return findings;
15358
15232
  }
@@ -15417,17 +15291,7 @@ function detect2(ctx) {
15417
15291
  if (anyLocations.length <= threshold)
15418
15292
  return findings;
15419
15293
  for (const loc of anyLocations) {
15420
- findings.push({
15421
- detectorId: "excessive-any",
15422
- message: `Excessive use of 'any' type (${anyLocations.length} in this file) — weakens type safety`,
15423
- severity: "warning",
15424
- file: ctx.file.path,
15425
- line: loc.line,
15426
- column: loc.column,
15427
- endLine: loc.endLine,
15428
- endColumn: loc.endColumn,
15429
- suggestion: "Replace with a specific type, unknown, or a generic type parameter"
15430
- });
15294
+ findings.push(makeLineFinding("excessive-any", ctx, loc.line, loc.column, `Excessive use of 'any' type (${anyLocations.length} in this file) — weakens type safety`, "warning", "Replace with a specific type, unknown, or a generic type parameter", loc.endLine, loc.endColumn));
15431
15295
  }
15432
15296
  return findings;
15433
15297
  }
@@ -15466,18 +15330,7 @@ function detectJavaScript4(ctx) {
15466
15330
  const method = property.text();
15467
15331
  if (!DEBUG_METHODS.has(method))
15468
15332
  continue;
15469
- const range = call.range();
15470
- findings.push({
15471
- detectorId: "debug-console-in-prod",
15472
- message: `console.${method}() left in production code`,
15473
- severity: "warning",
15474
- file: ctx.file.path,
15475
- line: range.start.line + 1,
15476
- column: range.start.column + 1,
15477
- endLine: range.end.line + 1,
15478
- endColumn: range.end.column + 1,
15479
- suggestion: "Remove debug logging or replace with a structured logger"
15480
- });
15333
+ findings.push(makeFinding("debug-console-in-prod", ctx, call, `console.${method}() left in production code`, "warning", "Remove debug logging or replace with a structured logger"));
15481
15334
  }
15482
15335
  return findings;
15483
15336
  }
@@ -15491,18 +15344,7 @@ function detectPython4(ctx) {
15491
15344
  const callText = call.text();
15492
15345
  if (!callText.startsWith("print("))
15493
15346
  continue;
15494
- const range = call.range();
15495
- findings.push({
15496
- detectorId: "debug-console-in-prod",
15497
- message: "print() left in production code",
15498
- severity: "info",
15499
- file: ctx.file.path,
15500
- line: range.start.line + 1,
15501
- column: range.start.column + 1,
15502
- endLine: range.end.line + 1,
15503
- endColumn: range.end.column + 1,
15504
- suggestion: "Remove debug print or replace with logging module"
15505
- });
15347
+ findings.push(makeFinding("debug-console-in-prod", ctx, call, "print() left in production code", "info", "Remove debug print or replace with logging module"));
15506
15348
  }
15507
15349
  return findings;
15508
15350
  }
@@ -15541,15 +15383,7 @@ function detect3(ctx) {
15541
15383
  if (!isComment)
15542
15384
  continue;
15543
15385
  const hasSecurityImplication = SECURITY_KEYWORDS.test(line);
15544
- findings.push({
15545
- detectorId: "todo-in-production",
15546
- message: `${match[1]} comment in production code${hasSecurityImplication ? " (security-related)" : ""}`,
15547
- severity: hasSecurityImplication ? "warning" : "info",
15548
- file: ctx.file.path,
15549
- line: i + 1,
15550
- column: (match.index ?? 0) + 1,
15551
- suggestion: "Address the TODO or create a tracked issue and reference it in the comment"
15552
- });
15386
+ findings.push(makeLineFinding("todo-in-production", ctx, i + 1, (match.index ?? 0) + 1, `${match[1]} comment in production code${hasSecurityImplication ? " (security-related)" : ""}`, hasSecurityImplication ? "warning" : "info", "Address the TODO or create a tracked issue and reference it in the comment"));
15553
15387
  }
15554
15388
  return findings;
15555
15389
  }
@@ -15599,15 +15433,7 @@ function detect4(ctx) {
15599
15433
  const hasContext = CONFIG_CONTEXTS.test(line);
15600
15434
  if (!hasContext && description === "placeholder value")
15601
15435
  continue;
15602
- findings.push({
15603
- detectorId: "placeholder-in-production",
15604
- message: `Placeholder ${description} found: ${match[0].slice(0, 40)}`,
15605
- severity: "error",
15606
- file: ctx.file.path,
15607
- line: i + 1,
15608
- column: (match.index ?? 0) + 1,
15609
- suggestion: "Replace with actual configuration value or use environment variable"
15610
- });
15436
+ findings.push(makeLineFinding("placeholder-in-production", ctx, i + 1, (match.index ?? 0) + 1, `Placeholder ${description} found: ${match[0].slice(0, 40)}`, "error", "Replace with actual configuration value or use environment variable"));
15611
15437
  break;
15612
15438
  }
15613
15439
  }
@@ -15658,18 +15484,7 @@ function detect5(ctx) {
15658
15484
  if (!SENSITIVE_KEYS.test(keyArg.text()))
15659
15485
  continue;
15660
15486
  const storage = object.text();
15661
- const range = call.range();
15662
- findings.push({
15663
- detectorId: "token-in-localstorage",
15664
- message: `Auth token stored in ${storage} — vulnerable to XSS attacks`,
15665
- severity: "error",
15666
- file: ctx.file.path,
15667
- line: range.start.line + 1,
15668
- column: range.start.column + 1,
15669
- endLine: range.end.line + 1,
15670
- endColumn: range.end.column + 1,
15671
- suggestion: "Use httpOnly cookies for auth tokens instead of browser storage"
15672
- });
15487
+ findings.push(makeFinding("token-in-localstorage", ctx, call, `Auth token stored in ${storage} — vulnerable to XSS attacks`, "error", "Use httpOnly cookies for auth tokens instead of browser storage"));
15673
15488
  }
15674
15489
  return findings;
15675
15490
  }
@@ -15740,15 +15555,7 @@ function detect6(ctx) {
15740
15555
  if (violations.length < 2)
15741
15556
  return [];
15742
15557
  return [
15743
- {
15744
- detectorId: "god-component",
15745
- message: `Component file has too many hooks (${violations.join(", ")})`,
15746
- severity: "warning",
15747
- file: ctx.file.path,
15748
- line: 1,
15749
- column: 1,
15750
- suggestion: "Split this component into smaller, focused components. Extract custom hooks for related state and effects."
15751
- }
15558
+ makeLineFinding("god-component", ctx, 1, 1, `Component file has too many hooks (${violations.join(", ")})`, "warning", "Split this component into smaller, focused components. Extract custom hooks for related state and effects.")
15752
15559
  ];
15753
15560
  }
15754
15561
  var godComponent = {
@@ -15855,6 +15662,9 @@ function getJsFunctionName(node) {
15855
15662
  }
15856
15663
  return "<anonymous>";
15857
15664
  }
15665
+ function buildFinding(ctx, m, severity) {
15666
+ return makeLineFinding("god-function", ctx, m.startLine, m.startColumn, `Function '${m.name}' is too complex (${m.lines} lines, cyclomatic complexity ${m.complexity}, ${m.params} params)`, severity, "Break this function into smaller, focused functions. Extract helper methods, use early returns, and reduce branching.", m.endLine, m.endColumn);
15667
+ }
15858
15668
  function detectJavaScript5(ctx) {
15859
15669
  const findings = [];
15860
15670
  const root = ctx.root.root();
@@ -15946,19 +15756,6 @@ function detectPython5(ctx) {
15946
15756
  }
15947
15757
  return findings;
15948
15758
  }
15949
- function buildFinding(ctx, m, severity) {
15950
- return {
15951
- detectorId: "god-function",
15952
- message: `Function '${m.name}' is too complex (${m.lines} lines, cyclomatic complexity ${m.complexity}, ${m.params} params)`,
15953
- severity,
15954
- file: ctx.file.path,
15955
- line: m.startLine,
15956
- column: m.startColumn,
15957
- endLine: m.endLine,
15958
- endColumn: m.endColumn,
15959
- suggestion: "Break this function into smaller, focused functions. Extract helper methods, use early returns, and reduce branching."
15960
- };
15961
- }
15962
15759
  var godFunction = {
15963
15760
  id: "god-function",
15964
15761
  meta: {
@@ -16024,35 +15821,13 @@ function detectJavaScript6(ctx) {
16024
15821
  if (firstArg.kind() === "template_string") {
16025
15822
  const hasSubstitution = firstArg.children().some((ch) => ch.kind() === "template_substitution");
16026
15823
  if (hasSubstitution) {
16027
- const range = call.range();
16028
- findings.push({
16029
- detectorId: "sql-injection",
16030
- message: "SQL query uses template literal with interpolation — potential SQL injection",
16031
- severity: "error",
16032
- file: ctx.file.path,
16033
- line: range.start.line + 1,
16034
- column: range.start.column + 1,
16035
- endLine: range.end.line + 1,
16036
- endColumn: range.end.column + 1,
16037
- suggestion: "Use parameterized queries instead: db.query('SELECT * FROM users WHERE id = $1', [userId])"
16038
- });
15824
+ findings.push(makeFinding("sql-injection", ctx, call, "SQL query uses template literal with interpolation — potential SQL injection", "error", "Use parameterized queries instead: db.query('SELECT * FROM users WHERE id = $1', [userId])"));
16039
15825
  }
16040
15826
  }
16041
15827
  if (firstArg.kind() === "binary_expression") {
16042
15828
  const text = firstArg.text();
16043
15829
  if (/\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|DROP|ALTER|CREATE)\b/i.test(text)) {
16044
- const range = call.range();
16045
- findings.push({
16046
- detectorId: "sql-injection",
16047
- message: "SQL query built with string concatenation — potential SQL injection",
16048
- severity: "error",
16049
- file: ctx.file.path,
16050
- line: range.start.line + 1,
16051
- column: range.start.column + 1,
16052
- endLine: range.end.line + 1,
16053
- endColumn: range.end.column + 1,
16054
- suggestion: "Use parameterized queries instead of string concatenation"
16055
- });
15830
+ findings.push(makeFinding("sql-injection", ctx, call, "SQL query built with string concatenation — potential SQL injection", "error", "Use parameterized queries instead of string concatenation"));
16056
15831
  }
16057
15832
  }
16058
15833
  }
@@ -16077,51 +15852,18 @@ function detectPython6(ctx) {
16077
15852
  continue;
16078
15853
  const firstArg = args[0];
16079
15854
  if (firstArg.kind() === "string" && firstArg.text().startsWith("f")) {
16080
- const range = call.range();
16081
- findings.push({
16082
- detectorId: "sql-injection",
16083
- message: "SQL query uses f-string — potential SQL injection",
16084
- severity: "error",
16085
- file: ctx.file.path,
16086
- line: range.start.line + 1,
16087
- column: range.start.column + 1,
16088
- endLine: range.end.line + 1,
16089
- endColumn: range.end.column + 1,
16090
- suggestion: "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))"
16091
- });
15855
+ findings.push(makeFinding("sql-injection", ctx, call, "SQL query uses f-string — potential SQL injection", "error", "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))"));
16092
15856
  }
16093
15857
  if (firstArg.kind() === "call" && firstArg.text().includes(".format(")) {
16094
15858
  const text = firstArg.text();
16095
15859
  if (/\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b/i.test(text)) {
16096
- const range = call.range();
16097
- findings.push({
16098
- detectorId: "sql-injection",
16099
- message: "SQL query uses .format() — potential SQL injection",
16100
- severity: "error",
16101
- file: ctx.file.path,
16102
- line: range.start.line + 1,
16103
- column: range.start.column + 1,
16104
- endLine: range.end.line + 1,
16105
- endColumn: range.end.column + 1,
16106
- suggestion: "Use parameterized queries instead of .format()"
16107
- });
15860
+ findings.push(makeFinding("sql-injection", ctx, call, "SQL query uses .format() — potential SQL injection", "error", "Use parameterized queries instead of .format()"));
16108
15861
  }
16109
15862
  }
16110
15863
  if (firstArg.kind() === "binary_expression" || firstArg.kind() === "string" && args.length >= 2) {
16111
15864
  const stmtText = call.text();
16112
15865
  if (stmtText.includes(" % ") && /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b/i.test(stmtText)) {
16113
- const range = call.range();
16114
- findings.push({
16115
- detectorId: "sql-injection",
16116
- message: "SQL query uses % formatting — potential SQL injection",
16117
- severity: "error",
16118
- file: ctx.file.path,
16119
- line: range.start.line + 1,
16120
- column: range.start.column + 1,
16121
- endLine: range.end.line + 1,
16122
- endColumn: range.end.column + 1,
16123
- suggestion: "Use parameterized queries: cursor.execute('SELECT ... WHERE id = %s', (value,))"
16124
- });
15866
+ findings.push(makeFinding("sql-injection", ctx, call, "SQL query uses % formatting — potential SQL injection", "error", "Use parameterized queries: cursor.execute('SELECT ... WHERE id = %s', (value,))"));
16125
15867
  }
16126
15868
  }
16127
15869
  }
@@ -16159,18 +15901,7 @@ function detect7(ctx) {
16159
15901
  continue;
16160
15902
  if (name.text() !== "dangerouslySetInnerHTML")
16161
15903
  continue;
16162
- const range = attr.range();
16163
- findings.push({
16164
- detectorId: "dangerous-inner-html",
16165
- message: "dangerouslySetInnerHTML can lead to XSS attacks if the content is not sanitized",
16166
- severity: "warning",
16167
- file: ctx.file.path,
16168
- line: range.start.line + 1,
16169
- column: range.start.column + 1,
16170
- endLine: range.end.line + 1,
16171
- endColumn: range.end.column + 1,
16172
- suggestion: "Use a sanitization library like DOMPurify: dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(content)}}"
16173
- });
15904
+ findings.push(makeFinding("dangerous-inner-html", ctx, attr, "dangerouslySetInnerHTML can lead to XSS attacks if the content is not sanitized", "warning", "Use a sanitization library like DOMPurify: dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(content)}}"));
16174
15905
  }
16175
15906
  return findings;
16176
15907
  }
@@ -16287,18 +16018,7 @@ function detectJavaScript7(ctx) {
16287
16018
  }
16288
16019
  if (chainHasLimit)
16289
16020
  continue;
16290
- const range = call.range();
16291
- findings.push({
16292
- detectorId: "unbounded-query",
16293
- message: "Query fetches multiple records without a limit — may return excessive data",
16294
- severity: "info",
16295
- file: ctx.file.path,
16296
- line: range.start.line + 1,
16297
- column: range.start.column + 1,
16298
- endLine: range.end.line + 1,
16299
- endColumn: range.end.column + 1,
16300
- suggestion: "Add a limit: findMany({ take: 100 }) or .limit(100)"
16301
- });
16021
+ findings.push(makeFinding("unbounded-query", ctx, call, "Query fetches multiple records without a limit — may return excessive data", "info", "Add a limit: findMany({ take: 100 }) or .limit(100)"));
16302
16022
  }
16303
16023
  return findings;
16304
16024
  }
@@ -16391,26 +16111,10 @@ function detect8(ctx) {
16391
16111
  }
16392
16112
  }
16393
16113
  if (hasUIImport && hasDBImport) {
16394
- findings.push({
16395
- detectorId: "mixed-concerns",
16396
- message: `File imports both UI framework (${uiImportName}) and database (${dbImportName}) — mixed concerns`,
16397
- severity: "warning",
16398
- file: ctx.file.path,
16399
- line: 1,
16400
- column: 1,
16401
- suggestion: "Separate UI rendering from data access. Move database logic to a service/API layer."
16402
- });
16114
+ findings.push(makeLineFinding("mixed-concerns", ctx, 1, 1, `File imports both UI framework (${uiImportName}) and database (${dbImportName}) — mixed concerns`, "warning", "Separate UI rendering from data access. Move database logic to a service/API layer."));
16403
16115
  }
16404
16116
  if (hasUIImport && hasServerImport) {
16405
- findings.push({
16406
- detectorId: "mixed-concerns",
16407
- message: `File imports both UI framework (${uiImportName}) and server framework — mixed concerns`,
16408
- severity: "warning",
16409
- file: ctx.file.path,
16410
- line: 1,
16411
- column: 1,
16412
- suggestion: "Separate UI components from server-side logic."
16413
- });
16117
+ findings.push(makeLineFinding("mixed-concerns", ctx, 1, 1, `File imports both UI framework (${uiImportName}) and server framework — mixed concerns`, "warning", "Separate UI components from server-side logic."));
16414
16118
  }
16415
16119
  return findings;
16416
16120
  }
@@ -16425,36 +16129,770 @@ var mixedConcerns = {
16425
16129
  },
16426
16130
  detect: detect8
16427
16131
  };
16428
- // src/detectors/index.ts
16429
- var builtinDetectors = [
16430
- emptyErrorHandler,
16431
- trivialAssertion,
16432
- insecureDefaults,
16433
- undeclaredImport,
16434
- overDefensiveCoding,
16435
- excessiveCommentRatio,
16436
- overMocking,
16437
- nPlusOneQuery,
16438
- uncheckedDbResult,
16439
- deadCodePath,
16440
- doubleTypeAssertion,
16441
- excessiveAny,
16442
- debugConsoleInProd,
16443
- todoInProduction,
16444
- placeholderInProduction,
16132
+ // src/detectors/unsafe-shell-exec.ts
16133
+ var EXEC_FUNCTIONS = new Set(["exec", "execSync"]);
16134
+ var SUBPROCESS_METHODS = new Set(["run", "call", "Popen", "check_output", "check_call"]);
16135
+ function detectJavaScript8(ctx) {
16136
+ const findings = [];
16137
+ const root = ctx.root.root();
16138
+ const callExprs = root.findAll({ rule: { kind: "call_expression" } });
16139
+ for (const call of callExprs) {
16140
+ const children = call.children();
16141
+ const callee = children[0];
16142
+ if (!callee)
16143
+ continue;
16144
+ let isExecCall = false;
16145
+ if (callee.kind() === "identifier" && EXEC_FUNCTIONS.has(callee.text())) {
16146
+ isExecCall = true;
16147
+ } else if (callee.kind() === "member_expression") {
16148
+ const text = callee.text();
16149
+ for (const fn of EXEC_FUNCTIONS) {
16150
+ if (text.endsWith(`.${fn}`)) {
16151
+ isExecCall = true;
16152
+ break;
16153
+ }
16154
+ }
16155
+ }
16156
+ if (!isExecCall)
16157
+ continue;
16158
+ const args = children.find((ch) => ch.kind() === "arguments");
16159
+ if (!args)
16160
+ continue;
16161
+ const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
16162
+ if (argNodes.length === 0)
16163
+ continue;
16164
+ const firstArg = argNodes[0];
16165
+ const argKind = firstArg.kind();
16166
+ if (argKind === "string" || argKind === "string_fragment")
16167
+ continue;
16168
+ findings.push(makeFinding("unsafe-shell-exec", ctx, call, `${callee.text()}() called with dynamic argument — risk of shell injection`, "error", "Use execFile() or spawn() with an argument array instead of exec() with string interpolation"));
16169
+ }
16170
+ return findings;
16171
+ }
16172
+ function detectPython7(ctx) {
16173
+ const findings = [];
16174
+ const root = ctx.root.root();
16175
+ const calls = root.findAll({ rule: { kind: "call" } });
16176
+ for (const call of calls) {
16177
+ const children = call.children();
16178
+ const callee = children[0];
16179
+ if (!callee)
16180
+ continue;
16181
+ const calleeText = callee.text();
16182
+ let isSubprocessCall = false;
16183
+ for (const method of SUBPROCESS_METHODS) {
16184
+ if (calleeText === `subprocess.${method}` || calleeText.endsWith(`.${method}`)) {
16185
+ isSubprocessCall = true;
16186
+ break;
16187
+ }
16188
+ }
16189
+ if (!isSubprocessCall)
16190
+ continue;
16191
+ const argList = children.find((ch) => ch.kind() === "argument_list");
16192
+ if (!argList)
16193
+ continue;
16194
+ const kwargs = argList.children().filter((ch) => ch.kind() === "keyword_argument");
16195
+ let hasShellTrue = false;
16196
+ for (const kwarg of kwargs) {
16197
+ const kwChildren = kwarg.children();
16198
+ const key = kwChildren.find((ch) => ch.kind() === "identifier");
16199
+ const value = kwChildren.find((ch) => ch.kind() === "true");
16200
+ if (key && value && key.text() === "shell") {
16201
+ hasShellTrue = true;
16202
+ break;
16203
+ }
16204
+ }
16205
+ if (!hasShellTrue)
16206
+ continue;
16207
+ const positionalArgs = argList.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== "," && ch.kind() !== "keyword_argument");
16208
+ if (positionalArgs.length === 0)
16209
+ continue;
16210
+ const firstArg = positionalArgs[0];
16211
+ const argKind = firstArg.kind();
16212
+ if (argKind === "string") {
16213
+ const text = firstArg.text();
16214
+ if (!text.startsWith('f"') && !text.startsWith("f'")) {
16215
+ continue;
16216
+ }
16217
+ } else if (argKind === "concatenated_string") {} else if (argKind !== "identifier" && argKind !== "binary_operator") {
16218
+ continue;
16219
+ }
16220
+ findings.push(makeFinding("unsafe-shell-exec", ctx, call, `${calleeText}() called with shell=True and dynamic argument — risk of shell injection`, "error", "Pass arguments as a list and remove shell=True: subprocess.run(['cmd', arg])"));
16221
+ }
16222
+ return findings;
16223
+ }
16224
+ var unsafeShellExec = {
16225
+ id: "unsafe-shell-exec",
16226
+ meta: {
16227
+ name: "Unsafe Shell Execution",
16228
+ description: "Detects shell command execution with dynamic arguments that may be vulnerable to injection",
16229
+ severity: "error",
16230
+ category: "security",
16231
+ languages: ["javascript", "typescript", "tsx", "python"],
16232
+ priority: 10
16233
+ },
16234
+ detect(ctx) {
16235
+ if (ctx.file.language === "python")
16236
+ return detectPython7(ctx);
16237
+ return detectJavaScript8(ctx);
16238
+ }
16239
+ };
16240
+ // src/detectors/llm-call-no-timeout.ts
16241
+ var LLM_CONSTRUCTORS = new Set(["OpenAI", "Anthropic"]);
16242
+ function hasProperty(objectNode, propName) {
16243
+ const pairs = objectNode.findAll({ rule: { kind: "pair" } });
16244
+ for (const pair of pairs) {
16245
+ const children = pair.children();
16246
+ const key = children.find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string" || ch.kind() === "shorthand_property_identifier");
16247
+ if (key && key.text().replace(/["']/g, "") === propName) {
16248
+ return true;
16249
+ }
16250
+ }
16251
+ const shorthandProps = objectNode.findAll({ rule: { kind: "shorthand_property_identifier" } });
16252
+ for (const sp of shorthandProps) {
16253
+ if (sp.text() === propName)
16254
+ return true;
16255
+ }
16256
+ return false;
16257
+ }
16258
+ function detectJavaScript9(ctx) {
16259
+ const findings = [];
16260
+ const root = ctx.root.root();
16261
+ const newExprs = root.findAll({ rule: { kind: "new_expression" } });
16262
+ for (const newExpr of newExprs) {
16263
+ const children = newExpr.children();
16264
+ const constructorNode = children.find((ch) => ch.kind() === "identifier");
16265
+ if (!constructorNode || !LLM_CONSTRUCTORS.has(constructorNode.text()))
16266
+ continue;
16267
+ const args = children.find((ch) => ch.kind() === "arguments");
16268
+ if (!args) {
16269
+ findings.push(makeFinding("llm-call-no-timeout", ctx, newExpr, `new ${constructorNode.text()}() called without timeout option`, "warning", `Pass a timeout option: new ${constructorNode.text()}({ timeout: 30000 })`));
16270
+ continue;
16271
+ }
16272
+ const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
16273
+ if (argNodes.length === 0) {
16274
+ findings.push(makeFinding("llm-call-no-timeout", ctx, newExpr, `new ${constructorNode.text()}() called without timeout option`, "warning", `Pass a timeout option: new ${constructorNode.text()}({ timeout: 30000 })`));
16275
+ continue;
16276
+ }
16277
+ const firstArg = argNodes[0];
16278
+ if (firstArg.kind() === "object") {
16279
+ if (!hasProperty(firstArg, "timeout")) {
16280
+ findings.push(makeFinding("llm-call-no-timeout", ctx, newExpr, `new ${constructorNode.text()}() called without timeout option`, "warning", `Add timeout to options: new ${constructorNode.text()}({ timeout: 30000, ... })`));
16281
+ }
16282
+ }
16283
+ }
16284
+ const callExprs = root.findAll({ rule: { kind: "call_expression" } });
16285
+ for (const call of callExprs) {
16286
+ const children = call.children();
16287
+ const callee = children[0];
16288
+ if (!callee || callee.kind() !== "member_expression")
16289
+ continue;
16290
+ const calleeText = callee.text();
16291
+ if (!calleeText.endsWith(".create"))
16292
+ continue;
16293
+ if (!calleeText.includes("completions") && !calleeText.includes("messages") && !calleeText.includes("chat")) {
16294
+ continue;
16295
+ }
16296
+ const args = children.find((ch) => ch.kind() === "arguments");
16297
+ if (!args)
16298
+ continue;
16299
+ const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
16300
+ if (argNodes.length === 0)
16301
+ continue;
16302
+ const firstArg = argNodes[0];
16303
+ if (firstArg.kind() === "object") {
16304
+ if (!hasProperty(firstArg, "max_tokens")) {
16305
+ findings.push(makeFinding("llm-call-no-timeout", ctx, call, ".create() called without max_tokens — response size is unbounded", "warning", "Add max_tokens to limit response size: .create({ max_tokens: 1000, ... })"));
16306
+ }
16307
+ }
16308
+ }
16309
+ return findings;
16310
+ }
16311
+ function detectPython8(ctx) {
16312
+ const findings = [];
16313
+ const root = ctx.root.root();
16314
+ const calls = root.findAll({ rule: { kind: "call" } });
16315
+ for (const call of calls) {
16316
+ const children = call.children();
16317
+ const callee = children[0];
16318
+ if (!callee)
16319
+ continue;
16320
+ const calleeText = callee.text();
16321
+ if (!calleeText.endsWith(".create"))
16322
+ continue;
16323
+ if (!calleeText.includes("completions") && !calleeText.includes("Completion") && !calleeText.includes("messages") && !calleeText.includes("chat")) {
16324
+ continue;
16325
+ }
16326
+ const argList = children.find((ch) => ch.kind() === "argument_list");
16327
+ if (!argList)
16328
+ continue;
16329
+ const kwargs = argList.children().filter((ch) => ch.kind() === "keyword_argument");
16330
+ let hasTimeout = false;
16331
+ for (const kwarg of kwargs) {
16332
+ const kwChildren = kwarg.children();
16333
+ const key = kwChildren.find((ch) => ch.kind() === "identifier");
16334
+ if (key && key.text() === "timeout") {
16335
+ hasTimeout = true;
16336
+ break;
16337
+ }
16338
+ }
16339
+ if (!hasTimeout) {
16340
+ findings.push(makeFinding("llm-call-no-timeout", ctx, call, `${calleeText}() called without timeout — request may hang indefinitely`, "warning", "Add a timeout parameter: .create(timeout=30, ...)"));
16341
+ }
16342
+ }
16343
+ return findings;
16344
+ }
16345
+ var llmCallNoTimeout = {
16346
+ id: "llm-call-no-timeout",
16347
+ meta: {
16348
+ name: "LLM Call No Timeout",
16349
+ description: "Detects LLM API calls (OpenAI, Anthropic) without timeout or max_tokens configuration",
16350
+ severity: "warning",
16351
+ category: "quality",
16352
+ languages: ["javascript", "typescript", "tsx", "python"],
16353
+ priority: 10
16354
+ },
16355
+ detect(ctx) {
16356
+ if (ctx.file.language === "python")
16357
+ return detectPython8(ctx);
16358
+ return detectJavaScript9(ctx);
16359
+ }
16360
+ };
16361
+ // src/detectors/dynamic-code-exec.ts
16362
+ function detectJavaScript10(ctx) {
16363
+ const findings = [];
16364
+ const root = ctx.root.root();
16365
+ const callExprs = root.findAll({ rule: { kind: "call_expression" } });
16366
+ for (const call of callExprs) {
16367
+ const children = call.children();
16368
+ const callee = children[0];
16369
+ if (!callee || callee.kind() !== "identifier" || callee.text() !== "eval")
16370
+ continue;
16371
+ const args = children.find((ch) => ch.kind() === "arguments");
16372
+ if (!args)
16373
+ continue;
16374
+ const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
16375
+ if (argNodes.length === 0)
16376
+ continue;
16377
+ const firstArg = argNodes[0];
16378
+ if (firstArg.kind() === "string" || firstArg.kind() === "string_fragment")
16379
+ continue;
16380
+ findings.push(makeFinding("dynamic-code-exec", ctx, call, "eval() called with dynamic argument — arbitrary code execution risk", "error", "Avoid eval() with dynamic input. Use JSON.parse() for data or refactor to avoid dynamic code"));
16381
+ }
16382
+ const newExprs = root.findAll({ rule: { kind: "new_expression" } });
16383
+ for (const newExpr of newExprs) {
16384
+ const children = newExpr.children();
16385
+ const constructorNode = children.find((ch) => ch.kind() === "identifier");
16386
+ if (!constructorNode || constructorNode.text() !== "Function")
16387
+ continue;
16388
+ const args = children.find((ch) => ch.kind() === "arguments");
16389
+ if (!args)
16390
+ continue;
16391
+ const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
16392
+ if (argNodes.length === 0)
16393
+ continue;
16394
+ const lastArg = argNodes[argNodes.length - 1];
16395
+ if (lastArg.kind() === "string" || lastArg.kind() === "string_fragment")
16396
+ continue;
16397
+ findings.push(makeFinding("dynamic-code-exec", ctx, newExpr, "new Function() called with dynamic argument — arbitrary code execution risk", "error", "Avoid new Function() with dynamic input. Use static function definitions instead"));
16398
+ }
16399
+ return findings;
16400
+ }
16401
+ function detectPython9(ctx) {
16402
+ const findings = [];
16403
+ const root = ctx.root.root();
16404
+ const calls = root.findAll({ rule: { kind: "call" } });
16405
+ for (const call of calls) {
16406
+ const children = call.children();
16407
+ const callee = children[0];
16408
+ if (!callee || callee.kind() !== "identifier")
16409
+ continue;
16410
+ const funcName = callee.text();
16411
+ if (funcName !== "eval" && funcName !== "exec")
16412
+ continue;
16413
+ const argList = children.find((ch) => ch.kind() === "argument_list");
16414
+ if (!argList)
16415
+ continue;
16416
+ const argNodes = argList.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== "," && ch.kind() !== "keyword_argument");
16417
+ if (argNodes.length === 0)
16418
+ continue;
16419
+ const firstArg = argNodes[0];
16420
+ if (firstArg.kind() === "string") {
16421
+ const text = firstArg.text();
16422
+ if (!text.startsWith('f"') && !text.startsWith("f'")) {
16423
+ continue;
16424
+ }
16425
+ } else if (firstArg.kind() === "identifier" || firstArg.kind() === "binary_operator" || firstArg.kind() === "concatenated_string") {} else {
16426
+ continue;
16427
+ }
16428
+ findings.push(makeFinding("dynamic-code-exec", ctx, call, `${funcName}() called with dynamic argument — arbitrary code execution risk`, "error", `Avoid ${funcName}() with dynamic input. Use ast.literal_eval() for safe expression evaluation`));
16429
+ }
16430
+ return findings;
16431
+ }
16432
+ var dynamicCodeExec = {
16433
+ id: "dynamic-code-exec",
16434
+ meta: {
16435
+ name: "Dynamic Code Execution",
16436
+ description: "Detects eval() and new Function() / exec() with dynamic (non-literal) arguments",
16437
+ severity: "error",
16438
+ category: "security",
16439
+ languages: ["javascript", "typescript", "tsx", "python"],
16440
+ priority: 10
16441
+ },
16442
+ detect(ctx) {
16443
+ if (ctx.file.language === "python")
16444
+ return detectPython9(ctx);
16445
+ return detectJavaScript10(ctx);
16446
+ }
16447
+ };
16448
+ // src/detectors/llm-unpinned-model.ts
16449
+ var DATE_SUFFIX_RE = /[-_]\d{8}$/;
16450
+ var DATE_DASH_SUFFIX_RE = /[-_]\d{4}-\d{2}-\d{2}$/;
16451
+ var UNPINNED_MODEL_PATTERNS = [
16452
+ /^gpt-4o$/,
16453
+ /^gpt-4$/,
16454
+ /^gpt-4o-mini$/,
16455
+ /^gpt-4-turbo$/,
16456
+ /^gpt-3\.5-turbo$/,
16457
+ /^o1$/,
16458
+ /^o1-mini$/,
16459
+ /^o1-preview$/,
16460
+ /^o3$/,
16461
+ /^o3-mini$/,
16462
+ /^o4-mini$/,
16463
+ /^claude-.*-latest$/,
16464
+ /^claude-3-opus$/,
16465
+ /^claude-3-haiku$/,
16466
+ /^claude-3-5-sonnet$/,
16467
+ /^claude-3-5-haiku$/,
16468
+ /^claude-sonnet-4$/,
16469
+ /^claude-opus-4$/,
16470
+ /^gemini-pro$/,
16471
+ /^gemini-1\.5-pro$/,
16472
+ /^gemini-1\.5-flash$/,
16473
+ /^gemini-2\.0-flash$/
16474
+ ];
16475
+ function isPinned(model) {
16476
+ return DATE_SUFFIX_RE.test(model) || DATE_DASH_SUFFIX_RE.test(model);
16477
+ }
16478
+ function isUnpinnedModel(value) {
16479
+ if (isPinned(value))
16480
+ return false;
16481
+ return UNPINNED_MODEL_PATTERNS.some((re) => re.test(value));
16482
+ }
16483
+ function detect9(ctx) {
16484
+ const findings = [];
16485
+ const lines = ctx.source.split(`
16486
+ `);
16487
+ const stringLiteralRe = /(['"])([^'"]*)\1/g;
16488
+ for (let i = 0;i < lines.length; i++) {
16489
+ const line = lines[i];
16490
+ let match;
16491
+ stringLiteralRe.lastIndex = 0;
16492
+ while ((match = stringLiteralRe.exec(line)) !== null) {
16493
+ const value = match[2];
16494
+ if (isUnpinnedModel(value)) {
16495
+ findings.push(makeLineFinding("llm-unpinned-model", ctx, i + 1, match.index + 1, `Unpinned model alias "${value}" — model behavior may change without notice`, "warning", `Pin to a specific version with a date suffix, e.g. "${value}-YYYYMMDD"`));
16496
+ }
16497
+ }
16498
+ }
16499
+ return findings;
16500
+ }
16501
+ var llmUnpinnedModel = {
16502
+ id: "llm-unpinned-model",
16503
+ meta: {
16504
+ name: "LLM Unpinned Model",
16505
+ description: "Detects unpinned LLM model aliases that may change behavior without notice",
16506
+ severity: "warning",
16507
+ category: "quality",
16508
+ languages: ["javascript", "typescript", "tsx", "python"],
16509
+ priority: 10
16510
+ },
16511
+ detect: detect9
16512
+ };
16513
+ // src/detectors/llm-no-system-message.ts
16514
+ var SYSTEM_ROLE_RE = /["']system["']/;
16515
+ function detectJavaScript11(ctx) {
16516
+ const findings = [];
16517
+ const root = ctx.root.root();
16518
+ const pairs = root.findAll({ rule: { kind: "pair" } });
16519
+ for (const pair of pairs) {
16520
+ const children = pair.children();
16521
+ const key = children.find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string" || ch.kind() === "shorthand_property_identifier");
16522
+ if (!key)
16523
+ continue;
16524
+ const keyName = key.text().replace(/["']/g, "");
16525
+ if (keyName !== "messages")
16526
+ continue;
16527
+ const value = children.find((ch) => ch.kind() === "array");
16528
+ if (!value)
16529
+ continue;
16530
+ const arrayText = value.text();
16531
+ if (!SYSTEM_ROLE_RE.test(arrayText)) {
16532
+ findings.push(makeFinding("llm-no-system-message", ctx, pair, "messages array has no system message — LLM behavior may be unpredictable", "info", 'Add a system message: { role: "system", content: "You are a helpful assistant." }'));
16533
+ }
16534
+ }
16535
+ return findings;
16536
+ }
16537
+ function detectPython10(ctx) {
16538
+ const findings = [];
16539
+ const root = ctx.root.root();
16540
+ const kwargs = root.findAll({ rule: { kind: "keyword_argument" } });
16541
+ for (const kwarg of kwargs) {
16542
+ const children = kwarg.children();
16543
+ const key = children.find((ch) => ch.kind() === "identifier");
16544
+ if (!key || key.text() !== "messages")
16545
+ continue;
16546
+ const value = children.find((ch) => ch.kind() === "list");
16547
+ if (!value)
16548
+ continue;
16549
+ const listText = value.text();
16550
+ if (!SYSTEM_ROLE_RE.test(listText)) {
16551
+ findings.push(makeFinding("llm-no-system-message", ctx, kwarg, "messages list has no system message — LLM behavior may be unpredictable", "info", 'Add a system message: {"role": "system", "content": "You are a helpful assistant."}'));
16552
+ }
16553
+ }
16554
+ const assignments = root.findAll({ rule: { kind: "assignment" } });
16555
+ for (const assign of assignments) {
16556
+ const children = assign.children();
16557
+ const nameNode = children.find((ch) => ch.kind() === "identifier");
16558
+ if (!nameNode || nameNode.text() !== "messages")
16559
+ continue;
16560
+ const value = children.find((ch) => ch.kind() === "list");
16561
+ if (!value)
16562
+ continue;
16563
+ const listText = value.text();
16564
+ if (!SYSTEM_ROLE_RE.test(listText)) {
16565
+ findings.push(makeFinding("llm-no-system-message", ctx, assign, "messages list has no system message — LLM behavior may be unpredictable", "info", 'Add a system message: {"role": "system", "content": "You are a helpful assistant."}'));
16566
+ }
16567
+ }
16568
+ return findings;
16569
+ }
16570
+ var llmNoSystemMessage = {
16571
+ id: "llm-no-system-message",
16572
+ meta: {
16573
+ name: "LLM No System Message",
16574
+ description: "Detects LLM chat API calls where messages array lacks a system role message",
16575
+ severity: "info",
16576
+ category: "quality",
16577
+ languages: ["javascript", "typescript", "tsx", "python"],
16578
+ priority: 10
16579
+ },
16580
+ detect(ctx) {
16581
+ if (ctx.file.language === "python")
16582
+ return detectPython10(ctx);
16583
+ return detectJavaScript11(ctx);
16584
+ }
16585
+ };
16586
+ // src/detectors/llm-temperature-not-set.ts
16587
+ function detectJavaScript12(ctx) {
16588
+ const findings = [];
16589
+ const root = ctx.root.root();
16590
+ const callExprs = root.findAll({ rule: { kind: "call_expression" } });
16591
+ for (const call of callExprs) {
16592
+ const children = call.children();
16593
+ const callee = children[0];
16594
+ if (!callee || callee.kind() !== "member_expression")
16595
+ continue;
16596
+ const calleeText = callee.text();
16597
+ if (!calleeText.endsWith(".create"))
16598
+ continue;
16599
+ if (!calleeText.includes("completions") && !calleeText.includes("messages") && !calleeText.includes("chat")) {
16600
+ continue;
16601
+ }
16602
+ const args = children.find((ch) => ch.kind() === "arguments");
16603
+ if (!args)
16604
+ continue;
16605
+ const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
16606
+ if (argNodes.length === 0)
16607
+ continue;
16608
+ const firstArg = argNodes[0];
16609
+ if (firstArg.kind() !== "object")
16610
+ continue;
16611
+ const pairs = firstArg.findAll({ rule: { kind: "pair" } });
16612
+ let hasTemperature = false;
16613
+ for (const pair of pairs) {
16614
+ const pairChildren = pair.children();
16615
+ const key = pairChildren.find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string" || ch.kind() === "shorthand_property_identifier");
16616
+ if (key && key.text().replace(/["']/g, "") === "temperature") {
16617
+ hasTemperature = true;
16618
+ break;
16619
+ }
16620
+ }
16621
+ if (!hasTemperature) {
16622
+ const shorthandProps = firstArg.findAll({ rule: { kind: "shorthand_property_identifier" } });
16623
+ for (const sp of shorthandProps) {
16624
+ if (sp.text() === "temperature") {
16625
+ hasTemperature = true;
16626
+ break;
16627
+ }
16628
+ }
16629
+ }
16630
+ if (!hasTemperature) {
16631
+ findings.push(makeFinding("llm-temperature-not-set", ctx, call, ".create() called without temperature — model output randomness is not controlled", "info", "Set temperature explicitly: .create({ temperature: 0.7, ... })"));
16632
+ }
16633
+ }
16634
+ return findings;
16635
+ }
16636
+ function detectPython11(ctx) {
16637
+ const findings = [];
16638
+ const root = ctx.root.root();
16639
+ const calls = root.findAll({ rule: { kind: "call" } });
16640
+ for (const call of calls) {
16641
+ const children = call.children();
16642
+ const callee = children[0];
16643
+ if (!callee)
16644
+ continue;
16645
+ const calleeText = callee.text();
16646
+ if (!calleeText.endsWith(".create"))
16647
+ continue;
16648
+ if (!calleeText.includes("completions") && !calleeText.includes("Completion") && !calleeText.includes("messages") && !calleeText.includes("chat")) {
16649
+ continue;
16650
+ }
16651
+ const argList = children.find((ch) => ch.kind() === "argument_list");
16652
+ if (!argList)
16653
+ continue;
16654
+ const kwargs = argList.children().filter((ch) => ch.kind() === "keyword_argument");
16655
+ let hasTemperature = false;
16656
+ for (const kwarg of kwargs) {
16657
+ const kwChildren = kwarg.children();
16658
+ const key = kwChildren.find((ch) => ch.kind() === "identifier");
16659
+ if (key && key.text() === "temperature") {
16660
+ hasTemperature = true;
16661
+ break;
16662
+ }
16663
+ }
16664
+ if (!hasTemperature) {
16665
+ findings.push(makeFinding("llm-temperature-not-set", ctx, call, `${calleeText}() called without temperature — model output randomness is not controlled`, "info", "Set temperature explicitly: .create(temperature=0.7, ...)"));
16666
+ }
16667
+ }
16668
+ return findings;
16669
+ }
16670
+ var llmTemperatureNotSet = {
16671
+ id: "llm-temperature-not-set",
16672
+ meta: {
16673
+ name: "LLM Temperature Not Set",
16674
+ description: "Detects LLM API .create() calls without explicit temperature parameter",
16675
+ severity: "info",
16676
+ category: "quality",
16677
+ languages: ["javascript", "typescript", "tsx", "python"],
16678
+ priority: 10
16679
+ },
16680
+ detect(ctx) {
16681
+ if (ctx.file.language === "python")
16682
+ return detectPython11(ctx);
16683
+ return detectJavaScript12(ctx);
16684
+ }
16685
+ };
16686
+ // src/detectors/hallucinated-package.ts
16687
+ import { readFileSync as readFileSync3 } from "node:fs";
16688
+ import { fileURLToPath } from "node:url";
16689
+ import { dirname as dirname2, join as join2, basename as basename2 } from "node:path";
16690
+ var __dirname2 = dirname2(fileURLToPath(import.meta.url));
16691
+ var knownPackages = JSON.parse(readFileSync3(join2(__dirname2, "../data/known-packages.json"), "utf-8"));
16692
+ var KNOWN_SET = new Set(knownPackages);
16693
+ var KNOWN_SCOPES = new Set([
16694
+ "@types",
16695
+ "@babel",
16696
+ "@rollup",
16697
+ "@eslint",
16698
+ "@typescript-eslint",
16699
+ "@angular",
16700
+ "@vue",
16701
+ "@nuxt",
16702
+ "@svelte",
16703
+ "@sveltejs",
16704
+ "@react-native",
16705
+ "@react-native-community",
16706
+ "@aws-sdk",
16707
+ "@aws-cdk",
16708
+ "@google-cloud",
16709
+ "@azure",
16710
+ "@firebase",
16711
+ "@vercel",
16712
+ "@netlify",
16713
+ "@cloudflare",
16714
+ "@testing-library",
16715
+ "@storybook",
16716
+ "@prisma",
16717
+ "@trpc",
16718
+ "@tanstack",
16719
+ "@emotion",
16720
+ "@mui",
16721
+ "@chakra-ui",
16722
+ "@radix-ui",
16723
+ "@headlessui",
16724
+ "@sentry",
16725
+ "@datadog",
16726
+ "@opentelemetry",
16727
+ "@octokit",
16728
+ "@actions",
16729
+ "@nestjs",
16730
+ "@fastify",
16731
+ "@hapi",
16732
+ "@grpc",
16733
+ "@apollo",
16734
+ "@graphql-codegen",
16735
+ "@graphql-tools",
16736
+ "@remix-run",
16737
+ "@shopify",
16738
+ "@stripe",
16739
+ "@auth0",
16740
+ "@clerk",
16741
+ "@supabase",
16742
+ "@upstash",
16743
+ "@expo",
16744
+ "@react-navigation",
16745
+ "@mantine",
16746
+ "@floating-ui",
16747
+ "@dnd-kit",
16748
+ "@tailwindcss",
16749
+ "@heroicons",
16750
+ "@iconify",
16751
+ "@astrojs",
16752
+ "@sanity",
16753
+ "@contentful",
16754
+ "@mdx-js",
16755
+ "@codemirror",
16756
+ "@tiptap",
16757
+ "@monaco-editor",
16758
+ "@react-aria",
16759
+ "@react-stately",
16760
+ "@react-spring",
16761
+ "@react-three",
16762
+ "@fontsource",
16763
+ "@mapbox",
16764
+ "@nrwl",
16765
+ "@nx",
16766
+ "@swc",
16767
+ "@vitejs",
16768
+ "@esbuild",
16769
+ "@changesets",
16770
+ "@commitlint",
16771
+ "@semantic-release",
16772
+ "@rushstack",
16773
+ "@microsoft",
16774
+ "@sindresorhus",
16775
+ "@antfu",
16776
+ "@jridgewell",
16777
+ "@csstools",
16778
+ "@webassemblyjs",
16779
+ "@npmcli",
16780
+ "@isaacs",
16781
+ "@pkgr",
16782
+ "@nodelib",
16783
+ "@tsconfig",
16784
+ "@jest",
16785
+ "@vitest",
16786
+ "@sinonjs",
16787
+ "@hono",
16788
+ "@elysiajs",
16789
+ "@effect",
16790
+ "@langchain",
16791
+ "@ai-sdk",
16792
+ "@aws-lambda-powertools",
16793
+ "@middy",
16794
+ "@pulumi",
16795
+ "@builder.io",
16796
+ "@solidjs",
16797
+ "@biomejs"
16798
+ ]);
16799
+ function getScope(packageName) {
16800
+ if (!packageName.startsWith("@"))
16801
+ return null;
16802
+ const slashIdx = packageName.indexOf("/");
16803
+ if (slashIdx === -1)
16804
+ return null;
16805
+ return packageName.slice(0, slashIdx);
16806
+ }
16807
+ var hallucinatedPackage = {
16808
+ id: "hallucinated-package",
16809
+ meta: {
16810
+ name: "Hallucinated Package",
16811
+ description: "Detects potentially hallucinated (non-existent) packages in package.json",
16812
+ severity: "info",
16813
+ category: "correctness",
16814
+ languages: ["javascript", "typescript"],
16815
+ priority: 10
16816
+ },
16817
+ detect(ctx) {
16818
+ if (basename2(ctx.file.path) !== "package.json") {
16819
+ return [];
16820
+ }
16821
+ let pkg;
16822
+ try {
16823
+ pkg = JSON.parse(ctx.source);
16824
+ } catch {
16825
+ return [];
16826
+ }
16827
+ const findings = [];
16828
+ const lines = ctx.source.split(`
16829
+ `);
16830
+ const deps = pkg.dependencies;
16831
+ const devDeps = pkg.devDependencies;
16832
+ const allDeps = [];
16833
+ if (deps && typeof deps === "object") {
16834
+ allDeps.push(...Object.keys(deps));
16835
+ }
16836
+ if (devDeps && typeof devDeps === "object") {
16837
+ allDeps.push(...Object.keys(devDeps));
16838
+ }
16839
+ for (const dep of allDeps) {
16840
+ if (KNOWN_SET.has(dep))
16841
+ continue;
16842
+ const scope = getScope(dep);
16843
+ if (scope && KNOWN_SCOPES.has(scope))
16844
+ continue;
16845
+ const searchStr = `"${dep}"`;
16846
+ let lineNum = 1;
16847
+ for (let i = 0;i < lines.length; i++) {
16848
+ if (lines[i].includes(searchStr)) {
16849
+ lineNum = i + 1;
16850
+ break;
16851
+ }
16852
+ }
16853
+ const col = (lines[lineNum - 1]?.indexOf(searchStr) ?? 0) + 1;
16854
+ findings.push(makeLineFinding("hallucinated-package", ctx, lineNum, col, `Package '${dep}' is not in the known-packages allowlist — verify it exists on npm`, "info", `Run: npm view ${dep} to check if this package exists`));
16855
+ }
16856
+ return findings;
16857
+ }
16858
+ };
16859
+ // src/detectors/index.ts
16860
+ var builtinDetectors = [
16861
+ emptyErrorHandler,
16862
+ trivialAssertion,
16863
+ insecureDefaults,
16864
+ undeclaredImport,
16865
+ overDefensiveCoding,
16866
+ excessiveCommentRatio,
16867
+ overMocking,
16868
+ nPlusOneQuery,
16869
+ uncheckedDbResult,
16870
+ deadCodePath,
16871
+ doubleTypeAssertion,
16872
+ excessiveAny,
16873
+ debugConsoleInProd,
16874
+ todoInProduction,
16875
+ placeholderInProduction,
16445
16876
  tokenInLocalstorage,
16446
16877
  godComponent,
16447
16878
  godFunction,
16448
16879
  sqlInjection,
16449
16880
  dangerousInnerHtml,
16450
16881
  unboundedQuery,
16451
- mixedConcerns
16882
+ mixedConcerns,
16883
+ unsafeShellExec,
16884
+ llmCallNoTimeout,
16885
+ dynamicCodeExec,
16886
+ llmUnpinnedModel,
16887
+ llmNoSystemMessage,
16888
+ llmTemperatureNotSet,
16889
+ hallucinatedPackage
16452
16890
  ];
16453
16891
 
16454
16892
  // src/engine.ts
16455
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "node:fs";
16893
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "node:fs";
16456
16894
  import { createRequire as createRequire2 } from "node:module";
16457
- import { extname, join as join2, relative, resolve as resolve2 } from "node:path";
16895
+ import { extname, join as join3, relative, resolve as resolve2 } from "node:path";
16458
16896
  import { parse, Lang as SgLang, registerDynamicLanguage } from "@ast-grep/napi";
16459
16897
  var EXTENSION_MAP = {
16460
16898
  ".js": "javascript",
@@ -16554,7 +16992,7 @@ function runDetectors(files, detectors, project, config, options = {}) {
16554
16992
  filesProcessed++;
16555
16993
  let source;
16556
16994
  try {
16557
- source = readFileSync3(file.absolutePath, "utf-8");
16995
+ source = readFileSync4(file.absolutePath, "utf-8");
16558
16996
  } catch (err) {
16559
16997
  errors2.push({
16560
16998
  file: file.path,
@@ -16636,13 +17074,44 @@ function runDetectors(files, detectors, project, config, options = {}) {
16636
17074
  }
16637
17075
  const totalMs = performance.now() - startTime;
16638
17076
  const timing = options.verbose ? { totalMs, perDetector } : undefined;
17077
+ const dedupedFindings = dedupFindings(findings, detectors);
16639
17078
  return {
16640
- findings,
17079
+ findings: dedupedFindings,
16641
17080
  filesScanned: filesProcessed,
16642
17081
  errors: errors2,
16643
17082
  timing
16644
17083
  };
16645
17084
  }
17085
+ function dedupFindings(findings, detectors) {
17086
+ const priorityMap = new Map;
17087
+ for (const d of detectors) {
17088
+ priorityMap.set(d.id, d.meta.priority ?? 0);
17089
+ }
17090
+ const groups = new Map;
17091
+ for (const f of findings) {
17092
+ const key = `${f.file}:${f.line}`;
17093
+ const group = groups.get(key);
17094
+ if (group) {
17095
+ group.push(f);
17096
+ } else {
17097
+ groups.set(key, [f]);
17098
+ }
17099
+ }
17100
+ const deduped = [];
17101
+ for (const group of groups.values()) {
17102
+ if (group.length === 1) {
17103
+ deduped.push(group[0]);
17104
+ } else {
17105
+ group.sort((a, b) => {
17106
+ const pa = priorityMap.get(a.detectorId) ?? 0;
17107
+ const pb = priorityMap.get(b.detectorId) ?? 0;
17108
+ return pb - pa;
17109
+ });
17110
+ deduped.push(group[0]);
17111
+ }
17112
+ }
17113
+ return deduped;
17114
+ }
16646
17115
  function runWithTimeout(fn, timeoutMs, detectorId, filePath) {
16647
17116
  const start = performance.now();
16648
17117
  try {
@@ -16678,7 +17147,7 @@ function walkDirectory(dir, scanRoot, compiledIgnorePatterns, files) {
16678
17147
  throw err;
16679
17148
  }
16680
17149
  for (const entry of entries) {
16681
- const fullPath = join2(dir, entry.name);
17150
+ const fullPath = join3(dir, entry.name);
16682
17151
  const relativePath = relative(scanRoot, fullPath);
16683
17152
  if (matchesIgnorePattern(relativePath, entry.name, compiledIgnorePatterns)) {
16684
17153
  continue;
@@ -16725,9 +17194,9 @@ function matchesIgnorePattern(relativePath, baseName, compiledPatterns) {
16725
17194
  return false;
16726
17195
  }
16727
17196
  function loadGitignore(root) {
16728
- const gitignorePath = join2(root, ".gitignore");
17197
+ const gitignorePath = join3(root, ".gitignore");
16729
17198
  try {
16730
- const content = readFileSync3(gitignorePath, "utf-8");
17199
+ const content = readFileSync4(gitignorePath, "utf-8");
16731
17200
  return content.split(`
16732
17201
  `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
16733
17202
  if (line.startsWith("/")) {
@@ -16750,7 +17219,7 @@ function getExtension(filename) {
16750
17219
  }
16751
17220
  function isBinaryFile(filePath) {
16752
17221
  try {
16753
- const fd = readFileSync3(filePath, { encoding: null, flag: "r" });
17222
+ const fd = readFileSync4(filePath, { encoding: null, flag: "r" });
16754
17223
  const sample = fd.subarray(0, 512);
16755
17224
  for (const byte of sample) {
16756
17225
  if (byte === 0)
@@ -16792,6 +17261,19 @@ function isNodeError2(err) {
16792
17261
  return err instanceof Error && "code" in err;
16793
17262
  }
16794
17263
 
17264
+ // src/formatters/agent.ts
17265
+ function formatAgent(result) {
17266
+ if (result.findings.length === 0) {
17267
+ return "";
17268
+ }
17269
+ return result.findings.map((f) => {
17270
+ const location = `${f.file}:${f.line}:${f.column}`;
17271
+ const suffix = f.suggestion ? `. ${f.suggestion}` : "";
17272
+ return `${location} ${f.severity} ${f.detectorId}: ${f.message}${suffix}`;
17273
+ }).join(`
17274
+ `);
17275
+ }
17276
+
16795
17277
  // src/formatters/github.ts
16796
17278
  import { appendFileSync } from "node:fs";
16797
17279
  function ghLevel(severity) {
@@ -17272,14 +17754,16 @@ function getFormatter(format, options) {
17272
17754
  return formatSarif;
17273
17755
  case "html":
17274
17756
  return formatHtml;
17757
+ case "agent":
17758
+ return formatAgent;
17275
17759
  default:
17276
- throw new Error(`Unknown format '${format}'. Available formats: text, json, github, sarif, html`);
17760
+ throw new Error(`Unknown format '${format}'. Available formats: text, json, github, sarif, html, agent`);
17277
17761
  }
17278
17762
  }
17279
17763
 
17280
17764
  // src/project.ts
17281
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
17282
- import { dirname as dirname2, join as join3, resolve as resolve3 } from "node:path";
17765
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
17766
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
17283
17767
  function loadProjectInfo(scanRoot) {
17284
17768
  const info = {
17285
17769
  dependencies: new Set,
@@ -17306,11 +17790,11 @@ function findProjectRoot(startDir) {
17306
17790
  "pyproject.toml"
17307
17791
  ];
17308
17792
  for (const m of manifests) {
17309
- if (existsSync3(join3(dir, m))) {
17793
+ if (existsSync3(join4(dir, m))) {
17310
17794
  return dir;
17311
17795
  }
17312
17796
  }
17313
- const parentDir = dirname2(dir);
17797
+ const parentDir = dirname3(dir);
17314
17798
  if (parentDir === dir)
17315
17799
  break;
17316
17800
  dir = parentDir;
@@ -17318,12 +17802,12 @@ function findProjectRoot(startDir) {
17318
17802
  return null;
17319
17803
  }
17320
17804
  function parsePackageJson(root, info) {
17321
- const pkgPath = join3(root, "package.json");
17805
+ const pkgPath = join4(root, "package.json");
17322
17806
  if (!existsSync3(pkgPath))
17323
17807
  return;
17324
17808
  info.manifests.push(pkgPath);
17325
17809
  try {
17326
- const raw = readFileSync4(pkgPath, "utf-8");
17810
+ const raw = readFileSync5(pkgPath, "utf-8");
17327
17811
  const pkg = JSON.parse(raw);
17328
17812
  if (isRecord(pkg.dependencies)) {
17329
17813
  for (const name of Object.keys(pkg.dependencies)) {
@@ -17342,11 +17826,11 @@ function parsePackageJson(root, info) {
17342
17826
  } catch {}
17343
17827
  }
17344
17828
  function parseLockFiles(root, info) {
17345
- const npmLockPath = join3(root, "package-lock.json");
17829
+ const npmLockPath = join4(root, "package-lock.json");
17346
17830
  if (existsSync3(npmLockPath)) {
17347
17831
  info.manifests.push(npmLockPath);
17348
17832
  try {
17349
- const raw = readFileSync4(npmLockPath, "utf-8");
17833
+ const raw = readFileSync5(npmLockPath, "utf-8");
17350
17834
  const lock = JSON.parse(raw);
17351
17835
  if (isRecord(lock.packages)) {
17352
17836
  for (const key of Object.keys(lock.packages)) {
@@ -17366,11 +17850,11 @@ function parseLockFiles(root, info) {
17366
17850
  }
17367
17851
  } catch {}
17368
17852
  }
17369
- const yarnLockPath = join3(root, "yarn.lock");
17853
+ const yarnLockPath = join4(root, "yarn.lock");
17370
17854
  if (existsSync3(yarnLockPath)) {
17371
17855
  info.manifests.push(yarnLockPath);
17372
17856
  try {
17373
- const raw = readFileSync4(yarnLockPath, "utf-8");
17857
+ const raw = readFileSync5(yarnLockPath, "utf-8");
17374
17858
  for (const line of raw.split(`
17375
17859
  `)) {
17376
17860
  const match = line.match(/^"?(@?[^@\s"]+)@/);
@@ -17383,11 +17867,11 @@ function parseLockFiles(root, info) {
17383
17867
  }
17384
17868
  } catch {}
17385
17869
  }
17386
- const pnpmLockPath = join3(root, "pnpm-lock.yaml");
17870
+ const pnpmLockPath = join4(root, "pnpm-lock.yaml");
17387
17871
  if (existsSync3(pnpmLockPath)) {
17388
17872
  info.manifests.push(pnpmLockPath);
17389
17873
  try {
17390
- const raw = readFileSync4(pnpmLockPath, "utf-8");
17874
+ const raw = readFileSync5(pnpmLockPath, "utf-8");
17391
17875
  for (const line of raw.split(`
17392
17876
  `)) {
17393
17877
  const match = line.match(/^\s+'?\/?(@?[^@\s':]+)@/);
@@ -17402,12 +17886,12 @@ function parseLockFiles(root, info) {
17402
17886
  }
17403
17887
  }
17404
17888
  function parseRequirementsTxt(root, info) {
17405
- const reqPath = join3(root, "requirements.txt");
17889
+ const reqPath = join4(root, "requirements.txt");
17406
17890
  if (!existsSync3(reqPath))
17407
17891
  return;
17408
17892
  info.manifests.push(reqPath);
17409
17893
  try {
17410
- const raw = readFileSync4(reqPath, "utf-8");
17894
+ const raw = readFileSync5(reqPath, "utf-8");
17411
17895
  for (const line of raw.split(`
17412
17896
  `)) {
17413
17897
  const trimmed = line.trim();
@@ -17423,12 +17907,12 @@ function parseRequirementsTxt(root, info) {
17423
17907
  } catch {}
17424
17908
  }
17425
17909
  function parsePyprojectToml(root, info) {
17426
- const tomlPath = join3(root, "pyproject.toml");
17910
+ const tomlPath = join4(root, "pyproject.toml");
17427
17911
  if (!existsSync3(tomlPath))
17428
17912
  return;
17429
17913
  info.manifests.push(tomlPath);
17430
17914
  try {
17431
- const raw = readFileSync4(tomlPath, "utf-8");
17915
+ const raw = readFileSync5(tomlPath, "utf-8");
17432
17916
  const lines = raw.split(`
17433
17917
  `);
17434
17918
  let inProjectSection = false;
@@ -17496,7 +17980,7 @@ function isRecord(value) {
17496
17980
  function getVersion() {
17497
17981
  try {
17498
17982
  const pkgPath = new URL("../package.json", import.meta.url);
17499
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
17983
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
17500
17984
  return pkg.version;
17501
17985
  } catch {
17502
17986
  return "0.0.0";
@@ -17529,7 +18013,7 @@ async function readStdinFiles() {
17529
18013
  }
17530
18014
  function getGitDiffFiles(ref, scanRoot) {
17531
18015
  try {
17532
- const output = execSync(`git diff --name-only ${ref}`, {
18016
+ const output = execSync2(`git diff --name-only ${ref}`, {
17533
18017
  cwd: scanRoot,
17534
18018
  encoding: "utf-8",
17535
18019
  stdio: ["pipe", "pipe", "pipe"]
@@ -17590,7 +18074,7 @@ async function scanAction(scanPath, options) {
17590
18074
  }
17591
18075
  function checkAction(filePath, options) {
17592
18076
  const absolutePath = resolve4(filePath);
17593
- if (!existsSync4(absolutePath)) {
18077
+ if (!existsSync5(absolutePath)) {
17594
18078
  process.stderr.write(`Error: File not found: ${filePath}
17595
18079
  `);
17596
18080
  process.exit(2);
@@ -17632,8 +18116,12 @@ function main() {
17632
18116
  setupEpipeHandler();
17633
18117
  const program2 = new Command;
17634
18118
  program2.name("vibecop").description("AI code quality linter built on ast-grep").version(getVersion());
17635
- program2.command("scan").description("Scan a directory for code quality issues").argument("[path]", "Directory to scan", ".").option("-f, --format <format>", "Output format (text, json, github, sarif, html)", "text").option("-c, --config <path>", "Path to config file").option("--no-config", "Disable config file loading").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--diff <ref>", "Scan only files changed vs git ref").option("--stdin-files", "Read file list from stdin", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(scanAction);
17636
- program2.command("check").description("Check a single file for code quality issues").argument("<file>", "File to check").option("-f, --format <format>", "Output format (text, json, github, sarif, html)", "text").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(checkAction);
18119
+ program2.command("scan").description("Scan a directory for code quality issues").argument("[path]", "Directory to scan", ".").option("-f, --format <format>", "Output format (text, json, github, sarif, html, agent)", "text").option("-c, --config <path>", "Path to config file").option("--no-config", "Disable config file loading").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--diff <ref>", "Scan only files changed vs git ref").option("--stdin-files", "Read file list from stdin", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(scanAction);
18120
+ program2.command("check").description("Check a single file for code quality issues").argument("<file>", "File to check").option("-f, --format <format>", "Output format (text, json, github, sarif, html, agent)", "text").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(checkAction);
18121
+ program2.command("init").description("Set up vibecop integration with AI coding tools").action(async () => {
18122
+ const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), exports_init));
18123
+ await runInit2();
18124
+ });
17637
18125
  program2.parse();
17638
18126
  }
17639
18127
  main();