jstar-reviewer 2.4.0 → 2.4.2

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/bin/jstar.js CHANGED
@@ -29,7 +29,7 @@ function log(msg) {
29
29
 
30
30
  function printHelp() {
31
31
  log(`
32
- ${COLORS.bold}🌟 J-Star Reviewer v2.4.0${COLORS.reset}
32
+ ${COLORS.bold}🌟 J-Star Reviewer v2.4.2${COLORS.reset}
33
33
 
34
34
  ${COLORS.dim}AI-powered code review with local embeddings${COLORS.reset}
35
35
 
@@ -40,20 +40,34 @@ ${COLORS.bold}COMMANDS:${COLORS.reset}
40
40
  ${COLORS.green}init${COLORS.reset} Index the current codebase (build the brain)
41
41
  ${COLORS.green}review${COLORS.reset} Review staged git changes
42
42
  ${COLORS.green}chat${COLORS.reset} Resume an interactive session from the last review
43
+ ${COLORS.green}detect${COLORS.reset} Run static analysis (Detective Engine)
43
44
  ${COLORS.green}setup${COLORS.reset} Create .env.example and .jstar/ in current directory
44
45
 
45
46
  ${COLORS.bold}OPTIONS:${COLORS.reset}
46
47
  ${COLORS.yellow}--json${COLORS.reset} Output machine-readable JSON (for CI/CD)
47
48
  ${COLORS.yellow}--headless${COLORS.reset} Enable stdin/stdout protocol (for AI agents)
48
49
 
50
+ ${COLORS.bold}REVIEW OPTIONS:${COLORS.reset}
51
+ ${COLORS.yellow}--pr${COLORS.reset} Review a Pull Request (compare against main/base)
52
+ ${COLORS.yellow}--base <br>${COLORS.reset} Specify base branch for PR review (default: main)
53
+ ${COLORS.yellow}--last${COLORS.reset} Review the very last commit (HEAD~1..HEAD)
54
+ ${COLORS.yellow}--commit <h>${COLORS.reset} Review a specific commit hash
55
+ ${COLORS.yellow}--range <a> <b>${COLORS.reset} Review diff between two refs
56
+
49
57
  ${COLORS.bold}EXAMPLES:${COLORS.reset}
50
58
  ${COLORS.dim}# First time setup${COLORS.reset}
51
59
  jstar init
52
60
 
53
- ${COLORS.dim}# Review staged changes${COLORS.reset}
54
- git add .
61
+ ${COLORS.dim}# Review staged changes (default)${COLORS.reset}
55
62
  jstar review
56
63
 
64
+ ${COLORS.dim}# Review a Pull Request${COLORS.reset}
65
+ jstar review --pr
66
+ jstar review --pr --base develop
67
+
68
+ ${COLORS.dim}# Review the last commit${COLORS.reset}
69
+ jstar review --last
70
+
57
71
  ${COLORS.dim}# JSON output for CI${COLORS.reset}
58
72
  jstar review --json > report.json
59
73
 
@@ -225,6 +239,10 @@ switch (command) {
225
239
  case 'chat':
226
240
  runScript('chat.ts');
227
241
  break;
242
+ case 'detect':
243
+ case 'det':
244
+ runScript('detective.ts');
245
+ break;
228
246
  case 'setup':
229
247
  createSetupFiles();
230
248
  break;
@@ -58,7 +58,11 @@ async function loadSession() {
58
58
  }
59
59
  catch (e) {
60
60
  if (e.code === 'ENOENT') {
61
- return null; // File doesn't exist
61
+ return null; // File doesn't exist, start fresh
62
+ }
63
+ if (e instanceof SyntaxError) {
64
+ logger_1.Logger.warn(chalk_1.default.yellow("āš ļø Session file corrupted. Starting fresh."));
65
+ return null;
62
66
  }
63
67
  logger_1.Logger.error(`Failed to load session: ${e.message}`);
64
68
  return null;
@@ -1,21 +1,18 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.debateIssue = debateIssue;
7
4
  const llamaindex_1 = require("llamaindex");
8
5
  const ai_1 = require("ai");
9
6
  const groq_1 = require("@ai-sdk/groq");
10
7
  const config_1 = require("../config");
11
- const chalk_1 = __importDefault(require("chalk"));
8
+ const logger_1 = require("../utils/logger");
12
9
  const groq = (0, groq_1.createGroq)({ apiKey: process.env.GROQ_API_KEY });
13
10
  async function debateIssue(issueTitle, issueDescription, fileName, userArgument, index) {
14
11
  // Validate API key before making any calls
15
12
  if (!process.env.GROQ_API_KEY) {
16
13
  throw new Error("GROQ_API_KEY is required for debate mode. Please set it in your .env.local file.");
17
14
  }
18
- console.log(chalk_1.default.dim(" 🧠 Thinking... (Consulting the Brain)"));
15
+ logger_1.Logger.dim(" 🧠 Thinking... (Consulting the Brain)");
19
16
  // 1. Extract keywords/context
20
17
  const query = `${userArgument} ${issueTitle}`;
21
18
  // 2. Retrieve new context
@@ -23,11 +20,11 @@ async function debateIssue(issueTitle, issueDescription, fileName, userArgument,
23
20
  const contextNodes = await retriever.retrieve(query);
24
21
  const newContext = contextNodes.map(n => n.node.getContent(llamaindex_1.MetadataMode.NONE)).join("\n\n").slice(0, 2000);
25
22
  if (newContext.length >= 2000) {
26
- console.log(chalk_1.default.yellow(" āš ļø Context truncated to 2000 chars"));
23
+ logger_1.Logger.warn(" āš ļø Context truncated to 2000 chars");
27
24
  }
28
25
  const sources = contextNodes.map(n => n.node.metadata?.['file_name']).filter(Boolean).join(', ');
29
26
  if (sources) {
30
- console.log(chalk_1.default.dim(` šŸ” Found relevant context from: ${sources}`));
27
+ logger_1.Logger.dim(` šŸ” Found relevant context from: ${sources}`);
31
28
  }
32
29
  // 3. Ask the Judge
33
30
  const systemPrompt = `You are a Senior Code Reviewer in a debate with a developer.
@@ -52,7 +52,8 @@ const RULES = [
52
52
  id: 'ARCH-001',
53
53
  severity: 'medium',
54
54
  message: 'Avoid using console.log in production code',
55
- pattern: /console\.log\(/
55
+ pattern: /console\.log\(/,
56
+ excludePattern: /(bin[\\/]jstar\.js|scripts[\\/]utils[\\/]logger\.ts|setup\.js|test[\\/])/
56
57
  },
57
58
  ];
58
59
  // File-level rules that check the whole content
@@ -60,15 +61,17 @@ const FILE_RULES = [
60
61
  {
61
62
  id: 'ARCH-002',
62
63
  severity: 'high',
63
- message: 'Next.js "use client" must be at the very top of the file',
64
- pattern: /^(?!['"]use client['"]).*['"]use client['"]/s,
65
- filePattern: /\.tsx?$/
64
+ message: 'Next.js "use client" must be at the very top of the file (before imports)',
65
+ pattern: /^(?!(?:\s*|(?:\/\/[^\n]*\n)|(?:\/\*[\s\S]*?\*\/))*['"]use client['"]).*['"]use client['"]/s,
66
+ filePattern: /\.tsx?$/,
67
+ excludePattern: /(scripts|test)[\\/]/
66
68
  }
67
69
  ];
68
70
  class Detective {
69
- constructor(directory) {
71
+ constructor(directory, options = {}) {
70
72
  this.directory = directory;
71
73
  this.violations = [];
74
+ this.includeBuildFiles = options.includeBuildFiles ?? false;
72
75
  }
73
76
  async scan() {
74
77
  this.walk(this.directory);
@@ -82,9 +85,12 @@ class Detective {
82
85
  const filePath = path.join(dir, file);
83
86
  const stat = fs.statSync(filePath);
84
87
  if (stat.isDirectory()) {
85
- if (file !== 'node_modules' && file !== '.git' && file !== '.jstar') {
86
- this.walk(filePath);
88
+ // Ignore common build/config directories
89
+ const ignoredDirs = ['node_modules', '.git', '.jstar', 'dist', 'coverage', '.next'];
90
+ if (!this.includeBuildFiles && ignoredDirs.includes(file)) {
91
+ continue;
87
92
  }
93
+ this.walk(filePath);
88
94
  }
89
95
  else {
90
96
  this.checkFile(filePath);
@@ -94,12 +100,17 @@ class Detective {
94
100
  checkFile(filePath) {
95
101
  if (!filePath.match(/\.(ts|tsx|js|jsx)$/))
96
102
  return;
103
+ // Skip .d.ts files
104
+ if (filePath.endsWith('.d.ts'))
105
+ return;
97
106
  const content = fs.readFileSync(filePath, 'utf-8');
98
107
  const lines = content.split('\n');
99
108
  // Line-based rules
100
109
  for (const rule of RULES) {
101
110
  if (rule.filePattern && !filePath.match(rule.filePattern))
102
111
  continue;
112
+ if (rule.excludePattern && filePath.match(rule.excludePattern))
113
+ continue;
103
114
  lines.forEach((line, index) => {
104
115
  if (rule.pattern.test(line)) {
105
116
  this.addViolation(filePath, index + 1, rule);
@@ -110,6 +121,8 @@ class Detective {
110
121
  for (const rule of FILE_RULES) {
111
122
  if (rule.filePattern && !filePath.match(rule.filePattern))
112
123
  continue;
124
+ if (rule.excludePattern && filePath.match(rule.excludePattern))
125
+ continue;
113
126
  if (rule.pattern.test(content)) {
114
127
  this.addViolation(filePath, 1, rule);
115
128
  }
@@ -145,7 +158,10 @@ class Detective {
145
158
  exports.Detective = Detective;
146
159
  // CLI Integration
147
160
  if (require.main === module) {
148
- const detective = new Detective(path.join(process.cwd(), 'src'));
161
+ const args = process.argv.slice(2);
162
+ const includeBuildFiles = args.includes('--all');
163
+ // Scan current directory by default
164
+ const detective = new Detective(process.cwd(), { includeBuildFiles });
149
165
  detective.scan();
150
166
  detective.report();
151
167
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GeminiEmbedding = void 0;
4
4
  const generative_ai_1 = require("@google/generative-ai");
5
+ const logger_1 = require("./utils/logger");
5
6
  class GeminiEmbedding {
6
7
  constructor() {
7
8
  // Stubs for BaseEmbedding compliance
@@ -27,7 +28,7 @@ class GeminiEmbedding {
27
28
  if (e.message.includes("fetch failed") || e.message.includes("network")) {
28
29
  retries++;
29
30
  const waitTime = Math.pow(2, retries) * 1000;
30
- console.warn(`āš ļø Network error. Retrying in ${waitTime / 1000}s... (${retries}/${maxRetries})`);
31
+ logger_1.Logger.warn(`āš ļø Network error. Retrying in ${waitTime / 1000}s... (${retries}/${maxRetries})`);
31
32
  await new Promise(resolve => setTimeout(resolve, waitTime));
32
33
  }
33
34
  else {
@@ -42,7 +43,7 @@ class GeminiEmbedding {
42
43
  }
43
44
  async getTextEmbeddings(texts) {
44
45
  const embeddings = [];
45
- console.log(`Creating embeddings for ${texts.length} chunks (Batching to avoid rate limits)...`);
46
+ logger_1.Logger.info(`Creating embeddings for ${texts.length} chunks (Batching to avoid rate limits)...`);
46
47
  // Process in smaller batches with delay
47
48
  const BATCH_SIZE = 1; // Strict serial for safety on free tier
48
49
  const DELAY_MS = 1000; // 1s delay between calls
@@ -58,17 +59,17 @@ class GeminiEmbedding {
58
59
  success = true;
59
60
  // Standard delay between calls
60
61
  await new Promise(resolve => setTimeout(resolve, DELAY_MS));
61
- process.stdout.write("."); // Progress indicator
62
+ logger_1.Logger.inline("."); // Progress indicator
62
63
  }
63
64
  catch (e) {
64
65
  if (e.message.includes("429") || e.message.includes("quota")) {
65
66
  retries++;
66
67
  const waitTime = Math.pow(2, retries) * 2000; // 2s, 4s, 8s, 16s...
67
- console.warn(`\nāš ļø Rate limit hit. Retrying in ${waitTime / 1000}s...`);
68
+ logger_1.Logger.warn(`\nāš ļø Rate limit hit. Retrying in ${waitTime / 1000}s...`);
68
69
  await new Promise(resolve => setTimeout(resolve, waitTime));
69
70
  }
70
71
  else {
71
- console.error("\nāŒ Embedding failed irreversibly:", e.message);
72
+ logger_1.Logger.error("\nāŒ Embedding failed irreversibly: " + e.message);
72
73
  throw e;
73
74
  }
74
75
  }
@@ -78,7 +79,7 @@ class GeminiEmbedding {
78
79
  }
79
80
  }
80
81
  }
81
- console.log("\nāœ… Done embedding.");
82
+ logger_1.Logger.success("\nāœ… Done embedding.");
82
83
  return embeddings;
83
84
  }
84
85
  similarity(embedding1, embedding2) {
@@ -42,6 +42,7 @@ const mock_llm_1 = require("./mock-llm");
42
42
  const path = __importStar(require("path"));
43
43
  const fs = __importStar(require("fs"));
44
44
  const chalk_1 = __importDefault(require("chalk"));
45
+ const logger_1 = require("./utils/logger");
45
46
  // IMPORTANT: Import config for side effects (loads dotenv from cwd)
46
47
  require("./config");
47
48
  // Configuration
@@ -57,7 +58,7 @@ function getSourceDir() {
57
58
  if (fs.existsSync(customPath)) {
58
59
  return customPath;
59
60
  }
60
- console.error(chalk_1.default.red(`āŒ Custom path not found: ${customPath}`));
61
+ logger_1.Logger.error(`āŒ Custom path not found: ${customPath}`);
61
62
  process.exit(1);
62
63
  }
63
64
  // 2. Try common source directories
@@ -76,30 +77,49 @@ function getSourceDir() {
76
77
  return cwd;
77
78
  }
78
79
  async function main() {
80
+ logger_1.Logger.init(); // Initialize Logger (auto-detects modes)
79
81
  // 0. Environment Validation
80
82
  const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
81
83
  if (!geminiKey) {
82
- console.error(chalk_1.default.red("āŒ Missing GEMINI_API_KEY (or GOOGLE_API_KEY)!"));
83
- console.log(chalk_1.default.yellow("\nPlease ensure you have a .env.local file. Check .env.example for a template.\n"));
84
+ logger_1.Logger.error("āŒ Missing GEMINI_API_KEY (or GOOGLE_API_KEY)!");
85
+ logger_1.Logger.warn("\nPlease ensure you have a .env.local file. Check .env.example for a template.\n");
84
86
  process.exit(1);
85
87
  }
86
88
  const args = process.argv.slice(2);
87
89
  const isWatch = args.includes("--watch");
88
90
  const SOURCE_DIR = getSourceDir();
89
- console.log(chalk_1.default.blue("🧠 J-Star Indexer: Scanning codebase..."));
90
- console.log(chalk_1.default.dim(` Source: ${SOURCE_DIR}`));
91
+ logger_1.Logger.info(chalk_1.default.blue("🧠 J-Star Indexer: Scanning codebase..."));
92
+ logger_1.Logger.dim(` Source: ${SOURCE_DIR}`);
91
93
  // 1. Load documents (Your Code)
92
94
  if (!fs.existsSync(SOURCE_DIR)) {
93
- console.error(chalk_1.default.red(`āŒ Source directory not found: ${SOURCE_DIR}`));
95
+ logger_1.Logger.error(`āŒ Source directory not found: ${SOURCE_DIR}`);
94
96
  process.exit(1);
95
97
  }
96
98
  const reader = new llamaindex_1.SimpleDirectoryReader();
97
99
  const documents = await reader.loadData({ directoryPath: SOURCE_DIR });
98
- console.log(chalk_1.default.yellow(`šŸ“„ Found ${documents.length} files to index.`));
100
+ // --- SECURITY FILTER ---
101
+ // Exclude sensitive files (like .env) and build artifacts
102
+ const EXCLUDED_PATTERNS = [
103
+ /pnpm-lock\.yaml/,
104
+ /package-lock\.json/,
105
+ /yarn\.lock/,
106
+ /\.env/,
107
+ /\.DS_Store/,
108
+ /node_modules/,
109
+ /\.git/,
110
+ /\.jstar/,
111
+ /\.json$/, // Prefer code over JSON data for context unless docs
112
+ ];
113
+ const filteredDocuments = documents.filter(doc => {
114
+ const filePath = doc.metadata?.file_path || doc.id_ || '';
115
+ // Check strict exclusions
116
+ const isExcluded = EXCLUDED_PATTERNS.some(pattern => pattern.test(filePath));
117
+ return !isExcluded;
118
+ });
119
+ logger_1.Logger.info(chalk_1.default.yellow(`šŸ“„ Found ${documents.length} files. Indexing ${filteredDocuments.length} valid files (filtered ${documents.length - filteredDocuments.length} excluded).`));
99
120
  const isInit = args.includes("--init");
100
121
  try {
101
122
  // 2. Setup Service Context with Google Gemini Embeddings
102
- // using 'models/text-embedding-004' which is a strong, recent model
103
123
  const embedModel = new gemini_embedding_1.GeminiEmbedding();
104
124
  const llm = new mock_llm_1.MockLLM();
105
125
  const serviceContext = (0, llamaindex_1.serviceContextFromDefaults)({
@@ -109,13 +129,13 @@ async function main() {
109
129
  // 3. Create the Storage Context
110
130
  let storageContext;
111
131
  if (isInit) {
112
- console.log(chalk_1.default.blue("✨ Initializing fresh Local Brain..."));
132
+ logger_1.Logger.info(chalk_1.default.blue("✨ Initializing fresh Local Brain..."));
113
133
  storageContext = await (0, llamaindex_1.storageContextFromDefaults)({});
114
134
  }
115
135
  else {
116
136
  // Try to load
117
137
  if (!fs.existsSync(STORAGE_DIR)) {
118
- console.log(chalk_1.default.yellow("āš ļø Storage not found. Running fresh init..."));
138
+ logger_1.Logger.warn("āš ļø Storage not found. Running fresh init...");
119
139
  storageContext = await (0, llamaindex_1.storageContextFromDefaults)({});
120
140
  }
121
141
  else {
@@ -125,12 +145,11 @@ async function main() {
125
145
  }
126
146
  }
127
147
  // 4. Generate the Index
128
- const index = await llamaindex_1.VectorStoreIndex.fromDocuments(documents, {
148
+ const index = await llamaindex_1.VectorStoreIndex.fromDocuments(filteredDocuments, {
129
149
  storageContext,
130
150
  serviceContext,
131
151
  });
132
152
  // 4. Persist (Save the Brain)
133
- // Manual persistence for LlamaIndex TS compatibility
134
153
  const ctxToPersist = index.storageContext;
135
154
  if (ctxToPersist.docStore)
136
155
  await ctxToPersist.docStore.persist(path.join(STORAGE_DIR, "doc_store.json"));
@@ -140,17 +159,17 @@ async function main() {
140
159
  await ctxToPersist.indexStore.persist(path.join(STORAGE_DIR, "index_store.json"));
141
160
  if (ctxToPersist.propStore)
142
161
  await ctxToPersist.propStore.persist(path.join(STORAGE_DIR, "property_store.json"));
143
- console.log(chalk_1.default.green("āœ… Indexing Complete. Brain is updated."));
162
+ logger_1.Logger.success("āœ… Indexing Complete. Brain is updated.");
144
163
  if (isWatch) {
145
- console.log(chalk_1.default.blue("šŸ‘€ Watch mode enabled."));
164
+ logger_1.Logger.info(chalk_1.default.blue("šŸ‘€ Watch mode enabled."));
146
165
  }
147
166
  }
148
167
  catch (e) {
149
- console.error(chalk_1.default.red("āŒ Indexing Failed:"), e.message);
168
+ logger_1.Logger.error("āŒ Indexing Failed: " + e.message);
150
169
  if (e.message.includes("API") || e.message.includes("key")) {
151
- console.log(chalk_1.default.yellow("šŸ‘‰ Tip: Make sure you have GEMINI_API_KEY in your .env.local file."));
170
+ logger_1.Logger.warn("šŸ‘‰ Tip: Make sure you have GEMINI_API_KEY in your .env.local file.");
152
171
  }
153
172
  process.exit(1);
154
173
  }
155
174
  }
156
- main().catch(console.error);
175
+ main().catch(err => logger_1.Logger.error(err));
@@ -52,8 +52,8 @@ async function startInteractiveSession(findings, index) {
52
52
  if (interactiveFindings.length === 0 || interactiveFindings.every(f => f.issues.length === 0)) {
53
53
  return { updatedFindings: interactiveFindings, hasUpdates: false };
54
54
  }
55
- console.log(chalk_1.default.bold.magenta("\nšŸ—£ļø Interactive Review Session"));
56
- console.log(chalk_1.default.dim(" Use arrow keys to navigate. Select an issue to debate."));
55
+ logger_1.Logger.info(chalk_1.default.bold.magenta("\nšŸ—£ļø Interactive Review Session"));
56
+ logger_1.Logger.dim(" Use arrow keys to navigate. Select an issue to debate.");
57
57
  while (active) {
58
58
  // Re-calculate choices every loop to reflect status changes
59
59
  // Flatten
@@ -92,33 +92,33 @@ async function startInteractiveSession(findings, index) {
92
92
  const selected = flatIssues[selectedIdx];
93
93
  const { issue, file } = selected;
94
94
  // Show Details
95
- console.log(chalk_1.default.cyan(`\nTitle: ${issue.title}`));
96
- console.log(chalk_1.default.white(issue.description));
97
- console.log(chalk_1.default.dim(`File: ${file}`));
95
+ logger_1.Logger.info(chalk_1.default.cyan(`\nTitle: ${issue.title}`));
96
+ logger_1.Logger.info(chalk_1.default.white(issue.description));
97
+ logger_1.Logger.dim(`File: ${file}`);
98
98
  if (issue.confidenceScore)
99
- console.log(chalk_1.default.yellow(`Confidence: ${issue.confidenceScore}/5`));
99
+ logger_1.Logger.info(chalk_1.default.yellow(`Confidence: ${issue.confidenceScore}/5`));
100
100
  if (issue.status)
101
- console.log(chalk_1.default.green(`Status: ${issue.status}`));
101
+ logger_1.Logger.success(`Status: ${issue.status}`);
102
102
  // Action Menu
103
103
  const action = await (0, interaction_1.showActionMenu)(issue.title);
104
104
  if (action === 'discuss') {
105
105
  const argument = await (0, interaction_1.askForArgument)();
106
106
  const result = await (0, debate_1.debateIssue)(issue.title, issue.description, file, argument, index);
107
- console.log(chalk_1.default.yellow(`\nšŸ¤– Bot: ${result.text}`));
107
+ logger_1.Logger.info(chalk_1.default.yellow(`\nšŸ¤– Bot: ${result.text}`));
108
108
  if (result.severity === 'LGTM') {
109
- console.log(chalk_1.default.green("āœ… Issue withdrawn by AI!"));
109
+ logger_1.Logger.success("āœ… Issue withdrawn by AI!");
110
110
  // Direct update to our state
111
111
  interactiveFindings[selected.fileIndex].issues[selected.issueIndex].status = 'resolved';
112
112
  hasUpdates = true;
113
113
  }
114
114
  }
115
115
  else if (action === 'ignore') {
116
- console.log(chalk_1.default.dim('Issue ignored locally.'));
116
+ logger_1.Logger.dim('Issue ignored locally.');
117
117
  interactiveFindings[selected.fileIndex].issues[selected.issueIndex].status = 'ignored';
118
118
  hasUpdates = true;
119
119
  }
120
120
  else if (action === 'accept') {
121
- console.log(chalk_1.default.green('Issue accepted.'));
121
+ logger_1.Logger.success('Issue accepted.');
122
122
  }
123
123
  else if (action === 'exit') {
124
124
  active = false;
package/package.json CHANGED
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "jstar-reviewer",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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",
@@ -56,13 +65,5 @@
56
65
  "type": "commonjs",
57
66
  "bugs": {
58
67
  "url": "https://github.com/JStaRFilms/jstar-code-review/issues"
59
- },
60
- "scripts": {
61
- "build": "tsc",
62
- "index:init": "ts-node scripts/indexer.ts --init",
63
- "index:watch": "ts-node scripts/indexer.ts --watch",
64
- "review": "ts-node scripts/reviewer.ts",
65
- "chat": "ts-node scripts/chat.ts",
66
- "detect": "ts-node scripts/detective.ts"
67
68
  }
68
69
  }
package/scripts/chat.ts CHANGED
@@ -27,7 +27,11 @@ async function loadSession(): Promise<SessionState | null> {
27
27
  return JSON.parse(content);
28
28
  } catch (e: any) {
29
29
  if (e.code === 'ENOENT') {
30
- return null; // File doesn't exist
30
+ return null; // File doesn't exist, start fresh
31
+ }
32
+ if (e instanceof SyntaxError) {
33
+ Logger.warn(chalk.yellow("āš ļø Session file corrupted. Starting fresh."));
34
+ return null;
31
35
  }
32
36
  Logger.error(`Failed to load session: ${e.message}`);
33
37
  return null;
@@ -2,6 +2,7 @@ import { VectorStoreIndex, MetadataMode } from "llamaindex";
2
2
  import { generateText } from "ai";
3
3
  import { createGroq } from "@ai-sdk/groq";
4
4
  import { Config } from "../config";
5
+ import { Logger } from "../utils/logger";
5
6
  import chalk from "chalk";
6
7
 
7
8
  const groq = createGroq({ apiKey: process.env.GROQ_API_KEY });
@@ -24,7 +25,7 @@ export async function debateIssue(
24
25
  throw new Error("GROQ_API_KEY is required for debate mode. Please set it in your .env.local file.");
25
26
  }
26
27
 
27
- console.log(chalk.dim(" 🧠 Thinking... (Consulting the Brain)"));
28
+ Logger.dim(" 🧠 Thinking... (Consulting the Brain)");
28
29
 
29
30
  // 1. Extract keywords/context
30
31
  const query = `${userArgument} ${issueTitle}`;
@@ -35,12 +36,12 @@ export async function debateIssue(
35
36
  const newContext = contextNodes.map(n => n.node.getContent(MetadataMode.NONE)).join("\n\n").slice(0, 2000);
36
37
 
37
38
  if (newContext.length >= 2000) {
38
- console.log(chalk.yellow(" āš ļø Context truncated to 2000 chars"));
39
+ Logger.warn(" āš ļø Context truncated to 2000 chars");
39
40
  }
40
41
 
41
42
  const sources = contextNodes.map(n => n.node.metadata?.['file_name']).filter(Boolean).join(', ');
42
43
  if (sources) {
43
- console.log(chalk.dim(` šŸ” Found relevant context from: ${sources}`));
44
+ Logger.dim(` šŸ” Found relevant context from: ${sources}`);
44
45
  }
45
46
 
46
47
  // 3. Ask the Judge
@@ -17,6 +17,7 @@ interface Rule {
17
17
  message: string;
18
18
  pattern: RegExp;
19
19
  filePattern?: RegExp; // Only check files matching this pattern
20
+ excludePattern?: RegExp; // Exclude files matching this pattern
20
21
  }
21
22
 
22
23
  const RULES: Rule[] = [
@@ -30,7 +31,8 @@ const RULES: Rule[] = [
30
31
  id: 'ARCH-001',
31
32
  severity: 'medium',
32
33
  message: 'Avoid using console.log in production code',
33
- pattern: /console\.log\(/
34
+ pattern: /console\.log\(/,
35
+ excludePattern: /(bin[\\/]jstar\.js|scripts[\\/]utils[\\/]logger\.ts|setup\.js|test[\\/])/
34
36
  },
35
37
  ];
36
38
 
@@ -39,16 +41,20 @@ const FILE_RULES: Rule[] = [
39
41
  {
40
42
  id: 'ARCH-002',
41
43
  severity: 'high',
42
- message: 'Next.js "use client" must be at the very top of the file',
43
- pattern: /^(?!['"]use client['"]).*['"]use client['"]/s,
44
- filePattern: /\.tsx?$/
44
+ message: 'Next.js "use client" must be at the very top of the file (before imports)',
45
+ pattern: /^(?!(?:\s*|(?:\/\/[^\n]*\n)|(?:\/\*[\s\S]*?\*\/))*['"]use client['"]).*['"]use client['"]/s,
46
+ filePattern: /\.tsx?$/,
47
+ excludePattern: /(scripts|test)[\\/]/
45
48
  }
46
49
  ];
47
50
 
48
51
  export class Detective {
49
52
  violations: Violation[] = [];
53
+ private includeBuildFiles: boolean;
50
54
 
51
- constructor(private directory: string) { }
55
+ constructor(private directory: string, options: { includeBuildFiles?: boolean } = {}) {
56
+ this.includeBuildFiles = options.includeBuildFiles ?? false;
57
+ }
52
58
 
53
59
  async scan(): Promise<Violation[]> {
54
60
  this.walk(this.directory);
@@ -63,9 +69,12 @@ export class Detective {
63
69
  const stat = fs.statSync(filePath);
64
70
 
65
71
  if (stat.isDirectory()) {
66
- if (file !== 'node_modules' && file !== '.git' && file !== '.jstar') {
67
- this.walk(filePath);
72
+ // Ignore common build/config directories
73
+ const ignoredDirs = ['node_modules', '.git', '.jstar', 'dist', 'coverage', '.next'];
74
+ if (!this.includeBuildFiles && ignoredDirs.includes(file)) {
75
+ continue;
68
76
  }
77
+ this.walk(filePath);
69
78
  } else {
70
79
  this.checkFile(filePath);
71
80
  }
@@ -74,6 +83,8 @@ export class Detective {
74
83
 
75
84
  private checkFile(filePath: string) {
76
85
  if (!filePath.match(/\.(ts|tsx|js|jsx)$/)) return;
86
+ // Skip .d.ts files
87
+ if (filePath.endsWith('.d.ts')) return;
77
88
 
78
89
  const content = fs.readFileSync(filePath, 'utf-8');
79
90
  const lines = content.split('\n');
@@ -81,6 +92,7 @@ export class Detective {
81
92
  // Line-based rules
82
93
  for (const rule of RULES) {
83
94
  if (rule.filePattern && !filePath.match(rule.filePattern)) continue;
95
+ if (rule.excludePattern && filePath.match(rule.excludePattern)) continue;
84
96
 
85
97
  lines.forEach((line, index) => {
86
98
  if (rule.pattern.test(line)) {
@@ -92,6 +104,8 @@ export class Detective {
92
104
  // File-based rules
93
105
  for (const rule of FILE_RULES) {
94
106
  if (rule.filePattern && !filePath.match(rule.filePattern)) continue;
107
+ if (rule.excludePattern && filePath.match(rule.excludePattern)) continue;
108
+
95
109
  if (rule.pattern.test(content)) {
96
110
  this.addViolation(filePath, 1, rule);
97
111
  }
@@ -132,7 +146,11 @@ export class Detective {
132
146
 
133
147
  // CLI Integration
134
148
  if (require.main === module) {
135
- const detective = new Detective(path.join(process.cwd(), 'src'));
149
+ const args = process.argv.slice(2);
150
+ const includeBuildFiles = args.includes('--all');
151
+
152
+ // Scan current directory by default
153
+ const detective = new Detective(process.cwd(), { includeBuildFiles });
136
154
  detective.scan();
137
155
  detective.report();
138
156
  }
@@ -1,4 +1,5 @@
1
1
  import { GoogleGenerativeAI } from "@google/generative-ai";
2
+ import { Logger } from "./utils/logger";
2
3
 
3
4
  export class GeminiEmbedding {
4
5
  private genAI: GoogleGenerativeAI;
@@ -26,7 +27,7 @@ export class GeminiEmbedding {
26
27
  if (e.message.includes("fetch failed") || e.message.includes("network")) {
27
28
  retries++;
28
29
  const waitTime = Math.pow(2, retries) * 1000;
29
- console.warn(`āš ļø Network error. Retrying in ${waitTime / 1000}s... (${retries}/${maxRetries})`);
30
+ Logger.warn(`āš ļø Network error. Retrying in ${waitTime / 1000}s... (${retries}/${maxRetries})`);
30
31
  await new Promise(resolve => setTimeout(resolve, waitTime));
31
32
  } else {
32
33
  throw e;
@@ -42,7 +43,7 @@ export class GeminiEmbedding {
42
43
 
43
44
  async getTextEmbeddings(texts: string[]): Promise<number[][]> {
44
45
  const embeddings: number[][] = [];
45
- console.log(`Creating embeddings for ${texts.length} chunks (Batching to avoid rate limits)...`);
46
+ Logger.info(`Creating embeddings for ${texts.length} chunks (Batching to avoid rate limits)...`);
46
47
 
47
48
  // Process in smaller batches with delay
48
49
  const BATCH_SIZE = 1; // Strict serial for safety on free tier
@@ -60,15 +61,15 @@ export class GeminiEmbedding {
60
61
  success = true;
61
62
  // Standard delay between calls
62
63
  await new Promise(resolve => setTimeout(resolve, DELAY_MS));
63
- process.stdout.write("."); // Progress indicator
64
+ Logger.inline("."); // Progress indicator
64
65
  } catch (e: any) {
65
66
  if (e.message.includes("429") || e.message.includes("quota")) {
66
67
  retries++;
67
68
  const waitTime = Math.pow(2, retries) * 2000; // 2s, 4s, 8s, 16s...
68
- console.warn(`\nāš ļø Rate limit hit. Retrying in ${waitTime / 1000}s...`);
69
+ Logger.warn(`\nāš ļø Rate limit hit. Retrying in ${waitTime / 1000}s...`);
69
70
  await new Promise(resolve => setTimeout(resolve, waitTime));
70
71
  } else {
71
- console.error("\nāŒ Embedding failed irreversibly:", e.message);
72
+ Logger.error("\nāŒ Embedding failed irreversibly: " + e.message);
72
73
  throw e;
73
74
  }
74
75
  }
@@ -78,7 +79,7 @@ export class GeminiEmbedding {
78
79
  }
79
80
  }
80
81
  }
81
- console.log("\nāœ… Done embedding.");
82
+ Logger.success("\nāœ… Done embedding.");
82
83
  return embeddings;
83
84
  }
84
85
 
@@ -9,6 +9,7 @@ import { MockLLM } from "./mock-llm";
9
9
  import * as path from "path";
10
10
  import * as fs from "fs";
11
11
  import chalk from "chalk";
12
+ import { Logger } from "./utils/logger";
12
13
  // IMPORTANT: Import config for side effects (loads dotenv from cwd)
13
14
  import "./config";
14
15
 
@@ -27,7 +28,7 @@ function getSourceDir(): string {
27
28
  if (fs.existsSync(customPath)) {
28
29
  return customPath;
29
30
  }
30
- console.error(chalk.red(`āŒ Custom path not found: ${customPath}`));
31
+ Logger.error(`āŒ Custom path not found: ${customPath}`);
31
32
  process.exit(1);
32
33
  }
33
34
 
@@ -49,11 +50,13 @@ function getSourceDir(): string {
49
50
  }
50
51
 
51
52
  async function main() {
53
+ Logger.init(); // Initialize Logger (auto-detects modes)
54
+
52
55
  // 0. Environment Validation
53
56
  const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
54
57
  if (!geminiKey) {
55
- console.error(chalk.red("āŒ Missing GEMINI_API_KEY (or GOOGLE_API_KEY)!"));
56
- console.log(chalk.yellow("\nPlease ensure you have a .env.local file. Check .env.example for a template.\n"));
58
+ Logger.error("āŒ Missing GEMINI_API_KEY (or GOOGLE_API_KEY)!");
59
+ Logger.warn("\nPlease ensure you have a .env.local file. Check .env.example for a template.\n");
57
60
  process.exit(1);
58
61
  }
59
62
 
@@ -61,25 +64,48 @@ async function main() {
61
64
  const isWatch = args.includes("--watch");
62
65
  const SOURCE_DIR = getSourceDir();
63
66
 
64
- console.log(chalk.blue("🧠 J-Star Indexer: Scanning codebase..."));
65
- console.log(chalk.dim(` Source: ${SOURCE_DIR}`));
67
+ Logger.info(chalk.blue("🧠 J-Star Indexer: Scanning codebase..."));
68
+ Logger.dim(` Source: ${SOURCE_DIR}`);
66
69
 
67
70
  // 1. Load documents (Your Code)
68
71
  if (!fs.existsSync(SOURCE_DIR)) {
69
- console.error(chalk.red(`āŒ Source directory not found: ${SOURCE_DIR}`));
72
+ Logger.error(`āŒ Source directory not found: ${SOURCE_DIR}`);
70
73
  process.exit(1);
71
74
  }
72
75
 
73
76
  const reader = new SimpleDirectoryReader();
74
77
  const documents = await reader.loadData({ directoryPath: SOURCE_DIR });
75
78
 
76
- console.log(chalk.yellow(`šŸ“„ Found ${documents.length} files to index.`));
79
+ // --- SECURITY FILTER ---
80
+ // Exclude sensitive files (like .env) and build artifacts
81
+ const EXCLUDED_PATTERNS = [
82
+ /pnpm-lock\.yaml/,
83
+ /package-lock\.json/,
84
+ /yarn\.lock/,
85
+ /\.env/,
86
+ /\.DS_Store/,
87
+ /node_modules/,
88
+ /\.git/,
89
+ /\.jstar/,
90
+ /\.json$/, // Prefer code over JSON data for context unless docs
91
+ ];
92
+
93
+ const filteredDocuments = documents.filter(doc => {
94
+ const filePath = (doc.metadata as any)?.file_path || (doc as any).id_ || '';
95
+
96
+ // Check strict exclusions
97
+ const isExcluded = EXCLUDED_PATTERNS.some(pattern => pattern.test(filePath));
98
+
99
+ return !isExcluded;
100
+ });
101
+
102
+ Logger.info(chalk.yellow(`šŸ“„ Found ${documents.length} files. Indexing ${filteredDocuments.length} valid files (filtered ${documents.length - filteredDocuments.length} excluded).`));
103
+
77
104
 
78
105
  const isInit = args.includes("--init");
79
106
 
80
107
  try {
81
108
  // 2. Setup Service Context with Google Gemini Embeddings
82
- // using 'models/text-embedding-004' which is a strong, recent model
83
109
  const embedModel = new GeminiEmbedding();
84
110
  const llm = new MockLLM();
85
111
  const serviceContext = serviceContextFromDefaults({
@@ -90,12 +116,12 @@ async function main() {
90
116
  // 3. Create the Storage Context
91
117
  let storageContext;
92
118
  if (isInit) {
93
- console.log(chalk.blue("✨ Initializing fresh Local Brain..."));
119
+ Logger.info(chalk.blue("✨ Initializing fresh Local Brain..."));
94
120
  storageContext = await storageContextFromDefaults({});
95
121
  } else {
96
122
  // Try to load
97
123
  if (!fs.existsSync(STORAGE_DIR)) {
98
- console.log(chalk.yellow("āš ļø Storage not found. Running fresh init..."));
124
+ Logger.warn("āš ļø Storage not found. Running fresh init...");
99
125
  storageContext = await storageContextFromDefaults({});
100
126
  } else {
101
127
  storageContext = await storageContextFromDefaults({
@@ -105,32 +131,31 @@ async function main() {
105
131
  }
106
132
 
107
133
  // 4. Generate the Index
108
- const index = await VectorStoreIndex.fromDocuments(documents, {
134
+ const index = await VectorStoreIndex.fromDocuments(filteredDocuments, {
109
135
  storageContext,
110
136
  serviceContext,
111
137
  });
112
138
 
113
139
  // 4. Persist (Save the Brain)
114
- // Manual persistence for LlamaIndex TS compatibility
115
140
  const ctxToPersist: any = index.storageContext;
116
141
  if (ctxToPersist.docStore) await ctxToPersist.docStore.persist(path.join(STORAGE_DIR, "doc_store.json"));
117
142
  if (ctxToPersist.vectorStore) await ctxToPersist.vectorStore.persist(path.join(STORAGE_DIR, "vector_store.json"));
118
143
  if (ctxToPersist.indexStore) await ctxToPersist.indexStore.persist(path.join(STORAGE_DIR, "index_store.json"));
119
144
  if (ctxToPersist.propStore) await ctxToPersist.propStore.persist(path.join(STORAGE_DIR, "property_store.json"));
120
145
 
121
- console.log(chalk.green("āœ… Indexing Complete. Brain is updated."));
146
+ Logger.success("āœ… Indexing Complete. Brain is updated.");
122
147
 
123
148
  if (isWatch) {
124
- console.log(chalk.blue("šŸ‘€ Watch mode enabled."));
149
+ Logger.info(chalk.blue("šŸ‘€ Watch mode enabled."));
125
150
  }
126
151
 
127
152
  } catch (e: any) {
128
- console.error(chalk.red("āŒ Indexing Failed:"), e.message);
153
+ Logger.error("āŒ Indexing Failed: " + e.message);
129
154
  if (e.message.includes("API") || e.message.includes("key")) {
130
- console.log(chalk.yellow("šŸ‘‰ Tip: Make sure you have GEMINI_API_KEY in your .env.local file."));
155
+ Logger.warn("šŸ‘‰ Tip: Make sure you have GEMINI_API_KEY in your .env.local file.");
131
156
  }
132
157
  process.exit(1);
133
158
  }
134
159
  }
135
160
 
136
- main().catch(console.error);
161
+ main().catch(err => Logger.error(err));
@@ -8,11 +8,11 @@ export class MockLLM {
8
8
  tokenizer: undefined,
9
9
  };
10
10
 
11
- async chat(messages: any[], parentEvent?: any): Promise<any> {
11
+ async chat(messages: { content: string, role: string }[], parentEvent?: any): Promise<{ message: { content: string } }> {
12
12
  return { message: { content: "Mock response" } };
13
13
  }
14
14
 
15
- async complete(prompt: string, parentEvent?: any): Promise<any> {
15
+ async complete(prompt: string, parentEvent?: any): Promise<{ text: string }> {
16
16
  return { text: "Mock response" };
17
17
  }
18
18
  }
@@ -21,8 +21,8 @@ export async function startInteractiveSession(
21
21
  return { updatedFindings: interactiveFindings, hasUpdates: false };
22
22
  }
23
23
 
24
- console.log(chalk.bold.magenta("\nšŸ—£ļø Interactive Review Session"));
25
- console.log(chalk.dim(" Use arrow keys to navigate. Select an issue to debate."));
24
+ Logger.info(chalk.bold.magenta("\nšŸ—£ļø Interactive Review Session"));
25
+ Logger.dim(" Use arrow keys to navigate. Select an issue to debate.");
26
26
 
27
27
  while (active) {
28
28
  // Re-calculate choices every loop to reflect status changes
@@ -74,11 +74,11 @@ export async function startInteractiveSession(
74
74
  const { issue, file } = selected;
75
75
 
76
76
  // Show Details
77
- console.log(chalk.cyan(`\nTitle: ${issue.title}`));
78
- console.log(chalk.white(issue.description));
79
- console.log(chalk.dim(`File: ${file}`));
80
- if (issue.confidenceScore) console.log(chalk.yellow(`Confidence: ${issue.confidenceScore}/5`));
81
- if (issue.status) console.log(chalk.green(`Status: ${issue.status}`));
77
+ Logger.info(chalk.cyan(`\nTitle: ${issue.title}`));
78
+ Logger.info(chalk.white(issue.description));
79
+ Logger.dim(`File: ${file}`);
80
+ if (issue.confidenceScore) Logger.info(chalk.yellow(`Confidence: ${issue.confidenceScore}/5`));
81
+ if (issue.status) Logger.success(`Status: ${issue.status}`);
82
82
 
83
83
  // Action Menu
84
84
  const action = await showActionMenu(issue.title);
@@ -93,21 +93,21 @@ export async function startInteractiveSession(
93
93
  index
94
94
  );
95
95
 
96
- console.log(chalk.yellow(`\nšŸ¤– Bot: ${result.text}`));
96
+ Logger.info(chalk.yellow(`\nšŸ¤– Bot: ${result.text}`));
97
97
 
98
98
  if (result.severity === 'LGTM') {
99
- console.log(chalk.green("āœ… Issue withdrawn by AI!"));
99
+ Logger.success("āœ… Issue withdrawn by AI!");
100
100
  // Direct update to our state
101
101
  interactiveFindings[selected.fileIndex].issues[selected.issueIndex].status = 'resolved';
102
102
  hasUpdates = true;
103
103
  }
104
104
 
105
105
  } else if (action === 'ignore') {
106
- console.log(chalk.dim('Issue ignored locally.'));
106
+ Logger.dim('Issue ignored locally.');
107
107
  interactiveFindings[selected.fileIndex].issues[selected.issueIndex].status = 'ignored';
108
108
  hasUpdates = true;
109
109
  } else if (action === 'accept') {
110
- console.log(chalk.green('Issue accepted.'));
110
+ Logger.success('Issue accepted.');
111
111
  } else if (action === 'exit') {
112
112
  active = false;
113
113
  }