mirror of
https://github.com/zilliztech/claude-context.git
synced 2025-10-06 01:10:02 +03:00
resolved 5 conflicts in chrome & core
This commit is contained in:
18
package.json
18
package.json
@@ -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"
|
||||
}
|
||||
|
||||
58
packages/chrome-extension/package.json
Normal file
58
packages/chrome-extension/package.json
Normal 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"
|
||||
}
|
||||
33
packages/chrome-extension/scripts/generate-icons.js
Normal file
33
packages/chrome-extension/scripts/generate-icons.js
Normal 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();
|
||||
496
packages/chrome-extension/src/background.ts
Normal file
496
packages/chrome-extension/src/background.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
323
packages/chrome-extension/src/content.ts
Normal file
323
packages/chrome-extension/src/content.ts
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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 });
|
||||
9
packages/chrome-extension/src/icons/icon128.svg
Normal file
9
packages/chrome-extension/src/icons/icon128.svg
Normal 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 |
9
packages/chrome-extension/src/icons/icon16.svg
Normal file
9
packages/chrome-extension/src/icons/icon16.svg
Normal 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 |
9
packages/chrome-extension/src/icons/icon32.svg
Normal file
9
packages/chrome-extension/src/icons/icon32.svg
Normal 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 |
9
packages/chrome-extension/src/icons/icon48.svg
Normal file
9
packages/chrome-extension/src/icons/icon48.svg
Normal 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 |
46
packages/chrome-extension/src/manifest.json
Normal file
46
packages/chrome-extension/src/manifest.json
Normal 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>" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
140
packages/chrome-extension/src/options.html
Normal file
140
packages/chrome-extension/src/options.html
Normal 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>
|
||||
157
packages/chrome-extension/src/options.ts
Normal file
157
packages/chrome-extension/src/options.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
});
|
||||
310
packages/chrome-extension/src/styles.css
Normal file
310
packages/chrome-extension/src/styles.css
Normal 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;
|
||||
}
|
||||
1
packages/chrome-extension/src/vm-stub.js
Normal file
1
packages/chrome-extension/src/vm-stub.js
Normal file
@@ -0,0 +1 @@
|
||||
// This file is intentionally left blank to act as a stub for the 'vm' module in the browser environment.
|
||||
16
packages/chrome-extension/tsconfig.json
Normal file
16
packages/chrome-extension/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
113
packages/chrome-extension/webpack.config.js
Normal file
113
packages/chrome-extension/webpack.config.js
Normal 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
|
||||
}
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './embedding';
|
||||
export * from './vectordb';
|
||||
export * from './types';
|
||||
export * from './indexer';
|
||||
export * from './sync/synchronizer';
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
- 'examples/*'
|
||||
- packages/*
|
||||
- examples/*
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- faiss-node
|
||||
|
||||
Reference in New Issue
Block a user