jstar-reviewer 2.1.3 → 2.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.
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Logger = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ /**
9
+ * Logger Utility
10
+ * Centralizes all CLI output to support both human-readable (TTY) and machine-readable (JSON) modes.
11
+ *
12
+ * Usage:
13
+ * Logger.init(); // Auto-detects --json flag
14
+ * Logger.info("Starting..."); // Suppressed in JSON mode
15
+ * Logger.json({ status: "ok" }); // Only outputs in JSON mode
16
+ */
17
+ let jsonMode = false;
18
+ exports.Logger = {
19
+ /**
20
+ * Initialize the logger.
21
+ * Auto-detects --json or --headless flags from process.argv.
22
+ */
23
+ init() {
24
+ jsonMode = process.argv.includes('--json') || process.argv.includes('--headless');
25
+ },
26
+ /**
27
+ * Check if we are in headless/JSON mode.
28
+ */
29
+ isHeadless() {
30
+ return jsonMode;
31
+ },
32
+ /**
33
+ * Alias for isHeadless for backwards compatibility.
34
+ */
35
+ isJsonMode() {
36
+ return jsonMode;
37
+ },
38
+ /**
39
+ * Standard informational message (suppressed in JSON mode).
40
+ */
41
+ info(message) {
42
+ if (!jsonMode) {
43
+ console.log(message);
44
+ }
45
+ },
46
+ /**
47
+ * Success message with green styling (suppressed in JSON mode).
48
+ */
49
+ success(message) {
50
+ if (!jsonMode) {
51
+ console.log(chalk_1.default.green(message));
52
+ }
53
+ },
54
+ /**
55
+ * Warning message with yellow styling (suppressed in JSON mode).
56
+ */
57
+ warn(message) {
58
+ if (!jsonMode) {
59
+ console.log(chalk_1.default.yellow(message));
60
+ }
61
+ },
62
+ /**
63
+ * Error message - always outputs to stderr.
64
+ */
65
+ error(message) {
66
+ console.error(chalk_1.default.red(message));
67
+ },
68
+ /**
69
+ * Dim/faded message for secondary info (suppressed in JSON mode).
70
+ */
71
+ dim(message) {
72
+ if (!jsonMode) {
73
+ console.log(chalk_1.default.dim(message));
74
+ }
75
+ },
76
+ /**
77
+ * Write inline (no newline) for progress indicators (suppressed in JSON mode).
78
+ * Alias: progress()
79
+ */
80
+ inline(message) {
81
+ if (!jsonMode) {
82
+ process.stdout.write(message);
83
+ }
84
+ },
85
+ /**
86
+ * Progress indicator - writes inline without newline.
87
+ */
88
+ progress(message) {
89
+ if (!jsonMode) {
90
+ process.stdout.write(message);
91
+ }
92
+ },
93
+ /**
94
+ * Output structured JSON to stdout.
95
+ * Only outputs in JSON mode. For API/AI consumption.
96
+ */
97
+ json(data) {
98
+ if (jsonMode) {
99
+ console.log(JSON.stringify(data, null, 2));
100
+ }
101
+ },
102
+ /**
103
+ * Output a single-line JSON object (for streaming events).
104
+ */
105
+ jsonLine(data) {
106
+ if (jsonMode) {
107
+ console.log(JSON.stringify(data));
108
+ }
109
+ }
110
+ };
package/package.json CHANGED
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "jstar-reviewer",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "Local-First, Context-Aware AI Code Reviewer - Works with any language",
5
5
  "bin": {
6
6
  "jstar": "bin/jstar.js"
7
7
  },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "index:init": "ts-node scripts/indexer.ts --init",
11
+ "index:watch": "ts-node scripts/indexer.ts --watch",
12
+ "review": "ts-node scripts/reviewer.ts",
13
+ "chat": "ts-node scripts/chat.ts",
14
+ "detect": "ts-node scripts/detective.ts",
15
+ "prepare": "husky install"
16
+ },
8
17
  "keywords": [
9
18
  "code-review",
10
19
  "ai",
@@ -29,13 +38,15 @@
29
38
  "chalk": "^4.1.2",
30
39
  "dotenv": "^16.3.1",
31
40
  "llamaindex": "^0.1.0",
41
+ "prompts": "^2.4.2",
32
42
  "simple-git": "^3.20.0"
33
43
  },
34
44
  "devDependencies": {
35
45
  "@types/node": "^20.0.0",
46
+ "@types/prompts": "^2.4.9",
47
+ "husky": "^8.0.3",
36
48
  "ts-node": "^10.9.1",
37
- "typescript": "^5.2.2",
38
- "husky": "^8.0.3"
49
+ "typescript": "^5.2.2"
39
50
  },
40
51
  "engines": {
41
52
  "node": ">=18"
@@ -54,12 +65,5 @@
54
65
  "type": "commonjs",
55
66
  "bugs": {
56
67
  "url": "https://github.com/JStaRFilms/jstar-code-review/issues"
57
- },
58
- "scripts": {
59
- "build": "tsc",
60
- "index:init": "ts-node scripts/indexer.ts --init",
61
- "index:watch": "ts-node scripts/indexer.ts --watch",
62
- "review": "ts-node scripts/reviewer.ts",
63
- "detect": "ts-node scripts/detective.ts"
64
68
  }
65
69
  }
@@ -0,0 +1,130 @@
1
+ import { startInteractiveSession, startHeadlessSession } from "./session";
2
+ import {
3
+ VectorStoreIndex,
4
+ storageContextFromDefaults,
5
+ serviceContextFromDefaults
6
+ } from "llamaindex";
7
+ import { GeminiEmbedding } from "./gemini-embedding";
8
+ import { MockLLM } from "./mock-llm";
9
+ import { Logger } from "./utils/logger";
10
+ import * as path from "path";
11
+ import * as fs from "fs";
12
+ import chalk from "chalk";
13
+ import { SessionState, DashboardReport } from "./types";
14
+ import { renderDashboard, determineStatus, generateRecommendation } from "./dashboard";
15
+
16
+ const STORAGE_DIR = path.join(process.cwd(), ".jstar", "storage");
17
+ const SESSION_FILE = path.join(process.cwd(), ".jstar", "session.json");
18
+ const OUTPUT_FILE = path.join(process.cwd(), ".jstar", "last-review.md");
19
+
20
+ const embedModel = new GeminiEmbedding();
21
+ const llm = new MockLLM();
22
+ const serviceContext = serviceContextFromDefaults({ embedModel, llm: llm as any });
23
+
24
+ async function loadSession(): Promise<SessionState | null> {
25
+ try {
26
+ const content = fs.readFileSync(SESSION_FILE, 'utf-8');
27
+ return JSON.parse(content);
28
+ } catch (e: any) {
29
+ if (e.code === 'ENOENT') {
30
+ return null; // File doesn't exist
31
+ }
32
+ Logger.error(`Failed to load session: ${e.message}`);
33
+ return null;
34
+ }
35
+ }
36
+
37
+ async function main() {
38
+ // Initialize logger mode
39
+ Logger.init();
40
+
41
+ Logger.info(chalk.bold.magenta("\n💬 J-Star Chat: Resuming Session...\n"));
42
+
43
+ // 1. Load Session
44
+ const session = await loadSession();
45
+ if (!session) {
46
+ Logger.error(chalk.red("❌ No active session found."));
47
+ Logger.info(chalk.yellow("Run 'jstar review' first to analyze the codebase."));
48
+ return;
49
+ }
50
+
51
+ Logger.info(chalk.dim(` 📅 Loaded session from: ${session.date}`));
52
+ Logger.info(chalk.dim(` 🔍 Loaded ${session.findings.reduce((acc, f) => acc + f.issues.length, 0)} issues.`));
53
+
54
+ // 2. Load Brain (Fast)
55
+ if (!fs.existsSync(STORAGE_DIR)) {
56
+ Logger.error(chalk.red("❌ Local Brain not found. Run 'pnpm index:init' first."));
57
+ return;
58
+ }
59
+ const storageContext = await storageContextFromDefaults({ persistDir: STORAGE_DIR });
60
+ const index = await VectorStoreIndex.init({ storageContext, serviceContext });
61
+
62
+ // 3. Start Chat (Headless or Interactive)
63
+ let updatedFindings;
64
+ let hasUpdates;
65
+
66
+ if (Logger.isHeadless()) {
67
+ // Headless mode: stdin/stdout JSON protocol
68
+ const result = await startHeadlessSession(session.findings, index);
69
+ updatedFindings = result.updatedFindings;
70
+ hasUpdates = result.hasUpdates;
71
+ } else {
72
+ // Normal TUI mode
73
+ const result = await startInteractiveSession(session.findings, index);
74
+ updatedFindings = result.updatedFindings;
75
+ hasUpdates = result.hasUpdates;
76
+ }
77
+
78
+ // 4. Update Session & Report if changed
79
+ if (hasUpdates) {
80
+ Logger.info(chalk.blue("\n🔄 Updating Session & Dashboard..."));
81
+
82
+ // Recalculate metrics based on new findings
83
+ const newMetrics = {
84
+ ...session.metrics, // keep files/tokens same
85
+ violations: updatedFindings.reduce((sum, f) => sum + f.issues.length, 0),
86
+ critical: updatedFindings.filter(f => f.severity === 'P0_CRITICAL').length,
87
+ high: updatedFindings.filter(f => f.severity === 'P1_HIGH').length,
88
+ medium: updatedFindings.filter(f => f.severity === 'P2_MEDIUM').length,
89
+ lgtm: updatedFindings.filter(f => f.severity === 'LGTM').length,
90
+ };
91
+
92
+ // Save Session
93
+ const newSession: SessionState = {
94
+ date: new Date().toISOString().split('T')[0],
95
+ findings: updatedFindings,
96
+ metrics: newMetrics
97
+ };
98
+
99
+ try {
100
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(newSession, null, 2));
101
+ } catch (err: any) {
102
+ Logger.error(`Failed to save session: ${err.message}`);
103
+ return;
104
+ }
105
+
106
+ // Save Dashboard
107
+ const report: DashboardReport = {
108
+ date: newSession.date,
109
+ reviewer: 'J-Star Chat',
110
+ status: determineStatus(newMetrics),
111
+ metrics: newMetrics,
112
+ findings: updatedFindings,
113
+ recommendedAction: generateRecommendation(newMetrics)
114
+ };
115
+ const dashboard = renderDashboard(report);
116
+
117
+ try {
118
+ fs.writeFileSync(OUTPUT_FILE, dashboard);
119
+ } catch (err: any) {
120
+ Logger.error(`Failed to save dashboard: ${err.message}`);
121
+ return;
122
+ }
123
+
124
+ Logger.info(chalk.bold.green("✅ Saved."));
125
+ } else {
126
+ Logger.info(chalk.dim(" No changes made."));
127
+ }
128
+ }
129
+
130
+ main().catch(console.error);
package/scripts/config.ts CHANGED
@@ -5,7 +5,7 @@ import { Severity } from "./types";
5
5
 
6
6
  // --- Auto-Setup Logic ---
7
7
  const REQUIRED_ENV_VARS = {
8
- 'GOOGLE_API_KEY': '# Required: Google API key for Gemini embeddings\nGOOGLE_API_KEY=your_google_api_key_here',
8
+ 'GEMINI_API_KEY': '# Required: Gemini API key (or GOOGLE_API_KEY)\nGEMINI_API_KEY=your_gemini_api_key_here',
9
9
  'GROQ_API_KEY': '# Required: Groq API key for LLM reviews\nGROQ_API_KEY=your_groq_api_key_here',
10
10
  'REVIEW_MODEL_NAME': '# Optional: Override the default model\n# REVIEW_MODEL_NAME=moonshotai/kimi-k2-instruct-0905'
11
11
  };
@@ -55,9 +55,13 @@ const DEFAULT_MODEL = "moonshotai/kimi-k2-instruct-0905";
55
55
 
56
56
  export const Config = {
57
57
  MODEL_NAME: process.env.REVIEW_MODEL_NAME || DEFAULT_MODEL,
58
+ CRITIQUE_MODEL_NAME: process.env.CRITIQUE_MODEL_NAME || process.env.REVIEW_MODEL_NAME || DEFAULT_MODEL,
58
59
  DEFAULT_SEVERITY: 'P2_MEDIUM' as Severity,
59
60
  THRESHOLDS: {
60
61
  MEDIUM: 5
61
- }
62
+ },
63
+ // Smart Review Settings
64
+ CONFIDENCE_THRESHOLD: 3, // Minimum confidence (1-5) to include an issue
65
+ ENABLE_SELF_CRITIQUE: true, // Enable second-pass validation
62
66
  };
63
67
 
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Self-Critique Module
3
+ * Second-pass validation to filter out false positives
4
+ */
5
+
6
+ import { generateText } from "ai";
7
+ import { createGroq } from "@ai-sdk/groq";
8
+ import chalk from "chalk";
9
+ import { Logger } from "../utils/logger";
10
+ import { Config } from "../config";
11
+ import { FileFinding, ReviewIssue } from "../types";
12
+
13
+ const groq = createGroq({ apiKey: process.env.GROQ_API_KEY });
14
+
15
+ interface CritiqueResult {
16
+ issueTitle: string;
17
+ verdict: 'VALID' | 'FALSE_POSITIVE' | 'NEEDS_CONTEXT';
18
+ reason: string;
19
+ }
20
+
21
+ interface CritiqueResponse {
22
+ results: CritiqueResult[];
23
+ }
24
+
25
+ /**
26
+ * Runs a self-critique pass on the initial findings.
27
+ * Returns filtered findings with only validated issues.
28
+ */
29
+ export async function critiqueFindings(
30
+ findings: FileFinding[],
31
+ diff: string
32
+ ): Promise<FileFinding[]> {
33
+ // Collect all issues across all files for batch critique
34
+ const allIssues: { file: string; issue: ReviewIssue; index: number }[] = [];
35
+ findings.forEach((f, fIdx) => {
36
+ f.issues.forEach((issue, iIdx) => {
37
+ allIssues.push({ file: f.file, issue, index: fIdx * 1000 + iIdx });
38
+ });
39
+ });
40
+
41
+ if (allIssues.length === 0) {
42
+ return findings;
43
+ }
44
+
45
+ Logger.info(chalk.blue("\n🔍 Self-Critique Pass: Validating findings...\n"));
46
+
47
+ // Build the critique prompt
48
+ const issueList = allIssues.map((item, idx) =>
49
+ `[${idx}] File: ${item.file}\n Title: ${item.issue.title}\n Description: ${item.issue.description}`
50
+ ).join("\n\n");
51
+
52
+ const systemPrompt = `You are a code review validator. Your job is to filter out FALSE POSITIVES.
53
+
54
+ For each issue below, decide:
55
+ - VALID: This is a real problem that should be reported
56
+ - FALSE_POSITIVE: This is NOT a real issue (test mock, intentional pattern, already handled, etc.)
57
+ - NEEDS_CONTEXT: Can't determine without more code context (treat as valid)
58
+
59
+ Return JSON with this structure:
60
+ {
61
+ "results": [
62
+ { "issueTitle": "...", "verdict": "VALID" | "FALSE_POSITIVE" | "NEEDS_CONTEXT", "reason": "..." }
63
+ ]
64
+ }
65
+
66
+ IMPORTANT:
67
+ - Be SKEPTICAL. If the code looks intentional, it's probably not a bug.
68
+ - Test files, mocks, and stubs are NOT bugs.
69
+ - "Missing error handling" in utility modules may be intentional.
70
+ - Return ONLY valid JSON.`;
71
+
72
+ const userPrompt = `ORIGINAL DIFF:
73
+ \`\`\`
74
+ ${diff.slice(0, 4000)}
75
+ \`\`\`
76
+
77
+ ISSUES TO VALIDATE:
78
+ ${issueList}`;
79
+
80
+ try {
81
+ const { text } = await generateText({
82
+ model: groq(Config.CRITIQUE_MODEL_NAME),
83
+ system: systemPrompt,
84
+ prompt: userPrompt,
85
+ temperature: 0.1,
86
+ });
87
+
88
+ // Parse critique response
89
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
90
+ if (!jsonMatch) {
91
+ Logger.warn(chalk.yellow(" ⚠️ Could not parse critique response, keeping all issues"));
92
+ return findings;
93
+ }
94
+
95
+ let response: CritiqueResponse;
96
+ try {
97
+ const parsed = JSON.parse(jsonMatch[0]);
98
+ // Validate structure
99
+ if (!parsed || !Array.isArray(parsed.results)) {
100
+ throw new Error("Invalid response structure");
101
+ }
102
+ response = parsed;
103
+ } catch (parseError) {
104
+ Logger.warn(chalk.yellow(" ⚠️ Invalid JSON from critique, keeping all issues"));
105
+ return findings;
106
+ }
107
+
108
+ // Build a map of verdicts by title
109
+ const verdictMap = new Map<string, CritiqueResult>();
110
+ for (const result of response.results) {
111
+ verdictMap.set(result.issueTitle.toLowerCase(), result);
112
+ }
113
+
114
+ // Filter findings
115
+ const filteredFindings: FileFinding[] = [];
116
+ let removedCount = 0;
117
+
118
+ for (const finding of findings) {
119
+ const validIssues: ReviewIssue[] = [];
120
+
121
+ for (const issue of finding.issues) {
122
+ const verdict = verdictMap.get(issue.title.toLowerCase());
123
+
124
+ if (verdict?.verdict === 'FALSE_POSITIVE') {
125
+ removedCount++;
126
+ Logger.info(chalk.dim(` ❌ Removed: "${issue.title}" (${verdict.reason})`));
127
+ } else {
128
+ validIssues.push(issue);
129
+ if (verdict?.verdict === 'VALID') {
130
+ Logger.info(chalk.green(` ✓ Kept: "${issue.title}"`));
131
+ }
132
+ }
133
+ }
134
+
135
+ if (validIssues.length > 0) {
136
+ filteredFindings.push({
137
+ ...finding,
138
+ issues: validIssues,
139
+ // Upgrade to LGTM if no issues remain? No, keep severity for record
140
+ });
141
+ } else if (finding.issues.length > 0) {
142
+ // All issues were false positives - mark as LGTM
143
+ filteredFindings.push({
144
+ ...finding,
145
+ severity: 'LGTM',
146
+ issues: []
147
+ });
148
+ } else {
149
+ filteredFindings.push(finding);
150
+ }
151
+ }
152
+
153
+ Logger.info(chalk.blue(`\n 📊 Self-Critique: ${removedCount} false positive(s) removed\n`));
154
+
155
+ return filteredFindings;
156
+
157
+ } catch (error: any) {
158
+ Logger.warn(chalk.yellow(` ⚠️ Self-critique failed: ${error.message.slice(0, 100)}`));
159
+ Logger.warn(chalk.yellow(" Keeping all issues as fallback."));
160
+ return findings;
161
+ }
162
+ }
@@ -0,0 +1,111 @@
1
+ import { VectorStoreIndex, MetadataMode } from "llamaindex";
2
+ import { generateText } from "ai";
3
+ import { createGroq } from "@ai-sdk/groq";
4
+ import { Config } from "../config";
5
+ import chalk from "chalk";
6
+
7
+ const groq = createGroq({ apiKey: process.env.GROQ_API_KEY });
8
+
9
+ export interface DebateResult {
10
+ text: string;
11
+ severity: 'P0_CRITICAL' | 'P1_HIGH' | 'P2_MEDIUM' | 'LGTM' | 'UNCHANGED';
12
+ }
13
+
14
+ export async function debateIssue(
15
+ issueTitle: string,
16
+ issueDescription: string,
17
+ fileName: string,
18
+ userArgument: string,
19
+ index: VectorStoreIndex
20
+ ): Promise<DebateResult> {
21
+
22
+ // Validate API key before making any calls
23
+ if (!process.env.GROQ_API_KEY) {
24
+ throw new Error("GROQ_API_KEY is required for debate mode. Please set it in your .env.local file.");
25
+ }
26
+
27
+ console.log(chalk.dim(" 🧠 Thinking... (Consulting the Brain)"));
28
+
29
+ // 1. Extract keywords/context
30
+ const query = `${userArgument} ${issueTitle}`;
31
+
32
+ // 2. Retrieve new context
33
+ const retriever = index.asRetriever({ similarityTopK: 2 });
34
+ const contextNodes = await retriever.retrieve(query);
35
+ const newContext = contextNodes.map(n => n.node.getContent(MetadataMode.NONE)).join("\n\n").slice(0, 2000);
36
+
37
+ if (newContext.length >= 2000) {
38
+ console.log(chalk.yellow(" ⚠️ Context truncated to 2000 chars"));
39
+ }
40
+
41
+ const sources = contextNodes.map(n => n.node.metadata?.['file_name']).filter(Boolean).join(', ');
42
+ if (sources) {
43
+ console.log(chalk.dim(` 🔍 Found relevant context from: ${sources}`));
44
+ }
45
+
46
+ // 3. Ask the Judge
47
+ const systemPrompt = `You are a Senior Code Reviewer in a debate with a developer.
48
+
49
+ ORIGINAL FINDING: "${issueTitle} - ${issueDescription}" in file ${fileName}.
50
+ USER DEFENSE: "${userArgument}"
51
+
52
+ NEW CONTEXT FOUND IN REPO:
53
+ ${newContext}
54
+
55
+ TASK:
56
+ Analyze the USER INPUT.
57
+
58
+ 1. **IS IT A QUESTION?** (e.g., "What does this mean?", "Why is this wrong?")
59
+ - If yes, **EXPLAIN** the technical reasoning behind the finding.
60
+ - Reference the specific code/context.
61
+ - Do NOT withdraw the issue (Severity: UNCHANGED).
62
+ - Tone: Educational and helpful.
63
+
64
+ 2. **IS IT A DEFENSE/ARGUMENT?** (e.g., "This is handled in utils.ts", "It's a false positive because...")
65
+ - Evaluate if the user is correct based on the NEW CONTEXT.
66
+ - If user is RIGHT: Apologize and withdraw (Severity: LGTM).
67
+ - If user is WRONG: Explain why, citing the context. (Severity: UNCHANGED).
68
+
69
+ RETURN JSON:
70
+ {
71
+ "response": "Conversational response (explanation or verdict).",
72
+ "severity": "P0_CRITICAL" | "P1_HIGH" | "P2_MEDIUM" | "LGTM" | "UNCHANGED"
73
+ }
74
+ `;
75
+
76
+ try {
77
+ const { text } = await generateText({
78
+ model: groq(Config.MODEL_NAME),
79
+ system: systemPrompt,
80
+ prompt: "What is your verdict?",
81
+ temperature: 0.2,
82
+ });
83
+
84
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
85
+ if (jsonMatch) {
86
+ try {
87
+ const parsed = JSON.parse(jsonMatch[0]);
88
+ // Validate expected structure
89
+ if (parsed && typeof parsed.response === 'string' && parsed.severity) {
90
+ return {
91
+ text: parsed.response,
92
+ severity: parsed.severity
93
+ };
94
+ }
95
+ } catch (parseError) {
96
+ // JSON parse failed, fall through to default response
97
+ }
98
+ }
99
+
100
+ return {
101
+ text: text,
102
+ severity: 'UNCHANGED'
103
+ };
104
+
105
+ } catch (error: any) {
106
+ return {
107
+ text: `Failed to debate: ${error.message}`,
108
+ severity: 'UNCHANGED'
109
+ };
110
+ }
111
+ }
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import chalk from 'chalk';
4
+ import { Logger } from './utils/logger';
4
5
 
5
6
  interface Violation {
6
7
  file: string;
@@ -109,22 +110,22 @@ export class Detective {
109
110
 
110
111
  report() {
111
112
  if (this.violations.length === 0) {
112
- console.log(chalk.green("✅ Detective Engine: No violations found."));
113
+ Logger.info(chalk.green("✅ Detective Engine: No violations found."));
113
114
  return;
114
115
  }
115
116
 
116
- console.log(chalk.red(`🚨 Detective Engine found ${this.violations.length} violations:`));
117
+ Logger.info(chalk.red(`🚨 Detective Engine found ${this.violations.length} violations:`));
117
118
  // Only show first 10 to avoid wall of text
118
119
  const total = this.violations.length;
119
120
  const toShow = this.violations.slice(0, 10);
120
121
 
121
122
  toShow.forEach(v => {
122
123
  const color = v.severity === 'high' ? chalk.red : chalk.yellow;
123
- console.log(color(`[${v.code}] ${v.file}:${v.line} - ${v.message}`));
124
+ Logger.info(color(`[${v.code}] ${v.file}:${v.line} - ${v.message}`));
124
125
  });
125
126
 
126
127
  if (total > 10) {
127
- console.log(chalk.dim(`... and ${total - 10} more.`));
128
+ Logger.dim(`... and ${total - 10} more.`);
128
129
  }
129
130
  }
130
131
  }
@@ -5,9 +5,9 @@ export class GeminiEmbedding {
5
5
  private model: any;
6
6
 
7
7
  constructor() {
8
- const apiKey = process.env.GOOGLE_API_KEY;
8
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
9
9
  if (!apiKey) {
10
- throw new Error("GOOGLE_API_KEY is missing from environment variables.");
10
+ throw new Error("GEMINI_API_KEY is missing from environment variables.");
11
11
  }
12
12
  this.genAI = new GoogleGenerativeAI(apiKey);
13
13
  // User requested 'text-embedding-004', which has better rate limits
@@ -50,8 +50,9 @@ function getSourceDir(): string {
50
50
 
51
51
  async function main() {
52
52
  // 0. Environment Validation
53
- if (!process.env.GOOGLE_API_KEY) {
54
- console.error(chalk.red("❌ Missing GOOGLE_API_KEY!"));
53
+ const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
54
+ if (!geminiKey) {
55
+ console.error(chalk.red("❌ Missing GEMINI_API_KEY (or GOOGLE_API_KEY)!"));
55
56
  console.log(chalk.yellow("\nPlease ensure you have a .env.local file. Check .env.example for a template.\n"));
56
57
  process.exit(1);
57
58
  }
@@ -126,7 +127,7 @@ async function main() {
126
127
  } catch (e: any) {
127
128
  console.error(chalk.red("❌ Indexing Failed:"), e.message);
128
129
  if (e.message.includes("API") || e.message.includes("key")) {
129
- console.log(chalk.yellow("👉 Tip: Make sure you have GOOGLE_API_KEY in your .env.local file."));
130
+ console.log(chalk.yellow("👉 Tip: Make sure you have GEMINI_API_KEY in your .env.local file."));
130
131
  }
131
132
  process.exit(1);
132
133
  }