pinata-security-cli 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { z } from 'zod';
3
- import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
4
- import fs, { mkdir, writeFile, readFile, readdir, stat } from 'fs/promises';
3
+ import fs, { mkdir, writeFile, readFile, stat, readdir } from 'fs/promises';
5
4
  import path, { dirname, resolve, basename, relative, join, extname } from 'path';
6
5
  import { useState } from 'react';
7
6
  import { render, useApp, useInput, Box, Text } from 'ink';
8
7
  import Spinner from 'ink-spinner';
9
8
  import { jsx, jsxs } from 'react/jsx-runtime';
9
+ import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
10
10
  import { homedir } from 'os';
11
11
  import { fileURLToPath } from 'url';
12
12
  import chalk5 from 'chalk';
@@ -160,9 +160,7 @@ async function saveScanResults(projectRoot, result) {
160
160
  try {
161
161
  const cacheDir = resolve(projectRoot, CACHE_DIR);
162
162
  const cachePath = getCachePath(projectRoot);
163
- if (!existsSync(cacheDir)) {
164
- await mkdir(cacheDir, { recursive: true });
165
- }
163
+ await mkdir(cacheDir, { recursive: true });
166
164
  const cached = {
167
165
  timestamp: result.completedAt.toISOString(),
168
166
  targetDirectory: result.targetDirectory,
@@ -187,7 +185,10 @@ async function saveScanResults(projectRoot, result) {
187
185
  async function loadScanResults(projectRoot) {
188
186
  try {
189
187
  const cachePath = getCachePath(projectRoot);
190
- if (!existsSync(cachePath)) {
188
+ let content;
189
+ try {
190
+ content = await readFile(cachePath, "utf-8");
191
+ } catch {
191
192
  return err(
192
193
  new PinataError(
193
194
  "No cached scan results found. Run `pinata analyze` first.",
@@ -195,7 +196,6 @@ async function loadScanResults(projectRoot) {
195
196
  )
196
197
  );
197
198
  }
198
- const content = await readFile(cachePath, "utf-8");
199
199
  const cached = JSON.parse(content);
200
200
  if (cached.version !== CACHE_VERSION) {
201
201
  return err(
@@ -743,97 +743,233 @@ var init_junit_formatter = __esm({
743
743
  });
744
744
 
745
745
  // src/core/verifier/ai-verifier.ts
746
- var VERIFICATION_PROMPT, AIVerifier;
746
+ var SKIP_PATTERNS, BATCH_PROMPT, SINGLE_ITEM_TEMPLATE, AIVerifier;
747
747
  var init_ai_verifier = __esm({
748
748
  "src/core/verifier/ai-verifier.ts"() {
749
749
  init_types();
750
- VERIFICATION_PROMPT = `You are a security code reviewer. Analyze this potential vulnerability and determine if it's a real issue.
751
-
752
- CATEGORY: {{category}}
753
- DESCRIPTION: {{description}}
754
-
755
- CODE CONTEXT:
756
- \`\`\`{{language}}
757
- {{codeContext}}
758
- \`\`\`
759
-
760
- FLAGGED LINE(S):
761
- \`\`\`{{language}}
762
- {{flaggedCode}}
763
- \`\`\`
764
-
765
- FILE: {{filePath}}
766
-
767
- Analyze this code and determine:
768
- 1. Is this actually vulnerable, or is it a false positive?
769
- 2. What is your confidence level (high/medium/low)?
770
- 3. What is your reasoning?
771
- 4. Are there any mitigating factors visible in the code?
772
- 5. If vulnerable, describe a concrete exploit scenario.
773
- 6. What is your recommendation?
750
+ SKIP_PATTERNS = {
751
+ paths: [
752
+ /\.test\.(ts|js|tsx|jsx)$/,
753
+ /\.spec\.(ts|js|tsx|jsx)$/,
754
+ /tests?\//i,
755
+ /fixtures?\//i,
756
+ /mocks?\//i,
757
+ /__tests__\//,
758
+ /node_modules\//,
759
+ /dist\//,
760
+ /\.d\.ts$/,
761
+ /examples?\//i
762
+ ],
763
+ // Content patterns that indicate false positive
764
+ content: [
765
+ /\/\/ SAFE:/i,
766
+ // Explicit safe marker
767
+ /\/\/ nosec/i,
768
+ // Security ignore
769
+ /eslint-disable/i,
770
+ /sanitized?|escaped?|validated?/i
771
+ // Near sanitization
772
+ ]
773
+ };
774
+ BATCH_PROMPT = `You are a security code reviewer. Analyze these potential vulnerabilities and determine which are real issues vs false positives.
774
775
 
775
- Be rigorous. Consider:
776
+ For each item, consider:
776
777
  - Is user input actually reaching this code?
777
778
  - Is there sanitization, validation, or encoding nearby?
778
779
  - Is this test code, example code, or production code?
779
780
  - Is there context that makes this safe?
780
781
 
781
- Respond in JSON format:
782
- {
783
- "isVulnerable": boolean,
784
- "confidence": "high" | "medium" | "low",
785
- "reasoning": "detailed explanation",
786
- "mitigatingFactors": ["factor1", "factor2"],
787
- "exploitScenario": "how an attacker would exploit this, or null if not exploitable",
788
- "recommendation": "what the developer should do"
789
- }`;
782
+ Be rigorous. Most pattern matches are false positives.
783
+
784
+ ITEMS TO ANALYZE:
785
+ {{items}}
786
+
787
+ Respond with a JSON array. Each object MUST have these exact fields:
788
+ [
789
+ {
790
+ "id": "1",
791
+ "isVulnerable": true/false,
792
+ "confidence": "high"/"medium"/"low",
793
+ "reasoning": "brief explanation"
794
+ },
795
+ ...
796
+ ]
797
+
798
+ Only return the JSON array, no other text.`;
799
+ SINGLE_ITEM_TEMPLATE = `
800
+ ---
801
+ ID: {{id}}
802
+ CATEGORY: {{category}}
803
+ FILE: {{filePath}}:{{lineNumber}}
804
+ CODE:
805
+ \`\`\`{{language}}
806
+ {{codeContext}}
807
+ \`\`\`
808
+ FLAGGED LINE: {{flaggedLine}}
809
+ ---`;
790
810
  AIVerifier = class {
791
811
  config;
812
+ batchSize;
813
+ concurrency;
792
814
  constructor(config2) {
793
815
  this.config = config2;
816
+ this.batchSize = config2.batchSize ?? 10;
817
+ this.concurrency = config2.concurrency ?? 3;
794
818
  }
795
819
  /**
796
- * Verify a single gap using AI analysis.
797
- */
798
- async verify(gap, fileContent) {
799
- const codeContext = this.extractContext(fileContent, gap.lineStart, 20);
800
- const flaggedCode = this.extractContext(fileContent, gap.lineStart, 5);
801
- const prompt = VERIFICATION_PROMPT.replace("{{category}}", gap.categoryName).replace("{{description}}", this.getCategoryDescription(gap.categoryId)).replace(/\{\{language\}\}/g, this.getLanguage(gap.filePath)).replace("{{codeContext}}", codeContext).replace("{{flaggedCode}}", flaggedCode).replace("{{filePath}}", gap.filePath);
802
- const response = await this.callAI(prompt);
803
- return this.parseResponse(response);
804
- }
805
- /**
806
- * Verify multiple gaps, filtering out false positives.
820
+ * Verify multiple gaps efficiently using filtering, batching, and parallelism.
821
+ *
822
+ * Flow:
823
+ * 1. Pre-filter obvious false positives (test files, etc.)
824
+ * 2. Group remaining gaps into batches of 10
825
+ * 3. Process 3 batches in parallel
826
+ * 4. Return verified gaps and dismissed with reasons
807
827
  */
808
828
  async verifyAll(gaps, getFileContent) {
809
829
  const verified = [];
810
830
  const dismissed = [];
811
- let aiFailures = 0;
812
- for (const gap of gaps) {
813
- try {
814
- const content = await getFileContent(gap.filePath);
815
- const result = await this.verify(gap, content);
816
- if (result.isVulnerable) {
817
- gap.verification = result;
818
- verified.push(gap);
819
- } else {
820
- dismissed.push({
821
- gap,
822
- reason: result.reasoning
823
- });
831
+ const { toVerify, preFiltered } = this.preFilter(gaps);
832
+ dismissed.push(...preFiltered);
833
+ if (toVerify.length === 0) {
834
+ return {
835
+ verified: [],
836
+ dismissed,
837
+ stats: {
838
+ total: gaps.length,
839
+ preFiltered: preFiltered.length,
840
+ aiDismissed: 0,
841
+ aiVerified: 0
824
842
  }
825
- } catch (error) {
826
- aiFailures++;
827
- if (aiFailures === 1) {
828
- console.error(`AI verification failed: ${error instanceof Error ? error.message : String(error)}`);
843
+ };
844
+ }
845
+ console.log(`Pre-filtered ${preFiltered.length} gaps. Verifying ${toVerify.length} with AI...`);
846
+ const fileContents = /* @__PURE__ */ new Map();
847
+ const uniquePaths = [...new Set(toVerify.map((g) => g.filePath))];
848
+ await Promise.all(
849
+ uniquePaths.map(async (path2) => {
850
+ try {
851
+ fileContents.set(path2, await getFileContent(path2));
852
+ } catch {
853
+ fileContents.set(path2, "");
829
854
  }
855
+ })
856
+ );
857
+ const batches = this.createBatches(toVerify, fileContents);
858
+ console.log(`Created ${batches.length} batches of ~${this.batchSize} gaps each`);
859
+ const results = await this.processParallel(batches, toVerify);
860
+ let aiVerified = 0;
861
+ let aiDismissed = 0;
862
+ for (const gap of toVerify) {
863
+ const gapId = `${gap.filePath}:${gap.lineStart}`;
864
+ const result = results.get(gapId);
865
+ if (!result || result.isVulnerable) {
830
866
  verified.push(gap);
867
+ aiVerified++;
868
+ } else {
869
+ dismissed.push({
870
+ gap,
871
+ reason: result.reasoning
872
+ });
873
+ aiDismissed++;
831
874
  }
832
875
  }
833
- if (aiFailures > 0) {
834
- console.error(`AI verification failed for ${aiFailures}/${gaps.length} gaps (kept as fail-safe)`);
876
+ return {
877
+ verified,
878
+ dismissed,
879
+ stats: {
880
+ total: gaps.length,
881
+ preFiltered: preFiltered.length,
882
+ aiDismissed,
883
+ aiVerified
884
+ }
885
+ };
886
+ }
887
+ /**
888
+ * Pre-filter gaps that are obviously false positives without needing AI.
889
+ */
890
+ preFilter(gaps) {
891
+ const toVerify = [];
892
+ const preFiltered = [];
893
+ for (const gap of gaps) {
894
+ const pathMatch = SKIP_PATTERNS.paths.find((p) => p.test(gap.filePath));
895
+ if (pathMatch) {
896
+ preFiltered.push({
897
+ gap,
898
+ reason: `Skipped: test/example file (${pathMatch.source})`
899
+ });
900
+ continue;
901
+ }
902
+ if (gap.categoryId === "precision-loss" && gap.filePath.endsWith(".ts")) {
903
+ preFiltered.push({
904
+ gap,
905
+ reason: "TypeScript type annotation, not runtime code"
906
+ });
907
+ continue;
908
+ }
909
+ toVerify.push(gap);
910
+ }
911
+ return { toVerify, preFiltered };
912
+ }
913
+ /**
914
+ * Create batches of gaps for batch API calls.
915
+ */
916
+ createBatches(gaps, fileContents) {
917
+ const batches = [];
918
+ for (let i = 0; i < gaps.length; i += this.batchSize) {
919
+ const batchGaps = gaps.slice(i, i + this.batchSize);
920
+ const items = [];
921
+ const gapIds = [];
922
+ for (let j = 0; j < batchGaps.length; j++) {
923
+ const gap = batchGaps[j];
924
+ const content = fileContents.get(gap.filePath) ?? "";
925
+ const gapId = `${gap.filePath}:${gap.lineStart}`;
926
+ gapIds.push(gapId);
927
+ const codeContext = this.extractContext(content, gap.lineStart, 10);
928
+ const flaggedLine = this.extractLine(content, gap.lineStart);
929
+ items.push(
930
+ SINGLE_ITEM_TEMPLATE.replace("{{id}}", String(j + 1)).replace("{{category}}", gap.categoryName).replace("{{filePath}}", gap.filePath).replace("{{lineNumber}}", String(gap.lineStart)).replace("{{language}}", this.getLanguage(gap.filePath)).replace("{{codeContext}}", codeContext).replace("{{flaggedLine}}", flaggedLine)
931
+ );
932
+ }
933
+ const prompt = BATCH_PROMPT.replace("{{items}}", items.join("\n"));
934
+ batches.push({ prompt, gapIds });
835
935
  }
836
- return { verified, dismissed };
936
+ return batches;
937
+ }
938
+ /**
939
+ * Process batches in parallel with limited concurrency.
940
+ */
941
+ async processParallel(batches, gaps) {
942
+ const results = /* @__PURE__ */ new Map();
943
+ let completed = 0;
944
+ for (let i = 0; i < batches.length; i += this.concurrency) {
945
+ const wave = batches.slice(i, i + this.concurrency);
946
+ const waveResults = await Promise.all(
947
+ wave.map(async (batch) => {
948
+ try {
949
+ const response = await this.callAI(batch.prompt);
950
+ return { gapIds: batch.gapIds, response };
951
+ } catch (error) {
952
+ console.error(`Batch failed: ${error instanceof Error ? error.message : String(error)}`);
953
+ return { gapIds: batch.gapIds, response: null };
954
+ }
955
+ })
956
+ );
957
+ for (const { gapIds, response } of waveResults) {
958
+ if (response) {
959
+ const parsed = this.parseBatchResponse(response);
960
+ for (let j = 0; j < gapIds.length && j < parsed.length; j++) {
961
+ const gapId = gapIds[j];
962
+ const result = parsed[j];
963
+ if (result) {
964
+ results.set(gapId, result);
965
+ }
966
+ }
967
+ }
968
+ }
969
+ completed += wave.length;
970
+ console.log(`Processed ${completed}/${batches.length} batches...`);
971
+ }
972
+ return results;
837
973
  }
838
974
  extractContext(content, lineNumber, radius) {
839
975
  const lines = content.split("\n");
@@ -842,9 +978,13 @@ Respond in JSON format:
842
978
  return lines.slice(start, end).map((line, i) => {
843
979
  const num = start + i + 1;
844
980
  const marker = num === lineNumber ? ">" : " ";
845
- return `${marker} ${num.toString().padStart(4)}| ${line}`;
981
+ return `${marker}${num.toString().padStart(4)}| ${line}`;
846
982
  }).join("\n");
847
983
  }
984
+ extractLine(content, lineNumber) {
985
+ const lines = content.split("\n");
986
+ return lines[lineNumber - 1] ?? "";
987
+ }
848
988
  getLanguage(filePath) {
849
989
  if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
850
990
  if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
@@ -852,21 +992,6 @@ Respond in JSON format:
852
992
  if (filePath.endsWith(".go")) return "go";
853
993
  return "text";
854
994
  }
855
- getCategoryDescription(categoryId) {
856
- const descriptions = {
857
- "sql-injection": "SQL queries built with string concatenation allowing attackers to inject malicious SQL",
858
- "xss": "User input rendered in HTML without sanitization, allowing script injection",
859
- "command-injection": "Shell commands built with user input, allowing arbitrary command execution",
860
- "path-traversal": "File paths built with user input, allowing access to files outside intended directory",
861
- "hardcoded-secrets": "API keys, passwords, or tokens embedded in source code",
862
- "timing-attack": "Non-constant-time comparison of secrets, leaking information via timing",
863
- "memory-bloat": "Unbounded memory growth from accumulating data or inefficient patterns",
864
- "precision-loss": "Floating-point arithmetic for currency causing rounding errors",
865
- "ssrf": "Server-side requests with user-controlled URLs, allowing internal network access",
866
- "deserialization": "Deserializing untrusted data, potentially leading to code execution"
867
- };
868
- return descriptions[categoryId] ?? "Potential security or reliability issue";
869
- }
870
995
  async callAI(prompt) {
871
996
  const apiKey = this.config.apiKey ?? this.getApiKeyFromEnv();
872
997
  if (!apiKey) {
@@ -880,7 +1005,7 @@ Respond in JSON format:
880
1005
  }
881
1006
  async callAnthropic(prompt, apiKey) {
882
1007
  const controller = new AbortController();
883
- const timeout = setTimeout(() => controller.abort(), 3e4);
1008
+ const timeout = setTimeout(() => controller.abort(), 6e4);
884
1009
  try {
885
1010
  const response = await fetch("https://api.anthropic.com/v1/messages", {
886
1011
  method: "POST",
@@ -891,13 +1016,15 @@ Respond in JSON format:
891
1016
  },
892
1017
  body: JSON.stringify({
893
1018
  model: this.config.model ?? "claude-sonnet-4-20250514",
894
- max_tokens: 1024,
1019
+ max_tokens: 4096,
1020
+ // Larger for batch responses
895
1021
  messages: [{ role: "user", content: prompt }]
896
1022
  }),
897
1023
  signal: controller.signal
898
1024
  });
899
1025
  if (!response.ok) {
900
- throw new Error(`Anthropic API error: ${response.status}`);
1026
+ const body = await response.text();
1027
+ throw new Error(`Anthropic API error: ${response.status} - ${body}`);
901
1028
  }
902
1029
  const data = await response.json();
903
1030
  return data.content[0]?.text ?? "";
@@ -907,7 +1034,7 @@ Respond in JSON format:
907
1034
  }
908
1035
  async callOpenAI(prompt, apiKey) {
909
1036
  const controller = new AbortController();
910
- const timeout = setTimeout(() => controller.abort(), 3e4);
1037
+ const timeout = setTimeout(() => controller.abort(), 6e4);
911
1038
  try {
912
1039
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
913
1040
  method: "POST",
@@ -918,12 +1045,13 @@ Respond in JSON format:
918
1045
  body: JSON.stringify({
919
1046
  model: this.config.model ?? "gpt-4o",
920
1047
  messages: [{ role: "user", content: prompt }],
921
- max_tokens: 1024
1048
+ max_tokens: 4096
922
1049
  }),
923
1050
  signal: controller.signal
924
1051
  });
925
1052
  if (!response.ok) {
926
- throw new Error(`OpenAI API error: ${response.status}`);
1053
+ const body = await response.text();
1054
+ throw new Error(`OpenAI API error: ${response.status} - ${body}`);
927
1055
  }
928
1056
  const data = await response.json();
929
1057
  return data.choices[0]?.message?.content ?? "";
@@ -937,31 +1065,49 @@ Respond in JSON format:
937
1065
  }
938
1066
  return process.env["OPENAI_API_KEY"] ?? "";
939
1067
  }
940
- parseResponse(response) {
1068
+ parseBatchResponse(response) {
941
1069
  try {
942
- const jsonMatch = response.match(/\{[\s\S]*\}/);
1070
+ const jsonMatch = response.match(/\[[\s\S]*\]/);
943
1071
  if (!jsonMatch) {
944
- throw new Error("No JSON found in response");
1072
+ console.error("No JSON array found in batch response");
1073
+ return [];
945
1074
  }
946
1075
  const parsed = JSON.parse(jsonMatch[0]);
947
- return {
948
- isVulnerable: Boolean(parsed.isVulnerable),
949
- confidence: parsed.confidence ?? "medium",
950
- reasoning: parsed.reasoning ?? "No reasoning provided",
951
- mitigatingFactors: parsed.mitigatingFactors ?? [],
952
- exploitScenario: parsed.exploitScenario ?? null,
953
- recommendation: parsed.recommendation ?? "Review this code manually"
954
- };
955
- } catch {
1076
+ return parsed.map((item) => ({
1077
+ id: String(item.id),
1078
+ isVulnerable: Boolean(item.isVulnerable),
1079
+ confidence: item.confidence ?? "medium",
1080
+ reasoning: item.reasoning ?? "No reasoning provided"
1081
+ }));
1082
+ } catch (error) {
1083
+ console.error(`Failed to parse batch response: ${error instanceof Error ? error.message : String(error)}`);
1084
+ return [];
1085
+ }
1086
+ }
1087
+ /**
1088
+ * Legacy single-gap verification (kept for backwards compatibility).
1089
+ */
1090
+ async verify(gap, fileContent) {
1091
+ const result = await this.verifyAll([gap], async () => fileContent);
1092
+ if (result.verified.length > 0) {
956
1093
  return {
957
1094
  isVulnerable: true,
958
- confidence: "low",
959
- reasoning: "AI analysis failed to parse, flagging for manual review",
1095
+ confidence: "high",
1096
+ reasoning: "AI confirmed vulnerability",
960
1097
  mitigatingFactors: [],
961
1098
  exploitScenario: null,
962
- recommendation: "Manual review required"
1099
+ recommendation: "Fix this issue"
963
1100
  };
964
1101
  }
1102
+ const dismissal = result.dismissed[0];
1103
+ return {
1104
+ isVulnerable: false,
1105
+ confidence: "high",
1106
+ reasoning: dismissal?.reason ?? "AI dismissed as false positive",
1107
+ mitigatingFactors: [],
1108
+ exploitScenario: null,
1109
+ recommendation: "No action needed"
1110
+ };
965
1111
  }
966
1112
  };
967
1113
  }
@@ -1327,15 +1473,10 @@ __export(config_exports, {
1327
1473
  validateApiKey: () => validateApiKey
1328
1474
  });
1329
1475
  function ensureConfigDir() {
1330
- if (!existsSync(CONFIG_DIR)) {
1331
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1332
- }
1476
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1333
1477
  }
1334
1478
  function loadConfig() {
1335
1479
  try {
1336
- if (!existsSync(CONFIG_FILE)) {
1337
- return {};
1338
- }
1339
1480
  const content = readFileSync(CONFIG_FILE, "utf-8");
1340
1481
  const parsed = JSON.parse(content);
1341
1482
  const result = ConfigSchema.safeParse(parsed);
@@ -2896,7 +3037,12 @@ var Scanner = class {
2896
3037
  const startedAt = /* @__PURE__ */ new Date();
2897
3038
  const opts = this.mergeOptions(targetDirectory, options);
2898
3039
  this.log.info(`Starting scan of ${targetDirectory}`);
2899
- if (!existsSync(targetDirectory)) {
3040
+ try {
3041
+ const dirStat = await stat(targetDirectory);
3042
+ if (!dirStat.isDirectory()) {
3043
+ return err(new AnalysisError(`Not a directory: ${targetDirectory}`));
3044
+ }
3045
+ } catch {
2900
3046
  return err(new AnalysisError(`Directory not found: ${targetDirectory}`));
2901
3047
  }
2902
3048
  const categoriesResult = this.getCategoriesToScan(opts);
@@ -3092,9 +3238,6 @@ var Scanner = class {
3092
3238
  */
3093
3239
  readPinataIgnore(targetDirectory) {
3094
3240
  const ignorePath = resolve(targetDirectory, ".pinataignore");
3095
- if (!existsSync(ignorePath)) {
3096
- return [];
3097
- }
3098
3241
  try {
3099
3242
  const { readFileSync: readFileSync2 } = __require("fs");
3100
3243
  const content = readFileSync2(ignorePath, "utf-8");
@@ -3433,7 +3576,7 @@ function createScanner(categoryStore) {
3433
3576
  init_types();
3434
3577
 
3435
3578
  // src/core/index.ts
3436
- var VERSION = "0.2.0";
3579
+ var VERSION = "0.2.1";
3437
3580
 
3438
3581
  // src/lib/index.ts
3439
3582
  init_errors();
@@ -4354,11 +4497,11 @@ var AIService = class {
4354
4497
  */
4355
4498
  getApiKeyFromConfig(provider) {
4356
4499
  try {
4357
- const { existsSync: existsSync7, readFileSync: readFileSync2 } = __require("fs");
4500
+ const { existsSync: existsSync4, readFileSync: readFileSync2 } = __require("fs");
4358
4501
  const { homedir: homedir2 } = __require("os");
4359
4502
  const { join: join3 } = __require("path");
4360
4503
  const configPath = join3(homedir2(), ".pinata", "config.json");
4361
- if (!existsSync7(configPath)) {
4504
+ if (!existsSync4(configPath)) {
4362
4505
  return "";
4363
4506
  }
4364
4507
  const content = readFileSync2(configPath, "utf-8");
@@ -5005,13 +5148,13 @@ async function writeGeneratedTests(tests, basePath, outputDir) {
5005
5148
  for (const [outputPath, fileTests] of testsByFile) {
5006
5149
  try {
5007
5150
  const dir = dirname(outputPath);
5008
- if (!existsSync(dir)) {
5009
- await mkdir(dir, { recursive: true });
5010
- }
5011
- const fileExists = existsSync(outputPath);
5151
+ await mkdir(dir, { recursive: true });
5012
5152
  let existingContent = "";
5013
- if (fileExists) {
5153
+ let fileExists = false;
5154
+ try {
5014
5155
  existingContent = await readFile(outputPath, "utf-8");
5156
+ fileExists = true;
5157
+ } catch {
5015
5158
  }
5016
5159
  const contentParts = [];
5017
5160
  const allImports = /* @__PURE__ */ new Set();
@@ -5836,7 +5979,7 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
5836
5979
  const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
5837
5980
  const { readFile: readFile5 } = await import('fs/promises');
5838
5981
  const verifier = new AIVerifier2({ provider: "anthropic" });
5839
- const { verified, dismissed } = await verifier.verifyAll(
5982
+ const { verified, dismissed, stats } = await verifier.verifyAll(
5840
5983
  scanResult.data.gaps,
5841
5984
  async (path2) => readFile5(path2, "utf-8")
5842
5985
  );
@@ -5850,7 +5993,9 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
5850
5993
  const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
5851
5994
  scanResult.data.score.overall = newOverall;
5852
5995
  scanResult.data.score.grade = newGrade;
5853
- verifySpinner?.succeed(`Verified: ${verified.length} real issues, ${dismissed.length} false positives dismissed`);
5996
+ verifySpinner?.succeed(
5997
+ `AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
5998
+ );
5854
5999
  if (isVerbose && dismissed.length > 0) {
5855
6000
  console.log(chalk5.gray("\nDismissed as false positives:"));
5856
6001
  for (const { gap, reason } of dismissed.slice(0, 5)) {