jstar-reviewer 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/bin/jstar.js +63 -26
- package/dist/scripts/config.js +91 -0
- package/dist/scripts/dashboard.js +217 -0
- package/dist/scripts/detective.js +150 -0
- package/dist/scripts/gemini-embedding.js +95 -0
- package/dist/scripts/indexer.js +123 -0
- package/dist/scripts/local-embedding.js +49 -0
- package/dist/scripts/mock-llm.js +22 -0
- package/dist/scripts/reviewer.js +296 -0
- package/dist/scripts/types.js +14 -0
- package/package.json +9 -8
- package/scripts/config.ts +42 -2
- package/scripts/indexer.ts +9 -11
- package/scripts/reviewer.ts +12 -1
- package/setup.js +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GeminiEmbedding = void 0;
|
|
4
|
+
const generative_ai_1 = require("@google/generative-ai");
|
|
5
|
+
class GeminiEmbedding {
|
|
6
|
+
constructor() {
|
|
7
|
+
// Stubs for BaseEmbedding compliance
|
|
8
|
+
this.embedBatchSize = 10;
|
|
9
|
+
const apiKey = process.env.GOOGLE_API_KEY;
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
throw new Error("GOOGLE_API_KEY is missing from environment variables.");
|
|
12
|
+
}
|
|
13
|
+
this.genAI = new generative_ai_1.GoogleGenerativeAI(apiKey);
|
|
14
|
+
// User requested 'text-embedding-004', which has better rate limits
|
|
15
|
+
this.model = this.genAI.getGenerativeModel({ model: "text-embedding-004" });
|
|
16
|
+
}
|
|
17
|
+
async getTextEmbedding(text) {
|
|
18
|
+
// Retry logic for transient network errors
|
|
19
|
+
let retries = 0;
|
|
20
|
+
const maxRetries = 3;
|
|
21
|
+
while (retries < maxRetries) {
|
|
22
|
+
try {
|
|
23
|
+
const result = await this.model.embedContent(text);
|
|
24
|
+
return result.embedding.values;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (e.message.includes("fetch failed") || e.message.includes("network")) {
|
|
28
|
+
retries++;
|
|
29
|
+
const waitTime = Math.pow(2, retries) * 1000;
|
|
30
|
+
console.warn(`⚠️ Network error. Retrying in ${waitTime / 1000}s... (${retries}/${maxRetries})`);
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error("Max retries exceeded for embedding request.");
|
|
39
|
+
}
|
|
40
|
+
async getQueryEmbedding(query) {
|
|
41
|
+
return this.getTextEmbedding(query);
|
|
42
|
+
}
|
|
43
|
+
async getTextEmbeddings(texts) {
|
|
44
|
+
const embeddings = [];
|
|
45
|
+
console.log(`Creating embeddings for ${texts.length} chunks (Batching to avoid rate limits)...`);
|
|
46
|
+
// Process in smaller batches with delay
|
|
47
|
+
const BATCH_SIZE = 1; // Strict serial for safety on free tier
|
|
48
|
+
const DELAY_MS = 1000; // 1s delay between calls
|
|
49
|
+
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
|
50
|
+
const batch = texts.slice(i, i + BATCH_SIZE);
|
|
51
|
+
for (const text of batch) {
|
|
52
|
+
let retries = 0;
|
|
53
|
+
let success = false;
|
|
54
|
+
while (!success && retries < 5) {
|
|
55
|
+
try {
|
|
56
|
+
const embedding = await this.getTextEmbedding(text);
|
|
57
|
+
embeddings.push(embedding);
|
|
58
|
+
success = true;
|
|
59
|
+
// Standard delay between calls
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, DELAY_MS));
|
|
61
|
+
process.stdout.write("."); // Progress indicator
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (e.message.includes("429") || e.message.includes("quota")) {
|
|
65
|
+
retries++;
|
|
66
|
+
const waitTime = Math.pow(2, retries) * 2000; // 2s, 4s, 8s, 16s...
|
|
67
|
+
console.warn(`\n⚠️ Rate limit hit. Retrying in ${waitTime / 1000}s...`);
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.error("\n❌ Embedding failed irreversibly:", e.message);
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!success) {
|
|
77
|
+
throw new Error("Max retries exceeded for rate limits.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log("\n✅ Done embedding.");
|
|
82
|
+
return embeddings;
|
|
83
|
+
}
|
|
84
|
+
similarity(embedding1, embedding2) {
|
|
85
|
+
return embedding1.reduce((sum, val, i) => sum + val * embedding2[i], 0);
|
|
86
|
+
}
|
|
87
|
+
async transform(nodes, _options) {
|
|
88
|
+
for (const node of nodes) {
|
|
89
|
+
node.embedding = await this.getTextEmbedding(node.getContent("text"));
|
|
90
|
+
}
|
|
91
|
+
return nodes;
|
|
92
|
+
}
|
|
93
|
+
async getTextEmbeddingsBatch(texts) { return this.getTextEmbeddings(texts); }
|
|
94
|
+
}
|
|
95
|
+
exports.GeminiEmbedding = GeminiEmbedding;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const llamaindex_1 = require("llamaindex");
|
|
40
|
+
const gemini_embedding_1 = require("./gemini-embedding");
|
|
41
|
+
const mock_llm_1 = require("./mock-llm");
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
// Configuration
|
|
46
|
+
const STORAGE_DIR = path.join(process.cwd(), ".jstar", "storage");
|
|
47
|
+
const SOURCE_DIR = path.join(process.cwd(), "scripts"); // Changed from src/ to scripts/
|
|
48
|
+
async function main() {
|
|
49
|
+
// 0. Environment Validation
|
|
50
|
+
if (!process.env.GOOGLE_API_KEY) {
|
|
51
|
+
console.error(chalk_1.default.red("❌ Missing GOOGLE_API_KEY!"));
|
|
52
|
+
console.log(chalk_1.default.yellow("\nPlease ensure you have a .env.local file. Check .env.example for a template.\n"));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const args = process.argv.slice(2);
|
|
56
|
+
const isWatch = args.includes("--watch");
|
|
57
|
+
console.log(chalk_1.default.blue("🧠 J-Star Indexer: Scanning codebase..."));
|
|
58
|
+
// 1. Load documents (Your Code)
|
|
59
|
+
if (!fs.existsSync(SOURCE_DIR)) {
|
|
60
|
+
console.error(chalk_1.default.red(`❌ Source directory not found: ${SOURCE_DIR}`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const reader = new llamaindex_1.SimpleDirectoryReader();
|
|
64
|
+
const documents = await reader.loadData({ directoryPath: SOURCE_DIR });
|
|
65
|
+
console.log(chalk_1.default.yellow(`📄 Found ${documents.length} files to index.`));
|
|
66
|
+
const isInit = args.includes("--init");
|
|
67
|
+
try {
|
|
68
|
+
// 2. Setup Service Context with Google Gemini Embeddings
|
|
69
|
+
// using 'models/text-embedding-004' which is a strong, recent model
|
|
70
|
+
const embedModel = new gemini_embedding_1.GeminiEmbedding();
|
|
71
|
+
const llm = new mock_llm_1.MockLLM();
|
|
72
|
+
const serviceContext = (0, llamaindex_1.serviceContextFromDefaults)({
|
|
73
|
+
embedModel,
|
|
74
|
+
llm: llm
|
|
75
|
+
});
|
|
76
|
+
// 3. Create the Storage Context
|
|
77
|
+
let storageContext;
|
|
78
|
+
if (isInit) {
|
|
79
|
+
console.log(chalk_1.default.blue("✨ Initializing fresh Local Brain..."));
|
|
80
|
+
storageContext = await (0, llamaindex_1.storageContextFromDefaults)({});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Try to load
|
|
84
|
+
if (!fs.existsSync(STORAGE_DIR)) {
|
|
85
|
+
console.log(chalk_1.default.yellow("⚠️ Storage not found. Running fresh init..."));
|
|
86
|
+
storageContext = await (0, llamaindex_1.storageContextFromDefaults)({});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
storageContext = await (0, llamaindex_1.storageContextFromDefaults)({
|
|
90
|
+
persistDir: STORAGE_DIR,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 4. Generate the Index
|
|
95
|
+
const index = await llamaindex_1.VectorStoreIndex.fromDocuments(documents, {
|
|
96
|
+
storageContext,
|
|
97
|
+
serviceContext,
|
|
98
|
+
});
|
|
99
|
+
// 4. Persist (Save the Brain)
|
|
100
|
+
// Manual persistence for LlamaIndex TS compatibility
|
|
101
|
+
const ctxToPersist = index.storageContext;
|
|
102
|
+
if (ctxToPersist.docStore)
|
|
103
|
+
await ctxToPersist.docStore.persist(path.join(STORAGE_DIR, "doc_store.json"));
|
|
104
|
+
if (ctxToPersist.vectorStore)
|
|
105
|
+
await ctxToPersist.vectorStore.persist(path.join(STORAGE_DIR, "vector_store.json"));
|
|
106
|
+
if (ctxToPersist.indexStore)
|
|
107
|
+
await ctxToPersist.indexStore.persist(path.join(STORAGE_DIR, "index_store.json"));
|
|
108
|
+
if (ctxToPersist.propStore)
|
|
109
|
+
await ctxToPersist.propStore.persist(path.join(STORAGE_DIR, "property_store.json"));
|
|
110
|
+
console.log(chalk_1.default.green("✅ Indexing Complete. Brain is updated."));
|
|
111
|
+
if (isWatch) {
|
|
112
|
+
console.log(chalk_1.default.blue("👀 Watch mode enabled."));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
console.error(chalk_1.default.red("❌ Indexing Failed:"), e.message);
|
|
117
|
+
if (e.message.includes("OpenAI")) {
|
|
118
|
+
console.log(chalk_1.default.yellow("👉 Tip: Make sure you have OPENAI_API_KEY in your .env file (or configure a local embedding model)."));
|
|
119
|
+
}
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalEmbedding = void 0;
|
|
4
|
+
const transformers_1 = require("@xenova/transformers");
|
|
5
|
+
// Skip local model checks if needed, or let it download
|
|
6
|
+
transformers_1.env.allowLocalModels = false;
|
|
7
|
+
transformers_1.env.useBrowserCache = false;
|
|
8
|
+
class LocalEmbedding {
|
|
9
|
+
constructor() {
|
|
10
|
+
// Stubs for BaseEmbedding interface compliance
|
|
11
|
+
this.embedBatchSize = 10;
|
|
12
|
+
this.modelName = "Xenova/bge-small-en-v1.5";
|
|
13
|
+
}
|
|
14
|
+
async init() {
|
|
15
|
+
if (!this.pipe) {
|
|
16
|
+
console.log("📥 Loading local embedding model (Xenova/bge-small-en-v1.5)...");
|
|
17
|
+
this.pipe = await (0, transformers_1.pipeline)("feature-extraction", this.modelName);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async getTextEmbedding(text) {
|
|
21
|
+
await this.init();
|
|
22
|
+
const result = await this.pipe(text, { pooling: "mean", normalize: true });
|
|
23
|
+
return Array.from(result.data);
|
|
24
|
+
}
|
|
25
|
+
async getQueryEmbedding(query) {
|
|
26
|
+
return this.getTextEmbedding(query);
|
|
27
|
+
}
|
|
28
|
+
// Batch method (Required by LlamaIndex)
|
|
29
|
+
async getTextEmbeddings(texts) {
|
|
30
|
+
await this.init();
|
|
31
|
+
const embeddings = [];
|
|
32
|
+
for (const text of texts) {
|
|
33
|
+
embeddings.push(await this.getTextEmbedding(text));
|
|
34
|
+
}
|
|
35
|
+
return embeddings;
|
|
36
|
+
}
|
|
37
|
+
similarity(embedding1, embedding2) {
|
|
38
|
+
// Simple dot product for normalized vectors
|
|
39
|
+
return embedding1.reduce((sum, val, i) => sum + val * embedding2[i], 0);
|
|
40
|
+
}
|
|
41
|
+
async transform(nodes, _options) {
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
node.embedding = await this.getTextEmbedding(node.getContent("text"));
|
|
44
|
+
}
|
|
45
|
+
return nodes;
|
|
46
|
+
}
|
|
47
|
+
async getTextEmbeddingsBatch(texts) { return this.getTextEmbeddings(texts); }
|
|
48
|
+
}
|
|
49
|
+
exports.LocalEmbedding = LocalEmbedding;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MockLLM = void 0;
|
|
4
|
+
class MockLLM {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.hasStreaming = false;
|
|
7
|
+
this.metadata = {
|
|
8
|
+
model: "mock",
|
|
9
|
+
temperature: 0,
|
|
10
|
+
topP: 1,
|
|
11
|
+
contextWindow: 1024,
|
|
12
|
+
tokenizer: undefined,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async chat(messages, parentEvent) {
|
|
16
|
+
return { message: { content: "Mock response" } };
|
|
17
|
+
}
|
|
18
|
+
async complete(prompt, parentEvent) {
|
|
19
|
+
return { text: "Mock response" };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.MockLLM = MockLLM;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const ai_1 = require("ai");
|
|
40
|
+
const groq_1 = require("@ai-sdk/groq");
|
|
41
|
+
const google_1 = require("@ai-sdk/google");
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
46
|
+
const config_1 = require("./config");
|
|
47
|
+
const detective_1 = require("./detective");
|
|
48
|
+
const gemini_embedding_1 = require("./gemini-embedding");
|
|
49
|
+
const mock_llm_1 = require("./mock-llm");
|
|
50
|
+
const dashboard_1 = require("./dashboard");
|
|
51
|
+
const llamaindex_1 = require("llamaindex");
|
|
52
|
+
const google = (0, google_1.createGoogleGenerativeAI)({ apiKey: process.env.GOOGLE_API_KEY });
|
|
53
|
+
const groq = (0, groq_1.createGroq)({ apiKey: process.env.GROQ_API_KEY });
|
|
54
|
+
const embedModel = new gemini_embedding_1.GeminiEmbedding();
|
|
55
|
+
const llm = new mock_llm_1.MockLLM();
|
|
56
|
+
const serviceContext = (0, llamaindex_1.serviceContextFromDefaults)({ embedModel, llm: llm });
|
|
57
|
+
const STORAGE_DIR = path.join(process.cwd(), ".jstar", "storage");
|
|
58
|
+
const SOURCE_DIR = path.join(process.cwd(), "scripts");
|
|
59
|
+
const OUTPUT_FILE = path.join(process.cwd(), ".jstar", "last-review.md");
|
|
60
|
+
const git = (0, simple_git_1.default)();
|
|
61
|
+
// --- Config ---
|
|
62
|
+
const MODEL_NAME = config_1.Config.MODEL_NAME;
|
|
63
|
+
const MAX_TOKENS_PER_REQUEST = 8000;
|
|
64
|
+
const CHARS_PER_TOKEN = 4;
|
|
65
|
+
const DELAY_BETWEEN_CHUNKS_MS = 2000;
|
|
66
|
+
// --- Helpers ---
|
|
67
|
+
function estimateTokens(text) {
|
|
68
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
69
|
+
}
|
|
70
|
+
const EXCLUDED_PATTERNS = [
|
|
71
|
+
/pnpm-lock\.yaml/,
|
|
72
|
+
/package-lock\.json/,
|
|
73
|
+
/yarn\.lock/,
|
|
74
|
+
/\.env/,
|
|
75
|
+
/\.json$/,
|
|
76
|
+
/\.txt$/,
|
|
77
|
+
/\.md$/,
|
|
78
|
+
/node_modules/,
|
|
79
|
+
/\.jstar\//,
|
|
80
|
+
];
|
|
81
|
+
function shouldSkipFile(fileName) {
|
|
82
|
+
return EXCLUDED_PATTERNS.some(pattern => pattern.test(fileName));
|
|
83
|
+
}
|
|
84
|
+
function chunkDiffByFile(diff) {
|
|
85
|
+
return diff.split(/(?=^diff --git)/gm).filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
function sleep(ms) {
|
|
88
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
89
|
+
}
|
|
90
|
+
function parseReviewResponse(text) {
|
|
91
|
+
try {
|
|
92
|
+
// Try to extract JSON from the response
|
|
93
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
94
|
+
if (jsonMatch) {
|
|
95
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
96
|
+
// Validate structure
|
|
97
|
+
if (parsed &&
|
|
98
|
+
typeof parsed === 'object' &&
|
|
99
|
+
Array.isArray(parsed.issues) &&
|
|
100
|
+
['P0_CRITICAL', 'P1_HIGH', 'P2_MEDIUM', 'LGTM'].includes(parsed.severity)) {
|
|
101
|
+
return {
|
|
102
|
+
severity: parsed.severity,
|
|
103
|
+
issues: parsed.issues
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
// Parse failed, try to extract from markdown
|
|
110
|
+
}
|
|
111
|
+
// Fallback: If "LGTM" in text, it's clean
|
|
112
|
+
if (text.includes('LGTM') || text.includes('✅')) {
|
|
113
|
+
return { severity: 'LGTM', issues: [] };
|
|
114
|
+
}
|
|
115
|
+
// Otherwise, assume there are issues (treat as medium)
|
|
116
|
+
return {
|
|
117
|
+
severity: config_1.Config.DEFAULT_SEVERITY,
|
|
118
|
+
issues: [{
|
|
119
|
+
title: 'Review Notes',
|
|
120
|
+
description: text.slice(0, 500),
|
|
121
|
+
fixPrompt: 'Review the file and address the issues mentioned above.'
|
|
122
|
+
}]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// --- Main ---
|
|
126
|
+
async function main() {
|
|
127
|
+
console.log(chalk_1.default.blue("🕵️ J-Star Reviewer: Analyzing your changes...\n"));
|
|
128
|
+
// 0. Environment Validation
|
|
129
|
+
if (!process.env.GOOGLE_API_KEY || !process.env.GROQ_API_KEY) {
|
|
130
|
+
console.error(chalk_1.default.red("❌ Missing API Keys!"));
|
|
131
|
+
console.log(chalk_1.default.yellow("\nPlease ensure you have a .env.local file with:"));
|
|
132
|
+
console.log(chalk_1.default.white("- GOOGLE_API_KEY"));
|
|
133
|
+
console.log(chalk_1.default.white("- GROQ_API_KEY"));
|
|
134
|
+
console.log(chalk_1.default.white("\nCheck .env.example for a template.\n"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// 1. Detective
|
|
138
|
+
console.log(chalk_1.default.blue("🔎 Running Detective Engine..."));
|
|
139
|
+
const detective = new detective_1.Detective(SOURCE_DIR);
|
|
140
|
+
await detective.scan();
|
|
141
|
+
detective.report();
|
|
142
|
+
// 1. Get the Diff
|
|
143
|
+
const diff = await git.diff(["--staged"]);
|
|
144
|
+
if (!diff) {
|
|
145
|
+
console.log(chalk_1.default.green("\n✅ No staged changes to review. (Did you 'git add'?)"));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// 2. Load the Brain
|
|
149
|
+
if (!fs.existsSync(STORAGE_DIR)) {
|
|
150
|
+
console.error(chalk_1.default.red("❌ Local Brain not found. Run 'pnpm run index:init' first."));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const storageContext = await (0, llamaindex_1.storageContextFromDefaults)({ persistDir: STORAGE_DIR });
|
|
154
|
+
const index = await llamaindex_1.VectorStoreIndex.init({ storageContext, serviceContext });
|
|
155
|
+
// 3. Retrieval
|
|
156
|
+
const retriever = index.asRetriever({ similarityTopK: 1 });
|
|
157
|
+
const keywords = (diff.match(/import .* from ['"](.*)['"]/g) || [])
|
|
158
|
+
.map(s => s.replace(/import .* from ['"](.*)['"]/, '$1'))
|
|
159
|
+
.join(" ").slice(0, 300) || "general context";
|
|
160
|
+
const contextNodes = await retriever.retrieve(keywords);
|
|
161
|
+
const relatedContext = contextNodes.map(n => n.node.getContent(llamaindex_1.MetadataMode.NONE).slice(0, 1500)).join("\n");
|
|
162
|
+
console.log(chalk_1.default.yellow(`\n🧠 Found ${contextNodes.length} context chunk.`));
|
|
163
|
+
// 4. Chunk the Diff
|
|
164
|
+
const fileChunks = chunkDiffByFile(diff);
|
|
165
|
+
const totalTokens = estimateTokens(diff);
|
|
166
|
+
console.log(chalk_1.default.dim(` Total diff: ~${totalTokens} tokens across ${fileChunks.length} files.`));
|
|
167
|
+
// 5. Structured JSON Prompt
|
|
168
|
+
const systemPrompt = `You are J-Star, a Senior Code Reviewer. Be direct and professional.
|
|
169
|
+
|
|
170
|
+
Analyze the Git Diff and return a JSON response with this EXACT structure:
|
|
171
|
+
{
|
|
172
|
+
"severity": "P0_CRITICAL" | "P1_HIGH" | "P2_MEDIUM" | "LGTM",
|
|
173
|
+
"issues": [
|
|
174
|
+
{
|
|
175
|
+
"title": "Short issue title",
|
|
176
|
+
"description": "Detailed description of the problem",
|
|
177
|
+
"line": 42,
|
|
178
|
+
"fixPrompt": "A specific prompt an AI can use to fix this issue"
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
SEVERITY GUIDE:
|
|
184
|
+
- P0_CRITICAL: Security vulnerabilities, data leaks, auth bypass, SQL injection
|
|
185
|
+
- P1_HIGH: Missing validation, race conditions, architectural violations
|
|
186
|
+
- P2_MEDIUM: Code quality, missing types, cleanup needed
|
|
187
|
+
- LGTM: No issues found (return empty issues array)
|
|
188
|
+
|
|
189
|
+
IMPORTANT:
|
|
190
|
+
- Return ONLY valid JSON, no markdown or explanation
|
|
191
|
+
- Each issue MUST have a fixPrompt that explains exactly how to fix it
|
|
192
|
+
- If the file is clean, return {"severity": "LGTM", "issues": []}
|
|
193
|
+
|
|
194
|
+
Context: ${relatedContext.slice(0, 800)}`;
|
|
195
|
+
const findings = [];
|
|
196
|
+
let chunkIndex = 0;
|
|
197
|
+
let skippedCount = 0;
|
|
198
|
+
console.log(chalk_1.default.blue("\n⚖️ Sending to Judge...\n"));
|
|
199
|
+
for (const chunk of fileChunks) {
|
|
200
|
+
chunkIndex++;
|
|
201
|
+
const fileName = chunk.match(/diff --git a\/(.+?) /)?.[1] || `Chunk ${chunkIndex}`;
|
|
202
|
+
// Skip excluded files
|
|
203
|
+
if (shouldSkipFile(fileName)) {
|
|
204
|
+
console.log(chalk_1.default.dim(` ⏭️ Skipping ${fileName} (excluded)`));
|
|
205
|
+
skippedCount++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const chunkTokens = estimateTokens(chunk) + estimateTokens(systemPrompt);
|
|
209
|
+
// Skip huge files
|
|
210
|
+
if (chunkTokens > MAX_TOKENS_PER_REQUEST) {
|
|
211
|
+
console.log(chalk_1.default.yellow(` ⚠️ Skipping ${fileName} (too large: ~${chunkTokens} tokens)`));
|
|
212
|
+
findings.push({
|
|
213
|
+
file: fileName,
|
|
214
|
+
severity: config_1.Config.DEFAULT_SEVERITY,
|
|
215
|
+
issues: [{
|
|
216
|
+
title: 'File too large for review',
|
|
217
|
+
description: `This file has ~${chunkTokens} tokens which exceeds the limit.`,
|
|
218
|
+
fixPrompt: 'Consider splitting this file into smaller modules.'
|
|
219
|
+
}]
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
process.stdout.write(chalk_1.default.dim(` 📄 ${fileName}...`));
|
|
224
|
+
try {
|
|
225
|
+
const { text } = await (0, ai_1.generateText)({
|
|
226
|
+
model: groq(MODEL_NAME),
|
|
227
|
+
system: systemPrompt,
|
|
228
|
+
prompt: `REVIEW THIS DIFF:\n\n${chunk}`,
|
|
229
|
+
temperature: 0.1,
|
|
230
|
+
});
|
|
231
|
+
const response = parseReviewResponse(text);
|
|
232
|
+
findings.push({
|
|
233
|
+
file: fileName,
|
|
234
|
+
severity: response.severity,
|
|
235
|
+
issues: response.issues
|
|
236
|
+
});
|
|
237
|
+
const emoji = response.severity === 'LGTM' ? '✅' :
|
|
238
|
+
response.severity === 'P0_CRITICAL' ? '🛑' :
|
|
239
|
+
response.severity === 'P1_HIGH' ? '⚠️' : '📝';
|
|
240
|
+
console.log(` ${emoji}`);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.log(chalk_1.default.red(` ❌ (${error.message.slice(0, 50)})`));
|
|
244
|
+
findings.push({
|
|
245
|
+
file: fileName,
|
|
246
|
+
severity: config_1.Config.DEFAULT_SEVERITY,
|
|
247
|
+
issues: [{
|
|
248
|
+
title: 'Review failed',
|
|
249
|
+
description: error.message,
|
|
250
|
+
fixPrompt: 'Retry the review or check manually.'
|
|
251
|
+
}]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// Rate limit delay
|
|
255
|
+
if (chunkIndex < fileChunks.length) {
|
|
256
|
+
await sleep(DELAY_BETWEEN_CHUNKS_MS);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// 6. Build Dashboard Report
|
|
260
|
+
const metrics = {
|
|
261
|
+
filesScanned: fileChunks.length - skippedCount,
|
|
262
|
+
totalTokens,
|
|
263
|
+
violations: findings.reduce((sum, f) => sum + f.issues.length, 0),
|
|
264
|
+
critical: findings.filter(f => f.severity === 'P0_CRITICAL').length,
|
|
265
|
+
high: findings.filter(f => f.severity === 'P1_HIGH').length,
|
|
266
|
+
medium: findings.filter(f => f.severity === 'P2_MEDIUM').length,
|
|
267
|
+
lgtm: findings.filter(f => f.severity === 'LGTM').length,
|
|
268
|
+
};
|
|
269
|
+
const report = {
|
|
270
|
+
date: new Date().toISOString().split('T')[0],
|
|
271
|
+
reviewer: 'Detective Engine & Judge',
|
|
272
|
+
status: (0, dashboard_1.determineStatus)(metrics),
|
|
273
|
+
metrics,
|
|
274
|
+
findings,
|
|
275
|
+
recommendedAction: (0, dashboard_1.generateRecommendation)(metrics)
|
|
276
|
+
};
|
|
277
|
+
// 7. Render and Save Dashboard
|
|
278
|
+
const dashboard = (0, dashboard_1.renderDashboard)(report);
|
|
279
|
+
// Ensure .jstar directory exists
|
|
280
|
+
fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true });
|
|
281
|
+
fs.writeFileSync(OUTPUT_FILE, dashboard);
|
|
282
|
+
console.log("\n" + chalk_1.default.bold.green("📊 DASHBOARD GENERATED"));
|
|
283
|
+
console.log(chalk_1.default.dim(` Saved to: ${OUTPUT_FILE}`));
|
|
284
|
+
console.log("\n" + chalk_1.default.bold.white("─".repeat(50)));
|
|
285
|
+
// Print summary to console
|
|
286
|
+
const statusEmoji = report.status === 'APPROVED' ? '🟢' :
|
|
287
|
+
report.status === 'NEEDS_REVIEW' ? '🟡' : '🔴';
|
|
288
|
+
console.log(`\n${statusEmoji} Status: ${report.status.replace('_', ' ')}`);
|
|
289
|
+
console.log(` 🛑 Critical: ${metrics.critical}`);
|
|
290
|
+
console.log(` ⚠️ High: ${metrics.high}`);
|
|
291
|
+
console.log(` 📝 Medium: ${metrics.medium}`);
|
|
292
|
+
console.log(` ✅ LGTM: ${metrics.lgtm}`);
|
|
293
|
+
console.log(`\n💡 ${report.recommendedAction}`);
|
|
294
|
+
console.log(chalk_1.default.dim(`\n📄 Full report: ${OUTPUT_FILE}`));
|
|
295
|
+
}
|
|
296
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* J-Star Reviewer Types
|
|
4
|
+
* Structured types for dashboard output
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.EMPTY_REVIEW = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* Default empty response for parse failures
|
|
10
|
+
*/
|
|
11
|
+
exports.EMPTY_REVIEW = {
|
|
12
|
+
severity: 'LGTM',
|
|
13
|
+
issues: []
|
|
14
|
+
};
|
package/package.json
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jstar-reviewer",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.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
|
-
"index:init": "ts-node scripts/indexer.ts --init",
|
|
10
|
-
"index:watch": "ts-node scripts/indexer.ts --watch",
|
|
11
|
-
"review": "ts-node scripts/reviewer.ts",
|
|
12
|
-
"detect": "ts-node scripts/detective.ts",
|
|
13
|
-
"prepare": "husky install"
|
|
14
|
-
},
|
|
15
8
|
"keywords": [
|
|
16
9
|
"code-review",
|
|
17
10
|
"ai",
|
|
@@ -50,6 +43,7 @@
|
|
|
50
43
|
"files": [
|
|
51
44
|
"bin/",
|
|
52
45
|
"scripts/",
|
|
46
|
+
"dist/",
|
|
53
47
|
"README.md"
|
|
54
48
|
],
|
|
55
49
|
"main": "setup.js",
|
|
@@ -60,5 +54,12 @@
|
|
|
60
54
|
"type": "commonjs",
|
|
61
55
|
"bugs": {
|
|
62
56
|
"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"
|
|
63
64
|
}
|
|
64
65
|
}
|