resolved 5 conflicts in chrome & core

This commit is contained in:
ShawnZheng
2025-06-20 15:08:33 +08:00
committed by Cheney Zhang
parent 50a3a12a1a
commit d6b52bfd72
22 changed files with 2034 additions and 32 deletions

View File

@@ -23,10 +23,17 @@
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.8.3",
"eslint": "^9.25.1",
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1"
"@typescript-eslint/parser": "^8.31.1",
"assert": "^2.1.0",
"browserify-zlib": "^0.2.0",
"eslint": "^9.25.1",
"https-browserify": "^1.0.0",
"os-browserify": "^0.3.0",
"stream-http": "^3.2.0",
"typescript": "^5.8.3",
"url": "^0.11.4",
"vm-browserify": "^1.1.2"
},
"engines": {
"node": ">=20.0.0",
@@ -37,5 +44,6 @@
"url": "https://github.com/zilliztech/CodeIndexer.git"
},
"license": "MIT",
"author": "Cheney Zhang <277584121@qq.com>"
}
"author": "Cheney Zhang <277584121@qq.com>",
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
}

View File

@@ -0,0 +1,58 @@
{
"name": "@code-indexer/chrome-extension",
"version": "0.0.1",
"description": "CodeIndexer Chrome extension for web-based code indexing",
"private": true,
"scripts": {
"prebuild": "node scripts/generate-icons.js",
"build": "webpack --mode=production",
"dev": "webpack --mode=development --watch",
"clean": "rm -rf dist",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@code-indexer/core": "workspace:*",
"@xenova/transformers": "^2.17.2"
},
"overrides": {
"@zilliz/milvus2-sdk-node": false
},
"devDependencies": {
"@types/chrome": "^0.0.246",
"@types/node": "^20.0.0",
"assert": "^2.1.0",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"copy-webpack-plugin": "^11.0.0",
"crypto-browserify": "^3.12.1",
"https-browserify": "^1.0.0",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"terser-webpack-plugin": "^5.3.14",
"ts-loader": "^9.0.0",
"typescript": "^5.0.0",
"url": "^0.11.4",
"util": "^0.12.5",
"vm-browserify": "^1.1.2",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/zilliztech/CodeIndexer.git",
"directory": "packages/chrome-extension"
},
"license": "MIT",
"main": "content.js",
"keywords": [],
"author": "",
"bugs": {
"url": "https://github.com/zilliztech/CodeIndexer/issues"
},
"homepage": "https://github.com/zilliztech/CodeIndexer#readme"
}

View File

@@ -0,0 +1,33 @@
const fs = require('fs');
const path = require('path');
const ICON_SIZES = [16, 32, 48, 128];
const ICONS_DIR = path.join(__dirname, '../src/icons');
// Create base icon - a simple colored square with text
function generateBaseIcon() {
// Ensure icons directory exists
if (!fs.existsSync(ICONS_DIR)) {
fs.mkdirSync(ICONS_DIR, { recursive: true });
}
// Simple SVG icon for all sizes
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" fill="#4A90E2"/>
<text x="64" y="64" font-family="Arial" font-size="40"
fill="white" text-anchor="middle" dominant-baseline="middle">
CS
</text>
</svg>
`;
// Save SVG file for each size (browsers will scale it automatically)
for (const size of ICON_SIZES) {
const iconPath = path.join(ICONS_DIR, `icon${size}.svg`);
fs.writeFileSync(iconPath, svg, 'utf8');
console.log(`Generated icon: ${iconPath}`);
}
}
generateBaseIcon();

View File

@@ -0,0 +1,496 @@
// OpenAI Embedding utilities
// The extension now relies on OpenAI's embeddings endpoint instead of a locally hosted model.
export {};
const EMBEDDING_DIM = 1536;
const MAX_TOKENS_PER_BATCH = 250000; // Conservative token limit per API request
const MAX_CHUNKS_PER_BATCH = 100; // Align with core logic
function cosSim(a: number[], b: number[]): number {
let dot = 0;
let normA = 0;
let normB = 0;
const len = Math.min(a.length, b.length);
for (let i = 0; i < len; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
if (normA === 0 || normB === 0) {
return 0;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
type EmbeddingFunction = (input: string | string[], options?: any) => Promise<{ data: number[] }>;
class EmbeddingModel {
static instance: EmbeddingFunction | null = null;
static async getInstance(_progress_callback: Function | undefined = undefined): Promise<EmbeddingFunction> {
if (this.instance === null) {
// Retrieve the OpenAI API key from extension storage.
const apiKey: string | undefined = await new Promise((resolve, reject) => {
chrome.storage.sync.get(['openaiToken'], (items) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(items.openaiToken as string | undefined);
}
});
});
if (!apiKey) {
throw new Error('OpenAI API key is not configured.');
}
// Define the embedding function that wraps the OpenAI embeddings endpoint.
const embed: EmbeddingFunction = async (input: string | string[], _opts: any = {}): Promise<{ data: number[] }> => {
const inputs = Array.isArray(input) ? input : [input];
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'text-embedding-3-small',
input: inputs,
}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`OpenAI API error: ${response.status} - ${text}`);
}
const json = await response.json();
const vectors: number[][] = json.data.map((d: any) => d.embedding as number[]);
const flattened: number[] = ([] as number[]).concat(...vectors);
return { data: flattened };
};
this.instance = embed;
}
return this.instance;
}
}
class VectorDB {
private dbName: string;
private db: IDBDatabase | null;
constructor(dbName = 'CodeVectorDB') {
this.dbName = dbName;
this.db = null;
}
async open(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (event) => {
this.db = (event.target as IDBOpenDBRequest).result;
if (!this.db.objectStoreNames.contains('code_chunks')) {
const store = this.db.createObjectStore('code_chunks', { autoIncrement: true });
store.createIndex('repoId', 'repoId', { unique: false });
}
};
request.onsuccess = (event) => {
this.db = (event.target as IDBOpenDBRequest).result;
resolve();
};
request.onerror = (event) => {
reject('Error opening IndexedDB: ' + (event.target as IDBOpenDBRequest).error);
};
});
}
async addChunks(chunks: any[]): Promise<void> {
if (!this.db) await this.open();
const transaction = this.db!.transaction(['code_chunks'], 'readwrite');
const store = transaction.objectStore('code_chunks');
for (const chunk of chunks) {
store.add(chunk);
}
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = (event) => reject('Transaction error: ' + (event.target as IDBOpenDBRequest).error);
});
}
async getAllChunks(repoId: string): Promise<any[]> {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['code_chunks'], 'readonly');
const store = transaction.objectStore('code_chunks');
const index = store.index('repoId');
const request = index.getAll(repoId);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = (event) => {
reject('Error getting chunks: ' + (event.target as IDBOpenDBRequest).error);
};
});
}
async clearRepo(repoId: string): Promise<void> {
if (!this.db) await this.open();
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['code_chunks'], 'readwrite');
const store = transaction.objectStore('code_chunks');
const index = store.index('repoId');
const request = index.openKeyCursor(IDBKeyRange.only(repoId));
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
store.delete(cursor.primaryKey);
cursor.continue();
} else {
resolve();
}
};
request.onerror = (event) => reject('Error clearing repo: ' + (event.target as IDBOpenDBRequest).error);
});
}
}
class GitHubAPI {
token: string;
headers: { [key: string]: string };
constructor(token: string) {
this.token = token;
this.headers = {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github.v3+json',
};
}
async getRepoTree(owner: string, repo: string, branch: string = 'main'): Promise<any[]> {
const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
const response = await fetch(url, { headers: this.headers });
if (!response.ok) {
// If main branch fails, try master
if (branch === 'main') {
return this.getRepoTree(owner, repo, 'master');
}
throw new Error(`GitHub API error: ${response.status}`);
}
const data = await response.json();
return data.tree.filter((file: any) => file.type === 'blob' && !file.path.includes('.git'));
}
async getFileContent(owner: string, repo: string, fileSha: string): Promise<string> {
const url = `https://api.github.com/repos/${owner}/${repo}/git/blobs/${fileSha}`;
const response = await fetch(url, { headers: this.headers });
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const data = await response.json();
return atob(data.content);
}
}
const db = new VectorDB();
// Mapping from tabId to repoId to track which repository was indexed in which tab.
const tabRepoMap: Record<number, string> = {};
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'START_INDEXING') {
if (sender.tab?.id) {
const repoId = `${message.owner}/${message.repo}`;
// Track the repo associated with this tab for later cleanup.
tabRepoMap[sender.tab.id] = repoId;
startIndexing(message.owner, message.repo, sender.tab.id);
}
return true;
} else if (message.type === 'SEARCH') {
// Wrap in try-catch to ensure we always send a response
try {
performSearch(message.owner, message.repo, message.query)
.then(sendResponse)
.catch(error => {
console.error('Search error:', error);
sendResponse([]);
});
return true; // Will respond asynchronously
} catch (error) {
console.error('Search error:', error);
sendResponse([]);
return false; // Responded synchronously
}
} else if (message.type === 'CHECK_INDEX_STATUS') {
try {
checkIndexStatus(message.repoId)
.then(sendResponse)
.catch(error => {
console.error('Check index status error:', error);
sendResponse(false);
});
return true; // Will respond asynchronously
} catch (error) {
console.error('Check index status error:', error);
sendResponse(false);
return false; // Responded synchronously
}
} else if (message.type === 'CLEAR_INDEX') {
try {
clearRepoIndex(message.repoId)
.then(sendResponse)
.catch(error => {
console.error('Clear index error:', error);
sendResponse(false);
});
return true; // Will respond asynchronously
} catch (error) {
console.error('Clear index error:', error);
sendResponse(false);
return false; // Responded synchronously
}
}
return false;
});
/**
* Batch chunks so that each batch's estimated token count does not exceed the limit.
* @param chunks Array of string chunks
* @param maxTokensPerBatch Maximum tokens per batch
* @returns Array of chunk batches
*/
function batchChunksByTokenLimit(
chunks: string[],
maxTokensPerBatch: number,
maxChunksPerBatch: number = MAX_CHUNKS_PER_BATCH
): string[][] {
const batches: string[][] = [];
let currentBatch: string[] = [];
let currentTokens = 0;
for (const chunk of chunks) {
const estimatedTokens = Math.ceil(chunk.length / 4);
const exceedsTokenLimit = currentTokens + estimatedTokens > maxTokensPerBatch;
const exceedsChunkLimit = currentBatch.length >= maxChunksPerBatch;
if (exceedsTokenLimit || exceedsChunkLimit) {
if (currentBatch.length > 0) {
batches.push(currentBatch);
currentBatch = [];
currentTokens = 0;
}
}
currentBatch.push(chunk);
currentTokens += estimatedTokens;
}
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
return batches;
}
async function sendTabMessage(tabId: number, message: any): Promise<void> {
try {
const tab = await chrome.tabs.get(tabId);
if (!tab) {
console.warn(`Tab ${tabId} does not exist`);
return;
}
await chrome.tabs.sendMessage(tabId, message).catch(error => {
console.warn(`Failed to send message to tab ${tabId}:`, error);
});
} catch (error) {
console.warn(`Error checking tab ${tabId}:`, error);
}
}
async function startIndexing(owner: string, repo: string, tabId: number) {
const repoId = `${owner}/${repo}`;
console.log(`Starting indexing for ${repoId}`);
try {
const { githubToken, chunkSize, chunkOverlap } = await new Promise<{ githubToken?: string; chunkSize?: number; chunkOverlap?: number }>((resolve) => {
chrome.storage.sync.get(['githubToken', 'chunkSize', 'chunkOverlap'], (items) => resolve(items));
});
const effectiveChunkSize = typeof chunkSize === 'number' ? chunkSize : 1000;
const effectiveChunkOverlap = typeof chunkOverlap === 'number' ? chunkOverlap : 200;
if (!githubToken) {
throw new Error('GitHub token not set.');
}
const github = new GitHubAPI(githubToken);
const files = await github.getRepoTree(owner, repo);
// Exclude non-code files such as documentation, images, audio, and video assets
const excludedExtensions = [
'.md', '.markdown',
// Image formats
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.tif', '.webp', '.svg',
// Audio formats
'.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma',
// Video formats
'.mp4', '.mkv', '.mov', '.avi', '.webm', '.flv', '.wmv', '.mpeg', '.mpg'
];
const codeFiles = files.filter((file: any) => {
const lowerPath = file.path.toLowerCase();
return !excludedExtensions.some(ext => lowerPath.endsWith(ext));
});
await db.clearRepo(repoId);
const model = await EmbeddingModel.getInstance();
const totalFiles = codeFiles.length;
let processedFiles = 0;
const startTime = performance.now();
for (const file of codeFiles) {
try {
const content = await github.getFileContent(owner, repo, file.sha);
// Split file into textual chunks
const rawChunks = chunkText(content, effectiveChunkSize, effectiveChunkOverlap);
// Remove chunks that are empty or contain only whitespace
const chunks = rawChunks.filter((c) => c.trim().length > 0);
if (chunks.length === 0) {
continue; // Skip files that yield no meaningful chunks
}
// Embed in batches that respect both token and chunk count limits
const chunkBatches = batchChunksByTokenLimit(chunks, MAX_TOKENS_PER_BATCH);
let allEmbeddings: number[] = [];
for (const batch of chunkBatches) {
const embeddings = await model(batch);
allEmbeddings = allEmbeddings.concat(embeddings.data);
}
const chunkData = chunks.map((chunk, i) => ({
repoId,
file_path: file.path,
chunk,
embedding: Array.from(allEmbeddings.slice(i * EMBEDDING_DIM, (i + 1) * EMBEDDING_DIM))
}));
await db.addChunks(chunkData);
} catch (error) {
console.error(`Error processing file ${file.path}:`, error);
}
processedFiles++;
await sendTabMessage(tabId, {
type: 'INDEXING_PROGRESS',
progress: processedFiles,
total: totalFiles
});
}
const duration = ((performance.now() - startTime) / 1000).toFixed(2);
await sendTabMessage(tabId, {
type: 'INDEXING_COMPLETE',
duration
});
} catch (error: any) {
console.error('Indexing failed:', error);
await sendTabMessage(tabId, {
type: 'INDEXING_COMPLETE',
error: error.message
});
}
}
async function performSearch(owner: string, repo: string, query: string): Promise<any[]> {
const repoId = `${owner}/${repo}`;
console.log(`Searching for "${query}" in ${repoId}`);
try {
const model = await EmbeddingModel.getInstance();
const queryEmbedding = await model(query);
const allChunks = await db.getAllChunks(repoId);
if (allChunks.length === 0) {
return [];
}
const results = allChunks.map(chunk => {
const similarity = cosSim(Array.from(queryEmbedding.data), chunk.embedding);
return { ...chunk, similarity };
});
results.sort((a, b) => b.similarity - a.similarity);
return results.slice(0, 10);
} catch (error) {
console.error('Search failed:', error);
return [];
}
}
function chunkText(text: string, chunkSize: number, overlap: number): string[] {
// Clamp overlap to be less than chunkSize to avoid infinite loops
const safeOverlap = Math.min(overlap, chunkSize - 1);
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length);
const chunk = text.slice(start, end);
chunks.push(chunk);
// Move the window forward while keeping the desired overlap
start += chunkSize - safeOverlap;
}
return chunks;
}
// Clean up stored embeddings when the user closes the tab that initiated indexing.
chrome.tabs.onRemoved.addListener((tabId) => {
const repoId = tabRepoMap[tabId];
if (repoId) {
db.clearRepo(repoId).catch((err) => {
console.error('Error clearing repo on tab close:', err);
});
delete tabRepoMap[tabId];
}
});
async function checkIndexStatus(repoId: string): Promise<boolean> {
try {
const chunks = await db.getAllChunks(repoId);
return chunks.length > 0;
} catch (error) {
console.error('Error checking index status:', error);
return false;
}
}
async function clearRepoIndex(repoId: string): Promise<boolean> {
try {
await db.clearRepo(repoId);
return true;
} catch (error) {
console.error('Error clearing index:', error);
return false;
}
}

View File

@@ -0,0 +1,323 @@
export {};
function isRepoHomePage() {
// Matches /user/repo or /user/repo/tree/branch but not /user/repo/issues etc.
return /^\/[^/]+\/[^/]+(\/tree\/[^/]+)?\/?$/.test(window.location.pathname);
}
function injectUI() {
if (!isRepoHomePage()) {
const existingContainer = document.getElementById('code-search-container');
if (existingContainer) {
existingContainer.remove();
}
return;
}
// Attempt to locate GitHub's sidebar first so the search UI aligns with the "About" section
const sidebar = document.querySelector('.Layout-sidebar') as HTMLElement | null;
// Fallback to repository navigation bar ("Code", "Issues", etc.) if sidebar is not present
const repoNav = document.querySelector('nav.UnderlineNav') as HTMLElement | null;
const existingContainer = document.getElementById('code-search-container');
if ((sidebar || repoNav) && !existingContainer) {
// Check if GitHub token is set
chrome.storage.sync.get('githubToken', (data) => {
const hasToken = !!data.githubToken;
// Prevent duplicate insertion in case multiple async callbacks race
if (document.getElementById('code-search-container')) {
return;
}
const container = document.createElement('div');
container.id = 'code-search-container';
container.className = 'Box color-border-muted mb-3';
container.innerHTML = `
<div class="Box-header color-bg-subtle d-flex flex-items-center">
<h2 class="Box-title flex-auto">Code Search</h2>
<a href="#" id="open-settings-link" class="Link--muted">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.03.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"></path>
</svg>
</a>
</div>
<div class="Box-body">
${!hasToken ? `
<div class="flash flash-warn mb-2">
GitHub token not set.
<a href="#" id="open-settings-link-warning" class="settings-link">Configure settings</a>
</div>
` : ''}
<div class="d-flex flex-column">
<div class="form-group">
<div class="d-flex flex-items-center mb-2" id="search-row">
<input type="text" id="search-input" class="form-control input-sm flex-1" placeholder="Search code..." ${!hasToken ? 'disabled' : ''}>
<button id="search-btn" class="btn btn-sm ml-2" ${!hasToken ? 'disabled' : ''}>
Search
</button>
</div>
<div class="buttons-container">
<button id="index-repo-btn" class="btn btn-sm" ${!hasToken ? 'disabled' : ''}>
Index Repository
</button>
<button id="clear-index-btn" class="btn btn-sm" ${!hasToken ? 'disabled' : ''}>
Clear Index
</button>
</div>
</div>
<div id="search-results" class="Box mt-2" style="display:none;"></div>
<div id="indexing-status" class="color-fg-muted text-small mt-2"></div>
</div>
</div>
`;
// If sidebar is available, place container at the top; otherwise fallback to below nav bar
if (sidebar) {
sidebar.prepend(container);
} else if (repoNav) {
repoNav.parentElement?.insertBefore(container, repoNav.nextSibling);
}
document.getElementById('index-repo-btn')?.addEventListener('click', startIndexing);
document.getElementById('clear-index-btn')?.addEventListener('click', clearIndex);
document.getElementById('search-btn')?.addEventListener('click', handleSearch);
document.getElementById('search-input')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleSearch();
}
});
// Add event listeners for settings links
document.getElementById('open-settings-link')?.addEventListener('click', (e) => {
e.preventDefault();
const optionsUrl = chrome.runtime.getURL('options.html');
window.open(optionsUrl, '_blank');
});
document.getElementById('open-settings-link-warning')?.addEventListener('click', (e) => {
e.preventDefault();
const optionsUrl = chrome.runtime.getURL('options.html');
window.open(optionsUrl, '_blank');
});
// Check if repository is already indexed
checkIndexStatus();
});
}
}
function startIndexing() {
const [owner, repo] = window.location.pathname.slice(1).split('/');
console.log('Start indexing for:', owner, repo);
const statusEl = document.getElementById('indexing-status');
if(statusEl) statusEl.textContent = 'Starting indexing...';
const indexBtn = document.getElementById('index-repo-btn') as HTMLButtonElement;
const clearBtn = document.getElementById('clear-index-btn') as HTMLButtonElement;
const searchBtn = document.getElementById('search-btn') as HTMLButtonElement;
const searchInput = document.getElementById('search-input') as HTMLInputElement;
if(indexBtn) indexBtn.disabled = true;
if(clearBtn) clearBtn.disabled = true;
if(searchBtn) searchBtn.disabled = true;
if(searchInput) searchInput.disabled = true;
chrome.runtime.sendMessage({ type: 'START_INDEXING', owner, repo });
}
async function checkIndexStatus() {
const [owner, repo] = window.location.pathname.slice(1).split('/');
const repoId = `${owner}/${repo}`;
try {
chrome.runtime.sendMessage({ type: 'CHECK_INDEX_STATUS', repoId }, (response) => {
if (chrome.runtime.lastError) {
console.error('Error checking index status:', chrome.runtime.lastError);
updateUIState(false);
return;
}
updateUIState(response);
});
} catch (error) {
console.error('Error sending check index status message:', error);
updateUIState(false);
}
}
function updateUIState(isIndexed: boolean) {
const indexBtn = document.getElementById('index-repo-btn') as HTMLButtonElement;
const clearBtn = document.getElementById('clear-index-btn') as HTMLButtonElement;
const searchBtn = document.getElementById('search-btn') as HTMLButtonElement;
const searchInput = document.getElementById('search-input') as HTMLInputElement;
if (isIndexed) {
if (indexBtn) {
indexBtn.textContent = 'Re-Index Repository';
indexBtn.title = 'Re-index the repository to update the search index';
indexBtn.disabled = false;
}
if (clearBtn) {
clearBtn.disabled = false;
}
if (searchBtn) searchBtn.disabled = false;
if (searchInput) searchInput.disabled = false;
const statusEl = document.getElementById('indexing-status');
if (statusEl) statusEl.textContent = 'Repository is indexed and ready for search';
} else {
if (indexBtn) {
indexBtn.textContent = 'Index Repository';
indexBtn.title = 'Index the repository to enable code search';
indexBtn.disabled = false;
}
if (clearBtn) {
clearBtn.disabled = true;
}
if (searchBtn) searchBtn.disabled = true;
if (searchInput) searchInput.disabled = true;
const statusEl = document.getElementById('indexing-status');
if (statusEl) statusEl.textContent = 'Repository needs to be indexed before searching';
}
}
function handleSearch() {
const inputElement = document.getElementById('search-input') as HTMLInputElement;
const query = inputElement.value.trim();
const resultsContainer = document.getElementById('search-results');
const searchButton = document.getElementById('search-btn') as HTMLButtonElement;
if (!query || query.length < 3) {
if(resultsContainer) resultsContainer.style.display = 'none';
return;
}
if(searchButton) searchButton.disabled = true;
const [owner, repo] = window.location.pathname.slice(1).split('/');
try {
chrome.runtime.sendMessage({ type: 'SEARCH', owner, repo, query }, (response) => {
if (chrome.runtime.lastError) {
console.error('Search error:', chrome.runtime.lastError);
if(searchButton) searchButton.disabled = false;
const statusEl = document.getElementById('indexing-status');
if (statusEl) statusEl.textContent = 'Search failed: ' + chrome.runtime.lastError.message;
return;
}
displayResults(response || []);
if(searchButton) searchButton.disabled = false;
});
} catch (error) {
console.error('Error sending search message:', error);
if(searchButton) searchButton.disabled = false;
const statusEl = document.getElementById('indexing-status');
if (statusEl) statusEl.textContent = 'Search failed: ' + error;
}
}
function displayResults(results: any[]) {
const resultsContainer = document.getElementById('search-results');
if (!resultsContainer) return;
if (!results || results.length === 0) {
resultsContainer.style.display = 'none';
return;
}
resultsContainer.innerHTML = '';
resultsContainer.style.display = 'block';
const list = document.createElement('ul');
list.className = 'list-style-none';
results.forEach(result => {
const item = document.createElement('li');
// Format the file path to show it nicely
const filePath = result.file_path;
const fileExt = filePath.split('.').pop();
item.innerHTML = `
<div class="d-flex flex-items-center">
<svg class="octicon mr-2 color-fg-muted" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<path fill-rule="evenodd" d="M3.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25V6H9.75A1.75 1.75 0 018 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V1.75z"></path>
</svg>
<a href="https://github.com/${result.repoId}/blob/main/${result.file_path}" class="Link--primary flex-auto">
${result.file_path}
</a>
<span class="Label Label--secondary ml-1">${fileExt}</span>
</div>
<div class="mt-2 color-bg-subtle rounded-2 position-relative">
<div class="position-absolute right-0 top-0 pr-2 pt-1">
<span class="Label" title="Similarity score">
${(result.similarity * 100).toFixed(1)}%
</span>
</div>
<div class="p-2">
<code class="f6">${escapeHtml(result.chunk)}</code>
</div>
</div>
`;
list.appendChild(item);
});
resultsContainer.appendChild(list);
}
function escapeHtml(unsafe: string) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function clearIndex() {
const [owner, repo] = window.location.pathname.slice(1).split('/');
const repoId = `${owner}/${repo}`;
const clearBtn = document.getElementById('clear-index-btn') as HTMLButtonElement;
if (clearBtn) clearBtn.disabled = true;
try {
chrome.runtime.sendMessage({ type: 'CLEAR_INDEX', repoId }, (response) => {
if (chrome.runtime.lastError) {
console.error('Error clearing index:', chrome.runtime.lastError);
if (clearBtn) clearBtn.disabled = false;
const statusEl = document.getElementById('indexing-status');
if (statusEl) statusEl.textContent = 'Failed to clear index: ' + chrome.runtime.lastError.message;
return;
}
if (response) {
updateUIState(false);
const statusEl = document.getElementById('indexing-status');
if (statusEl) statusEl.textContent = 'Index cleared. Repository needs to be indexed before searching';
} else {
if (clearBtn) clearBtn.disabled = false;
const statusEl = document.getElementById('indexing-status');
if (statusEl) statusEl.textContent = 'Failed to clear index';
}
});
} catch (error) {
console.error('Error sending clear index message:', error);
if (clearBtn) clearBtn.disabled = false;
const statusEl = document.getElementById('indexing-status');
if (statusEl) statusEl.textContent = 'Failed to clear index: ' + error;
}
}
// Inject UI when the page is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectUI);
} else {
injectUI();
}
// Also handle dynamic page loads in GitHub
new MutationObserver((mutations, observer) => {
injectUI();
}).observe(document.body, { childList: true, subtree: true });

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" fill="#4A90E2"/>
<text x="64" y="64" font-family="Arial" font-size="40"
fill="white" text-anchor="middle" dominant-baseline="middle">
CS
</text>
</svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" fill="#4A90E2"/>
<text x="64" y="64" font-family="Arial" font-size="40"
fill="white" text-anchor="middle" dominant-baseline="middle">
CS
</text>
</svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" fill="#4A90E2"/>
<text x="64" y="64" font-family="Arial" font-size="40"
fill="white" text-anchor="middle" dominant-baseline="middle">
CS
</text>
</svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect width="128" height="128" fill="#4A90E2"/>
<text x="64" y="64" font-family="Arial" font-size="40"
fill="white" text-anchor="middle" dominant-baseline="middle">
CS
</text>
</svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1,46 @@
{
"manifest_version": 3,
"name": "GitHub Code Vector Search",
"version": "1.0",
"description": "Index GitHub repository code and perform vector search.",
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'"
},
"action": {
"default_icon": {
"16": "icons/icon16.svg",
"32": "icons/icon32.svg",
"48": "icons/icon48.svg",
"128": "icons/icon128.svg"
},
"default_popup": "options.html"
},
"permissions": [
"storage",
"scripting",
"unlimitedStorage"
],
"host_permissions": [
"https://github.com/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://github.com/*/*"],
"js": ["content.js"],
"css": ["styles.css"]
}
],
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"web_accessible_resources": [
{
"resources": [ "ort-wasm-simd-threaded.wasm", "options.html", "options.js" ],
"matches": [ "<all_urls>" ]
}
]
}

View File

@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<title>GitHub Code Vector Search Settings</title>
<link href="https://unpkg.com/@primer/css@^20.2.4/dist/primer.css" rel="stylesheet" />
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
padding: 2rem;
max-width: 800px;
margin: 0 auto;
color: #24292f;
background-color: #f6f8fa;
}
.settings-container {
background-color: #ffffff;
border-radius: 6px;
border: 1px solid #d0d7de;
padding: 24px;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.header {
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #d0d7de;
}
.form-group {
margin-bottom: 16px;
}
.note {
font-size: 12px;
color: #57606a;
margin-top: 4px;
}
.flash {
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
display: none;
}
.flash-success {
background-color: #dafbe1;
color: #116329;
border: 1px solid #2da44e;
}
.flash-error {
background-color: #ffebe9;
color: #cf222e;
border: 1px solid #cf222e;
display: none;
}
#debug-area {
margin-top: 20px;
padding: 10px;
border: 1px solid #d0d7de;
border-radius: 6px;
background-color: #f6f8fa;
display: none;
}
/* Popup specific styles */
@media (max-width: 500px) {
body {
padding: 1rem;
min-width: 350px;
max-width: 400px;
}
.settings-container {
padding: 16px;
}
.header {
margin-bottom: 12px;
padding-bottom: 12px;
}
.form-group {
margin-bottom: 12px;
}
}
</style>
</head>
<body>
<div class="settings-container">
<div class="header">
<h2>GitHub Code Vector Search Settings</h2>
<p class="color-fg-muted">Configure your GitHub API token and OpenAI API key to enable repository indexing and vector search.</p>
</div>
<div id="save-success" class="flash flash-success">
Settings saved successfully!
</div>
<div id="save-error" class="flash flash-error">
Error saving settings. Check console for details.
</div>
<form id="settings-form">
<div class="form-group">
<label for="github-token" class="d-block mb-1">GitHub Personal Access Token</label>
<input type="password" id="github-token" class="form-control width-full" placeholder="ghp_xxxxxxxxxxxxxxxxxxxx">
<p class="note">
This token is used to access GitHub API for repository indexing.
<br>Create a token with <code>repo</code> scope at
<a href="https://github.com/settings/tokens" target="_blank">GitHub Developer Settings</a>.
</p>
</div>
<div class="form-group">
<label for="chunk-size" class="d-block mb-1">Chunk Size</label>
<input type="number" id="chunk-size" class="form-control" value="512" min="128" max="1024">
<p class="note">Number of tokens per chunk when indexing code files (default: 512)</p>
</div>
<div class="form-group">
<label for="chunk-overlap" class="d-block mb-1">Chunk Overlap</label>
<input type="number" id="chunk-overlap" class="form-control" value="128" min="0" max="256">
<p class="note">Overlap between consecutive chunks in tokens (default: 128)</p>
</div>
<div class="form-group">
<label for="openai-token" class="d-block mb-1">OpenAI API Key</label>
<input type="password" id="openai-token" class="form-control width-full" placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
<p class="note">
This key is used to generate embeddings via the OpenAI Embedding API.
<br>You can create it in your <a href="https://platform.openai.com/account/api-keys" target="_blank">OpenAI dashboard</a>.
</p>
</div>
<div class="form-actions">
<button id="save" class="btn btn-primary">Save Settings</button>
<button id="toggle-debug" class="btn ml-2" type="button">Show Debug Info</button>
</div>
</form>
<div id="debug-area">
<h3>Debug Information</h3>
<div id="debug-content"></div>
</div>
</div>
<script src="options.js"></script>
</body>
</html>

View File

@@ -0,0 +1,157 @@
// Helper function to add debug info
function addDebugInfo(message: string) {
const debugContent = document.getElementById('debug-content');
if (debugContent) {
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.textContent = `[${timestamp}] ${message}`;
debugContent.appendChild(entry);
}
console.log(message);
}
function saveOptions() {
const tokenInput = document.getElementById('github-token') as HTMLInputElement;
const openaiInput = document.getElementById('openai-token') as HTMLInputElement;
const chunkSizeInput = document.getElementById('chunk-size') as HTMLInputElement;
const chunkOverlapInput = document.getElementById('chunk-overlap') as HTMLInputElement;
if (tokenInput && chunkSizeInput && chunkOverlapInput) {
const token = tokenInput.value;
const openaiToken = openaiInput.value;
const chunkSize = parseInt(chunkSizeInput.value) || 512;
const chunkOverlap = parseInt(chunkOverlapInput.value) || 128;
// Validate chunk size and overlap
if (chunkSize < 128 || chunkSize > 1024) {
alert('Chunk size must be between 128 and 1024');
return;
}
if (chunkOverlap < 0 || chunkOverlap > 256) {
alert('Chunk overlap must be between 0 and 256');
return;
}
if (chunkOverlap >= chunkSize) {
alert('Chunk overlap must be less than chunk size');
return;
}
addDebugInfo(`Saving settings: githubToken=${token ? '***' : 'empty'}, openaiToken=${openaiToken ? '***' : 'empty'}, chunkSize=${chunkSize}, chunkOverlap=${chunkOverlap}`);
chrome.storage.sync.set({
githubToken: token,
openaiToken: openaiToken,
chunkSize: chunkSize,
chunkOverlap: chunkOverlap
}, () => {
if (chrome.runtime.lastError) {
const errorMsg = `Error saving settings: ${chrome.runtime.lastError.message}`;
addDebugInfo(errorMsg);
// Show error message
const errorFlash = document.getElementById('save-error');
if (errorFlash) {
errorFlash.textContent = errorMsg;
errorFlash.style.display = 'block';
setTimeout(() => {
errorFlash.style.display = 'none';
}, 5000);
}
return;
}
addDebugInfo('Settings saved successfully');
// Show success message
const successFlash = document.getElementById('save-success');
if (successFlash) {
successFlash.style.display = 'block';
setTimeout(() => {
successFlash.style.display = 'none';
}, 3000);
}
// Verify the settings were saved
chrome.storage.sync.get(['githubToken', 'openaiToken'], (items) => {
addDebugInfo(`Verified tokens saved: githubToken=${items.githubToken ? '***' : 'empty'}, openaiToken=${items.openaiToken ? '***' : 'empty'}`);
});
});
}
}
function restoreOptions() {
addDebugInfo('Restoring options...');
// Check if chrome.storage is available
if (!chrome.storage || !chrome.storage.sync) {
addDebugInfo('ERROR: chrome.storage.sync is not available!');
return;
}
chrome.storage.sync.get({
githubToken: '',
openaiToken: '',
chunkSize: 512,
chunkOverlap: 128
}, (items) => {
if (chrome.runtime.lastError) {
addDebugInfo(`Error loading settings: ${chrome.runtime.lastError.message}`);
return;
}
addDebugInfo(`Settings loaded: token=${items.githubToken ? '***' : 'empty'}, openaiToken=${items.openaiToken ? '***' : 'empty'}, chunkSize=${items.chunkSize}, chunkOverlap=${items.chunkOverlap}`);
const tokenInput = document.getElementById('github-token') as HTMLInputElement;
const openaiInput = document.getElementById('openai-token') as HTMLInputElement;
const chunkSizeInput = document.getElementById('chunk-size') as HTMLInputElement;
const chunkOverlapInput = document.getElementById('chunk-overlap') as HTMLInputElement;
if (tokenInput) {
tokenInput.value = items.githubToken;
}
if (openaiInput) {
openaiInput.value = items.openaiToken;
}
if (chunkSizeInput) {
chunkSizeInput.value = items.chunkSize.toString();
}
if (chunkOverlapInput) {
chunkOverlapInput.value = items.chunkOverlap.toString();
}
});
}
document.addEventListener('DOMContentLoaded', () => {
addDebugInfo('Options page loaded');
restoreOptions();
// Add browser info to debug
const userAgent = navigator.userAgent;
addDebugInfo(`Browser: ${userAgent}`);
// Check Chrome API availability
addDebugInfo(`chrome object available: ${typeof chrome !== 'undefined'}`);
addDebugInfo(`chrome.storage available: ${typeof chrome !== 'undefined' && !!chrome.storage}`);
addDebugInfo(`chrome.storage.sync available: ${typeof chrome !== 'undefined' && !!chrome.storage && !!chrome.storage.sync}`);
});
document.getElementById('save')?.addEventListener('click', (e) => {
e.preventDefault();
saveOptions();
});
document.getElementById('toggle-debug')?.addEventListener('click', () => {
const debugArea = document.getElementById('debug-area');
if (debugArea) {
if (debugArea.style.display === 'none' || !debugArea.style.display) {
debugArea.style.display = 'block';
} else {
debugArea.style.display = 'none';
}
}
});

View File

@@ -0,0 +1,310 @@
:root {
--primary-color: #6366f1;
--primary-hover: #4f46e5;
--secondary-color: #14b8a6;
--accent-color: #f59e0b;
--success-color: #22c55e;
--error-color: #ef4444;
--background-light: #f8fafc;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
#code-search-container {
margin-bottom: 24px;
margin-left: 0;
max-width: 1000px;
min-height: 200px;
box-shadow: var(--shadow-md);
border-radius: 12px;
border: 1px solid var(--border-color);
background: white;
padding: 24px;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
gap: 16px;
}
#code-search-container:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
#search-results {
order: 4;
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 12px;
background-color: var(--background-light);
margin-top: 8px;
width: 100%;
box-shadow: var(--shadow-sm);
position: relative;
transition: all 0.3s ease;
}
#search-results:hover {
box-shadow: var(--shadow-md);
}
#search-results ul {
list-style-type: none;
padding: 0;
margin: 0;
}
#search-results li {
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
background: white;
transition: all 0.2s ease-in-out;
}
#search-results li:last-child {
border-bottom: none;
}
#search-results li:hover {
background-color: var(--background-light);
transform: scale(1.01);
}
#search-results code {
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
padding: 20px;
margin: 16px -12px;
display: block;
overflow-x: auto;
width: calc(100% + 24px);
}
.input-group {
display: flex;
align-items: center;
gap: 12px;
order: 1;
margin-bottom: 8px;
}
#search-input {
flex: 1;
min-width: 0;
padding: 5px 12px;
font-size: 14px;
line-height: 20px;
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: white;
transition: all 0.2s ease;
}
#search-input:focus {
border-color: #0969da;
outline: none;
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.3);
}
.buttons-container {
display: flex;
flex-direction: column;
gap: 8px;
order: 2;
width: 100%;
margin-bottom: 16px;
}
#search-btn {
white-space: nowrap;
padding: 5px 16px;
font-size: 14px;
line-height: 20px;
background-color: #f6f8fa;
color: #24292f;
border: 1px solid rgba(27, 31, 36, 0.15);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
box-shadow: 0 1px 0 rgba(27, 31, 36, 0.04);
}
#search-btn:hover {
background-color: #f3f4f6;
border-color: rgba(27, 31, 36, 0.15);
}
#search-btn:active {
background-color: #ebecf0;
box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2);
}
#search-btn:disabled {
background-color: #f6f8fa;
border-color: rgba(27, 31, 36, 0.15);
color: rgba(27, 31, 36, 0.3);
cursor: not-allowed;
}
#index-repo-btn {
order: unset;
align-self: flex-start;
white-space: nowrap;
padding: 5px 16px;
font-size: 14px;
line-height: 20px;
background-color: #2da44e;
color: white;
border: 1px solid rgba(27, 31, 36, 0.15);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
box-shadow: 0 1px 0 rgba(27, 31, 36, 0.1);
width: fit-content;
}
#index-repo-btn:hover {
background-color: #2c974b;
transform: none;
box-shadow: 0 1px 0 rgba(27, 31, 36, 0.1);
}
#index-repo-btn:active {
background-color: #298e46;
box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2);
}
#index-repo-btn:disabled {
background-color: #94d3a2;
border-color: rgba(27, 31, 36, 0.15);
color: rgba(255, 255, 255, 0.8);
cursor: not-allowed;
}
#clear-index-btn {
order: unset;
align-self: flex-start;
white-space: nowrap;
padding: 5px 16px;
font-size: 14px;
line-height: 20px;
background-color: #f6f8fa;
color: #cf222e;
border: 1px solid rgba(27, 31, 36, 0.15);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
box-shadow: 0 1px 0 rgba(27, 31, 36, 0.04);
display: block;
width: fit-content;
}
#clear-index-btn:hover {
background-color: #f3f4f6;
color: #a40e26;
border-color: rgba(27, 31, 36, 0.15);
}
#clear-index-btn:active {
background-color: #ebecf0;
box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2);
}
#clear-index-btn:disabled {
background-color: #f6f8fa;
border-color: rgba(27, 31, 36, 0.15);
color: rgba(207, 34, 46, 0.5);
cursor: not-allowed;
}
.flash-warn {
color: var(--accent-color);
background: linear-gradient(to right, #fef3c7, #fffbeb);
border: 1px solid var(--accent-color);
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 20px;
animation: slideIn 0.3s ease-out;
}
#indexing-status {
order: 5;
font-size: 14px;
color: var(--text-secondary);
margin-top: 12px;
padding: 8px;
border-radius: 6px;
background-color: var(--background-light);
transition: all 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Progress bar animation */
.progress-bar {
height: 4px;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
border-radius: 2px;
transition: width 0.3s ease;
animation: shimmer 2s infinite linear;
background-size: 200% 100%;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Responsive adjustments */
@media (max-width: 1024px) {
#code-search-container {
max-width: 90%;
margin: 16px auto;
}
#search-results code {
margin: 16px -8px;
width: calc(100% + 16px);
padding: 16px;
}
}
/* Make sure our UI aligns with GitHub's UI */
.Layout-sidebar #code-search-container {
margin-top: 25px;
width: 100%;
}
.Layout-main > #code-search-container {
margin-left: 0;
}
#repo-content-pjax-container + #code-search-container,
nav.UnderlineNav + #code-search-container {
margin-top: 16px;
margin-left: 0;
}

View File

@@ -0,0 +1 @@
// This file is intentionally left blank to act as a stub for the 'vm' module in the browser environment.

View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"types": ["chrome"],
"noImplicitAny": false,
"outDir": "dist"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@@ -0,0 +1,113 @@
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
background: './src/background.ts',
content: './src/content.ts',
options: './src/options.ts'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
clean: true
},
devtool: false,
experiments: {
outputModule: false
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
fallback: {
"crypto": require.resolve("crypto-browserify"),
"stream": require.resolve("stream-browserify"),
"buffer": require.resolve("buffer"),
"path": require.resolve("path-browserify"),
"util": require.resolve("util"),
"process": require.resolve("process/browser"),
"vm": false,
"os": require.resolve("os-browserify/browser"),
"fs": false,
"tls": false,
"net": false,
"http": require.resolve("stream-http"),
"https": require.resolve("https-browserify"),
"zlib": require.resolve("browserify-zlib"),
"dns": false,
"child_process": false,
"http2": false,
"url": require.resolve("url"),
"assert": require.resolve("assert/"),
"module": false,
"worker_threads": false
},
alias: {
'process/browser': require.resolve('process/browser')
}
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'src/manifest.json', to: 'manifest.json' },
{ from: 'src/options.html', to: 'options.html' },
{ from: 'src/styles.css', to: 'styles.css' },
{ from: 'src/icons', to: 'icons' },
{
from: '../../node_modules/.pnpm/onnxruntime-web@*/node_modules/onnxruntime-web/dist/*.wasm',
to: '[name][ext]'
}
]
}),
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer']
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
'global': 'globalThis'
}),
new webpack.NormalModuleReplacementPlugin(
/^vm$/,
require.resolve('./src/vm-stub.js')
)
],
target: 'web',
optimization: {
minimize: true,
minimizer: [
new (require('terser-webpack-plugin'))({
terserOptions: {
compress: {
drop_console: false,
drop_debugger: true,
pure_funcs: ['console.debug']
},
mangle: {
safari10: true
},
output: {
comments: false,
safari10: true
}
},
extractComments: false
})
]
},
node: {
global: false,
__filename: false,
__dirname: false
}
};

View File

@@ -14,6 +14,7 @@
},
"dependencies": {
"@zilliz/milvus2-sdk-node": "^2.5.10",
"faiss-node": "^0.5.1",
"fs-extra": "^11.0.0",
"glob": "^10.0.0",
"langchain": "^0.3.27",
@@ -22,7 +23,12 @@
"voyageai": "^0.0.4"
},
"devDependencies": {
"@types/fs-extra": "^11.0.0"
"@types/fs-extra": "^11.0.0",
"@types/jest": "^30.0.0",
"@types/mock-fs": "^4.13.4",
"jest": "^30.0.0",
"mock-fs": "^5.5.0",
"ts-jest": "^29.4.0"
},
"files": [
"dist",

View File

@@ -3,3 +3,4 @@ export * from './embedding';
export * from './vectordb';
export * from './types';
export * from './indexer';
export * from './sync/synchronizer';

View File

@@ -17,6 +17,7 @@ import { SemanticSearchResult } from './types';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { FileSynchronizer } from './sync/synchronizer';
const DEFAULT_SUPPORTED_EXTENSIONS = [
// Programming languages
@@ -94,6 +95,7 @@ export class CodeIndexer {
private codeSplitter: Splitter;
private supportedExtensions: string[];
private ignorePatterns: string[];
private synchronizers = new Map<string, FileSynchronizer>();
constructor(config: CodeIndexerConfig = {}) {
// Initialize services
@@ -137,6 +139,11 @@ export class CodeIndexer {
): Promise<{ indexedFiles: number; totalChunks: number }> {
console.log(`🚀 Starting to index codebase: ${codebasePath}`);
// Initialize file synchronizer
const synchronizer = new FileSynchronizer(codebasePath);
await synchronizer.initialize();
this.synchronizers.set(this.getCollectionName(codebasePath), synchronizer);
// 1. Check and prepare vector collection
progressCallback?.({ phase: 'Preparing collection...', current: 0, total: 100, percentage: 0 });
await this.prepareCollection(codebasePath);
@@ -195,6 +202,87 @@ export class CodeIndexer {
};
}
async reindexByChange(
codebasePath: string,
progressCallback?: (progress: { phase: string; current: number; total: number; percentage: number }) => void
): Promise<{ added: number, removed: number, modified: number }> {
const collectionName = this.getCollectionName(codebasePath);
const synchronizer = this.synchronizers.get(collectionName);
if (!synchronizer) {
// To be safe, let's initialize if it's not there.
const newSynchronizer = new FileSynchronizer(codebasePath);
await newSynchronizer.initialize();
this.synchronizers.set(collectionName, newSynchronizer);
}
const currentSynchronizer = this.synchronizers.get(collectionName)!;
progressCallback?.({ phase: 'Checking for file changes...', current: 0, total: 100, percentage: 0 });
const { added, removed, modified } = await currentSynchronizer.checkForChanges();
const totalChanges = added.length + removed.length + modified.length;
if (totalChanges === 0) {
progressCallback?.({ phase: 'No changes detected', current: 100, total: 100, percentage: 100 });
console.log('✅ No file changes detected.');
return { added: 0, removed: 0, modified: 0 };
}
console.log(`🔄 Found changes: ${added.length} added, ${removed.length} removed, ${modified.length} modified.`);
let processedChanges = 0;
const updateProgress = (phase: string) => {
processedChanges++;
const percentage = Math.round((processedChanges / (removed.length + modified.length + added.length)) * 100);
progressCallback?.({ phase, current: processedChanges, total: totalChanges, percentage });
};
// Handle removed files
for (const file of removed) {
await this.deleteFileChunks(collectionName, file);
updateProgress(`Removed ${file}`);
}
// Handle modified files
for (const file of modified) {
await this.deleteFileChunks(collectionName, file);
updateProgress(`Deleted old chunks for ${file}`);
}
// Handle added and modified files
const filesToIndex = [...added, ...modified].map(f => path.join(codebasePath, f));
if (filesToIndex.length > 0) {
const batchSize = 10;
for (let i = 0; i < filesToIndex.length; i += batchSize) {
const batch = filesToIndex.slice(i, i + batchSize);
await this.processBatch(batch, codebasePath);
updateProgress(`Indexed a batch of ${batch.length} files`);
}
}
console.log(`✅ Re-indexing complete. Added: ${added.length}, Removed: ${removed.length}, Modified: ${modified.length}`);
progressCallback?.({ phase: 'Re-indexing complete!', current: totalChanges, total: totalChanges, percentage: 100 });
return { added: added.length, removed: removed.length, modified: modified.length };
}
private async deleteFileChunks(collectionName: string, relativePath: string): Promise<void> {
const results = await this.vectorDatabase.query(
collectionName,
`relativePath == "${relativePath}"`,
['id']
);
if (results.length > 0) {
const ids = results.map(r => r.id as string).filter(id => id);
if (ids.length > 0) {
await this.vectorDatabase.delete(collectionName, ids);
console.log(`Deleted ${ids.length} chunks for file ${relativePath}`);
}
}
}
/**
* Semantic search
* @param codebasePath Codebase path to search in
@@ -261,6 +349,9 @@ export class CodeIndexer {
await this.vectorDatabase.dropCollection(collectionName);
}
// Delete snapshot file
await FileSynchronizer.deleteSnapshot(codebasePath);
progressCallback?.({ phase: 'Index cleared', current: 100, total: 100, percentage: 100 });
console.log('✅ Index data cleaned');
}

View File

@@ -182,6 +182,17 @@ export class MilvusVectorDatabase implements VectorDatabase {
});
}
async query(collectionName: string, filter: string, outputFields: string[]): Promise<Record<string, any>[]> {
const result = await this.client.query({
collection_name: collectionName,
filter: filter,
output_fields: outputFields,
});
if (result.status.error_code !== 'Success') {
throw new Error(`Failed to query Milvus: ${result.status.reason}`);
}
return result.data;
}
}

View File

@@ -9,6 +9,7 @@ import { OpenAIEmbedding } from "@code-indexer/core";
import { MilvusVectorDatabase } from "@code-indexer/core";
import * as path from "path";
import * as fs from "fs";
import * as os from "os";
interface CodeIndexerMcpConfig {
name: string;
@@ -21,14 +22,21 @@ interface CodeIndexerMcpConfig {
class CodeIndexerMcpServer {
private server: Server;
private codeIndexer: CodeIndexer;
private currentCodebasePath: string | null = null;
private activeCodebasePath: string | null = null;
private indexedCodebases: string[] = [];
private indexingStats: { indexedFiles: number; totalChunks: number } | null = null;
private isSyncing: boolean = false;
private stateFilePath: string;
constructor(config: CodeIndexerMcpConfig) {
// Redirect console.log and console.warn to stderr to avoid JSON parsing issues
// Only MCP protocol messages should go to stdout
this.setupConsoleRedirection();
const stateDir = path.join(os.homedir(), '.codeindexer');
fs.mkdirSync(stateDir, { recursive: true });
this.stateFilePath = path.join(stateDir, 'mcp_state.json');
// Initialize MCP server
this.server = new Server(
{
@@ -59,6 +67,7 @@ class CodeIndexerMcpServer {
});
this.setupTools();
this.loadState();
}
private setupConsoleRedirection() {
@@ -75,6 +84,41 @@ class CodeIndexerMcpServer {
// Keep console.error unchanged as it already goes to stderr
}
private async saveState(): Promise<void> {
const state = {
activeCodebasePath: this.activeCodebasePath,
indexedCodebases: this.indexedCodebases,
indexingStats: this.indexingStats
};
try {
await fs.promises.writeFile(this.stateFilePath, JSON.stringify(state, null, 2));
console.log(`Saved MCP state to ${this.stateFilePath}`);
} catch (error) {
console.error('Failed to save MCP state:', error);
}
}
private loadState(): void {
try {
if (fs.existsSync(this.stateFilePath)) {
const stateJson = fs.readFileSync(this.stateFilePath, 'utf-8');
const state = JSON.parse(stateJson);
this.activeCodebasePath = state.activeCodebasePath || null;
this.indexedCodebases = state.indexedCodebases || [];
this.indexingStats = state.indexingStats || null;
if(this.activeCodebasePath) {
console.log(`Loaded MCP state from ${this.stateFilePath}. Active codebase: ${this.activeCodebasePath}`);
}
}
} catch (error) {
console.error('Failed to load MCP state:', error);
// Reset state if file is corrupt
this.activeCodebasePath = null;
this.indexedCodebases = [];
this.indexingStats = null;
}
}
private setupTools() {
// Define available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -99,6 +143,29 @@ class CodeIndexerMcpServer {
required: ["path"]
}
},
{
name: "switch_codebase",
description: "Switch the active codebase for search and sync",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Path to the codebase to switch to"
}
},
required: ["path"]
}
},
{
name: "list_indexed_codebases",
description: "List all indexed codebases",
inputSchema: {
type: "object",
properties: {},
required: []
}
},
{
name: "search_code",
description: "Search the indexed codebase using natural language queries",
@@ -144,6 +211,10 @@ class CodeIndexerMcpServer {
switch (name) {
case "index_codebase":
return await this.handleIndexCodebase(args);
case "switch_codebase":
return await this.handleSwitchCodebase(args);
case "list_indexed_codebases":
return await this.handleListCodebases();
case "search_code":
return await this.handleSearchCode(args);
case "clear_index":
@@ -185,44 +256,121 @@ class CodeIndexerMcpServer {
const absolutePath = path.resolve(codebasePath);
// Clear index if force is true
if (forceReindex && this.currentCodebasePath) {
await this.codeIndexer.clearIndex(this.currentCodebasePath);
if (forceReindex && this.activeCodebasePath) {
await this.codeIndexer.clearIndex(this.activeCodebasePath);
}
// Start indexing
const stats = await this.codeIndexer.indexCodebase(absolutePath);
// Store current codebase path and stats
this.currentCodebasePath = absolutePath;
this.activeCodebasePath = absolutePath;
if (!this.indexedCodebases.includes(absolutePath)) {
this.indexedCodebases.push(absolutePath);
}
this.indexingStats = stats;
await this.saveState();
return {
content: [{
type: "text",
text: `Successfully indexed codebase at '${absolutePath}'\n` +
`Indexed files: ${stats.indexedFiles}\n` +
`Total chunks: ${stats.totalChunks}`
text: `Successfully indexed codebase '${absolutePath}'.\nIndexed ${stats.indexedFiles} files, ${stats.totalChunks} chunks.`
}]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
} catch (error: any) {
console.error('Error during indexing:', error);
return {
content: [{
type: "text",
text: `Error indexing codebase: ${errorMessage}`
text: `Error indexing codebase: ${error.message}`
}],
isError: true
};
}
}
private async handleSwitchCodebase(args: any) {
const { path: codebasePath } = args;
const absolutePath = path.resolve(codebasePath);
if (!this.indexedCodebases.includes(absolutePath)) {
return {
content: [{
type: "text",
text: `Error: Codebase '${absolutePath}' is not indexed. Please index it first.`
}],
isError: true
};
}
this.activeCodebasePath = absolutePath;
await this.saveState();
return {
content: [{
type: "text",
text: `Switched active codebase to '${absolutePath}'`
}]
};
}
private async handleListCodebases() {
if (this.indexedCodebases.length === 0) {
return {
content: [{
type: "text",
text: "No codebases have been indexed yet."
}]
};
}
const codebaseList = this.indexedCodebases.map(p =>
p === this.activeCodebasePath ? `* ${p} (active)` : ` ${p}`
).join('\n');
return {
content: [{
type: "text",
text: `Available codebases:\n${codebaseList}`
}]
};
}
private async handleSyncIndex() {
if (!this.activeCodebasePath) {
// Silently return if no codebase is indexed
return;
}
if (this.isSyncing) {
console.log('Index sync already in progress. Skipping.');
return;
}
this.isSyncing = true;
console.log(`Starting index sync for '${this.activeCodebasePath}'...`);
try {
const stats = await this.codeIndexer.reindexByChange(this.activeCodebasePath);
if (stats.added > 0 || stats.removed > 0 || stats.modified > 0) {
console.log(`Index sync complete. Added: ${stats.added}, Removed: ${stats.removed}, Modified: ${stats.modified}`);
} else {
console.log('No changes detected for index sync.');
}
} catch (error: any) {
console.error('Error during index sync:', error);
} finally {
this.isSyncing = false;
}
}
private async handleSearchCode(args: any) {
const { query, limit } = args;
const { query, limit = 10 } = args;
const resultLimit = limit || 10;
try {
// Check if we have a current codebase path
if (!this.currentCodebasePath) {
if (!this.activeCodebasePath) {
return {
content: [{
type: "text",
@@ -233,7 +381,7 @@ class CodeIndexerMcpServer {
}
const searchResults = await this.codeIndexer.semanticSearch(
this.currentCodebasePath,
this.activeCodebasePath,
query,
Math.min(resultLimit, 50),
0.3
@@ -289,7 +437,7 @@ class CodeIndexerMcpServer {
};
}
if (!this.currentCodebasePath) {
if (!this.activeCodebasePath) {
return {
content: [{
type: "text",
@@ -299,11 +447,14 @@ class CodeIndexerMcpServer {
}
try {
await this.codeIndexer.clearIndex(this.currentCodebasePath);
const pathToClear = this.activeCodebasePath;
await this.codeIndexer.clearIndex(pathToClear);
// Reset state
this.currentCodebasePath = null;
this.indexedCodebases = this.indexedCodebases.filter(p => p !== pathToClear);
this.activeCodebasePath = null;
this.indexingStats = null;
await this.saveState();
return {
content: [{
@@ -333,15 +484,17 @@ class CodeIndexerMcpServer {
return content.substring(0, maxLength) + '...';
}
private startBackgroundSync() {
// Periodically check for file changes and update the index
setInterval(() => this.handleSyncIndex(), 5 * 1000); // every 5 minutes
}
async start() {
try {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("CodeIndexer MCP Server started successfully (stdio)");
} catch (error) {
console.error("Failed to start CodeIndexer MCP Server:", error);
process.exit(1);
}
console.log('Starting CodeIndexer MCP server...');
this.startBackgroundSync();
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.log("MCP server started and listening on stdio.");
}
}

View File

@@ -1,3 +1,6 @@
packages:
- 'packages/*'
- 'examples/*'
- packages/*
- examples/*
ignoredBuiltDependencies:
- faiss-node