23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
499 lines
18 KiB
TypeScript
499 lines
18 KiB
TypeScript
/**
|
|
* Semantic Code Search Plugin
|
|
*
|
|
* Index and search code semantically using @ruvector/wasm vector database.
|
|
* Enables natural language queries like "find functions that handle authentication".
|
|
*
|
|
* Features:
|
|
* - Index code chunks with embeddings
|
|
* - Natural language code search (<1ms with HNSW)
|
|
* - Symbol-aware chunking
|
|
* - Language-specific tokenization
|
|
* - Incremental indexing
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { semanticCodeSearchPlugin } from '@claude-flow/plugins/examples/ruvector-plugins';
|
|
* await getDefaultRegistry().register(semanticCodeSearchPlugin);
|
|
* ```
|
|
*/
|
|
|
|
import {
|
|
PluginBuilder,
|
|
MCPToolBuilder,
|
|
HookBuilder,
|
|
HookEvent,
|
|
HookPriority,
|
|
Security,
|
|
} from '../../src/index.js';
|
|
|
|
// Import shared vector utilities (consolidated from all plugins)
|
|
import {
|
|
IVectorDB,
|
|
createVectorDB,
|
|
generateHashEmbedding,
|
|
} from './shared/vector-utils.js';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
export interface CodeChunk {
|
|
id: string;
|
|
filePath: string;
|
|
content: string;
|
|
language: string;
|
|
startLine: number;
|
|
endLine: number;
|
|
symbolType?: 'function' | 'class' | 'method' | 'variable' | 'import' | 'type' | 'interface';
|
|
symbolName?: string;
|
|
embedding?: Float32Array;
|
|
metadata: {
|
|
size: number;
|
|
hash: string;
|
|
indexedAt: Date;
|
|
dependencies?: string[];
|
|
};
|
|
}
|
|
|
|
export interface CodeSearchResult {
|
|
chunk: CodeChunk;
|
|
similarity: number;
|
|
relevance: number;
|
|
highlights?: string[];
|
|
}
|
|
|
|
export interface CodeSearchOptions {
|
|
k?: number;
|
|
minSimilarity?: number;
|
|
languages?: string[];
|
|
symbolTypes?: CodeChunk['symbolType'][];
|
|
filePaths?: string[];
|
|
includeContent?: boolean;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Semantic Code Search Core
|
|
// ============================================================================
|
|
|
|
export class SemanticCodeSearch {
|
|
private vectorDb: IVectorDB | null = null;
|
|
private chunks = new Map<string, CodeChunk>();
|
|
private fileIndex = new Map<string, Set<string>>();
|
|
private dimensions: number;
|
|
private nextId = 1;
|
|
private initPromise: Promise<void> | null = null;
|
|
|
|
constructor(dimensions: number = 768) {
|
|
this.dimensions = dimensions;
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
if (this.vectorDb) return;
|
|
if (this.initPromise) return this.initPromise;
|
|
|
|
this.initPromise = (async () => {
|
|
this.vectorDb = await createVectorDB(this.dimensions);
|
|
})();
|
|
|
|
return this.initPromise;
|
|
}
|
|
|
|
private async ensureInitialized(): Promise<IVectorDB> {
|
|
await this.initialize();
|
|
return this.vectorDb!;
|
|
}
|
|
|
|
/**
|
|
* Index a code file by chunking and embedding.
|
|
*/
|
|
async indexFile(filePath: string, content: string, language: string): Promise<string[]> {
|
|
const db = await this.ensureInitialized();
|
|
|
|
const safePath = Security.validateString(filePath, { maxLength: 500 });
|
|
const safeContent = Security.validateString(content, { maxLength: 1_000_000 });
|
|
const safeLang = Security.validateString(language, { maxLength: 50 });
|
|
|
|
await this.removeFile(safePath);
|
|
|
|
const chunks = this.chunkCode(safeContent, safeLang, safePath);
|
|
const chunkIds: string[] = [];
|
|
|
|
for (const chunk of chunks) {
|
|
const id = `code-${this.nextId++}`;
|
|
const embedding = this.generateCodeEmbedding(chunk.content, chunk.symbolName, chunk.symbolType);
|
|
|
|
const fullChunk: CodeChunk = { ...chunk, id, embedding };
|
|
|
|
db.insert(embedding, id, {
|
|
filePath: chunk.filePath,
|
|
language: chunk.language,
|
|
symbolType: chunk.symbolType,
|
|
symbolName: chunk.symbolName,
|
|
startLine: chunk.startLine,
|
|
endLine: chunk.endLine,
|
|
});
|
|
|
|
this.chunks.set(id, fullChunk);
|
|
|
|
if (!this.fileIndex.has(safePath)) {
|
|
this.fileIndex.set(safePath, new Set());
|
|
}
|
|
this.fileIndex.get(safePath)!.add(id);
|
|
chunkIds.push(id);
|
|
}
|
|
|
|
return chunkIds;
|
|
}
|
|
|
|
/**
|
|
* Search code using natural language query (<1ms with HNSW).
|
|
*/
|
|
async search(query: string, options?: CodeSearchOptions): Promise<CodeSearchResult[]> {
|
|
const db = await this.ensureInitialized();
|
|
|
|
const safeQuery = Security.validateString(query, { maxLength: 1000 });
|
|
const k = options?.k ?? 10;
|
|
const minSimilarity = options?.minSimilarity ?? 0.3;
|
|
|
|
const queryEmbedding = this.generateQueryEmbedding(safeQuery);
|
|
const searchResults = db.search(queryEmbedding, k * 2);
|
|
|
|
const results: CodeSearchResult[] = [];
|
|
|
|
for (const result of searchResults) {
|
|
if (result.score < minSimilarity) continue;
|
|
|
|
const chunk = this.chunks.get(result.id);
|
|
if (!chunk) continue;
|
|
|
|
if (options?.languages && !options.languages.includes(chunk.language)) continue;
|
|
if (options?.symbolTypes && chunk.symbolType && !options.symbolTypes.includes(chunk.symbolType)) continue;
|
|
if (options?.filePaths) {
|
|
const matchesPath = options.filePaths.some(p => chunk.filePath.includes(p));
|
|
if (!matchesPath) continue;
|
|
}
|
|
|
|
const relevance = this.calculateRelevance(chunk, safeQuery);
|
|
const highlights = this.findHighlights(chunk.content, safeQuery);
|
|
|
|
results.push({
|
|
chunk: options?.includeContent === false ? { ...chunk, content: '' } : chunk,
|
|
similarity: result.score,
|
|
relevance,
|
|
highlights,
|
|
});
|
|
|
|
if (results.length >= k) break;
|
|
}
|
|
|
|
return results.sort((a, b) => (b.similarity * b.relevance) - (a.similarity * a.relevance));
|
|
}
|
|
|
|
/**
|
|
* Find similar code to a given snippet.
|
|
*/
|
|
async findSimilar(code: string, k: number = 5): Promise<CodeSearchResult[]> {
|
|
const db = await this.ensureInitialized();
|
|
|
|
const safeCode = Security.validateString(code, { maxLength: 10000 });
|
|
const embedding = this.generateCodeEmbedding(safeCode);
|
|
const searchResults = db.search(embedding, k);
|
|
|
|
return searchResults.map(r => {
|
|
const chunk = this.chunks.get(r.id)!;
|
|
return { chunk, similarity: r.score, relevance: 1 };
|
|
}).filter(r => r.chunk);
|
|
}
|
|
|
|
/**
|
|
* Remove all chunks for a file.
|
|
*/
|
|
async removeFile(filePath: string): Promise<number> {
|
|
const db = await this.ensureInitialized();
|
|
const chunkIds = this.fileIndex.get(filePath);
|
|
if (!chunkIds) return 0;
|
|
|
|
let removed = 0;
|
|
for (const id of chunkIds) {
|
|
db.delete(id);
|
|
this.chunks.delete(id);
|
|
removed++;
|
|
}
|
|
|
|
this.fileIndex.delete(filePath);
|
|
return removed;
|
|
}
|
|
|
|
/**
|
|
* Get index statistics.
|
|
*/
|
|
getStats(): {
|
|
totalChunks: number;
|
|
totalFiles: number;
|
|
byLanguage: Record<string, number>;
|
|
bySymbolType: Record<string, number>;
|
|
} {
|
|
const byLanguage: Record<string, number> = {};
|
|
const bySymbolType: Record<string, number> = {};
|
|
|
|
for (const chunk of this.chunks.values()) {
|
|
byLanguage[chunk.language] = (byLanguage[chunk.language] ?? 0) + 1;
|
|
if (chunk.symbolType) {
|
|
bySymbolType[chunk.symbolType] = (bySymbolType[chunk.symbolType] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
return {
|
|
totalChunks: this.chunks.size,
|
|
totalFiles: this.fileIndex.size,
|
|
byLanguage,
|
|
bySymbolType,
|
|
};
|
|
}
|
|
|
|
// =========================================================================
|
|
// Private Helpers
|
|
// =========================================================================
|
|
|
|
private chunkCode(content: string, language: string, filePath: string): Omit<CodeChunk, 'id' | 'embedding'>[] {
|
|
const chunks: Omit<CodeChunk, 'id' | 'embedding'>[] = [];
|
|
const lines = content.split('\n');
|
|
const patterns = this.getLanguagePatterns(language);
|
|
|
|
let currentChunk: string[] = [];
|
|
let chunkStartLine = 1;
|
|
let currentSymbol: { type: CodeChunk['symbolType']; name: string } | null = null;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const lineNum = i + 1;
|
|
const symbol = this.detectSymbol(line, patterns);
|
|
|
|
if (symbol && currentChunk.length > 0) {
|
|
chunks.push(this.createChunk(currentChunk.join('\n'), filePath, language, chunkStartLine, lineNum - 1, currentSymbol));
|
|
currentChunk = [];
|
|
chunkStartLine = lineNum;
|
|
}
|
|
|
|
if (symbol) currentSymbol = symbol;
|
|
currentChunk.push(line);
|
|
|
|
if (currentChunk.length >= 50 && !this.isInsideBlock(currentChunk)) {
|
|
chunks.push(this.createChunk(currentChunk.join('\n'), filePath, language, chunkStartLine, lineNum, currentSymbol));
|
|
currentChunk = [];
|
|
chunkStartLine = lineNum + 1;
|
|
currentSymbol = null;
|
|
}
|
|
}
|
|
|
|
if (currentChunk.length > 0) {
|
|
chunks.push(this.createChunk(currentChunk.join('\n'), filePath, language, chunkStartLine, lines.length, currentSymbol));
|
|
}
|
|
|
|
return chunks;
|
|
}
|
|
|
|
private createChunk(
|
|
content: string, filePath: string, language: string, startLine: number, endLine: number,
|
|
symbol: { type: CodeChunk['symbolType']; name: string } | null
|
|
): Omit<CodeChunk, 'id' | 'embedding'> {
|
|
return {
|
|
filePath, content, language, startLine, endLine,
|
|
symbolType: symbol?.type, symbolName: symbol?.name,
|
|
metadata: { size: content.length, hash: this.hashString(content), indexedAt: new Date() },
|
|
};
|
|
}
|
|
|
|
private getLanguagePatterns(language: string): RegExp[] {
|
|
const patterns: Record<string, RegExp[]> = {
|
|
typescript: [/^\s*(export\s+)?(async\s+)?function\s+(\w+)/, /^\s*(export\s+)?class\s+(\w+)/, /^\s*(export\s+)?interface\s+(\w+)/, /^\s*(export\s+)?type\s+(\w+)/],
|
|
javascript: [/^\s*(export\s+)?(async\s+)?function\s+(\w+)/, /^\s*(export\s+)?class\s+(\w+)/],
|
|
python: [/^\s*def\s+(\w+)/, /^\s*class\s+(\w+)/],
|
|
rust: [/^\s*(pub\s+)?fn\s+(\w+)/, /^\s*(pub\s+)?struct\s+(\w+)/, /^\s*(pub\s+)?enum\s+(\w+)/],
|
|
go: [/^\s*func\s+(\w+)/, /^\s*type\s+(\w+)\s+struct/],
|
|
};
|
|
return patterns[language] ?? patterns.typescript;
|
|
}
|
|
|
|
private detectSymbol(line: string, patterns: RegExp[]): { type: CodeChunk['symbolType']; name: string } | null {
|
|
for (const pattern of patterns) {
|
|
const match = line.match(pattern);
|
|
if (match) {
|
|
const name = match[match.length - 1] || match[2] || match[1];
|
|
let type: CodeChunk['symbolType'] = 'function';
|
|
if (line.includes('class ')) type = 'class';
|
|
else if (line.includes('interface ')) type = 'interface';
|
|
else if (line.includes('type ')) type = 'type';
|
|
return { type, name };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private isInsideBlock(lines: string[]): boolean {
|
|
let braceCount = 0;
|
|
for (const line of lines) {
|
|
braceCount += (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
|
|
}
|
|
return braceCount > 0;
|
|
}
|
|
|
|
private generateCodeEmbedding(code: string, symbolName?: string, symbolType?: string): Float32Array {
|
|
const text = `${symbolType || ''} ${symbolName || ''} ${code}`.toLowerCase();
|
|
const embedding = new Float32Array(this.dimensions);
|
|
let hash = 0;
|
|
for (let i = 0; i < text.length; i++) { hash = ((hash << 5) - hash) + text.charCodeAt(i); hash = hash & hash; }
|
|
for (let i = 0; i < this.dimensions; i++) { embedding[i] = Math.sin(hash * (i + 1) * 0.001) * 0.5 + 0.5; }
|
|
let norm = 0;
|
|
for (let i = 0; i < this.dimensions; i++) norm += embedding[i] * embedding[i];
|
|
norm = Math.sqrt(norm);
|
|
for (let i = 0; i < this.dimensions; i++) embedding[i] /= norm;
|
|
return embedding;
|
|
}
|
|
|
|
private generateQueryEmbedding(query: string): Float32Array {
|
|
return this.generateCodeEmbedding(query);
|
|
}
|
|
|
|
private calculateRelevance(chunk: CodeChunk, query: string): number {
|
|
let relevance = 0.5;
|
|
const queryLower = query.toLowerCase();
|
|
const symbolLower = chunk.symbolName?.toLowerCase() ?? '';
|
|
if (symbolLower && queryLower.includes(symbolLower)) relevance += 0.3;
|
|
if (symbolLower && symbolLower.includes(queryLower.split(' ')[0])) relevance += 0.2;
|
|
return Math.min(1, relevance);
|
|
}
|
|
|
|
private findHighlights(content: string, query: string): string[] {
|
|
const highlights: string[] = [];
|
|
const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 2);
|
|
const lines = content.split('\n');
|
|
for (const line of lines) {
|
|
for (const keyword of keywords) {
|
|
if (line.toLowerCase().includes(keyword)) { highlights.push(line.trim()); break; }
|
|
}
|
|
if (highlights.length >= 3) break;
|
|
}
|
|
return highlights;
|
|
}
|
|
|
|
private hashString(str: string): string {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash = hash & hash; }
|
|
return hash.toString(16);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Plugin Definition
|
|
// ============================================================================
|
|
|
|
let searchInstance: SemanticCodeSearch | null = null;
|
|
|
|
async function getCodeSearch(): Promise<SemanticCodeSearch> {
|
|
if (!searchInstance) {
|
|
searchInstance = new SemanticCodeSearch(768);
|
|
await searchInstance.initialize();
|
|
}
|
|
return searchInstance;
|
|
}
|
|
|
|
export const semanticCodeSearchPlugin = new PluginBuilder('semantic-code-search', '1.0.0')
|
|
.withDescription('Semantic code search using @ruvector/wasm HNSW indexing (<1ms search)')
|
|
.withAuthor('Claude Flow Team')
|
|
.withTags(['search', 'code', 'semantic', 'ruvector', 'hnsw'])
|
|
.withMCPTools([
|
|
new MCPToolBuilder('code-index')
|
|
.withDescription('Index a code file for semantic search')
|
|
.addStringParam('filePath', 'Path to the file', { required: true })
|
|
.addStringParam('content', 'File content', { required: true })
|
|
.addStringParam('language', 'Programming language', { required: true })
|
|
.withHandler(async (params) => {
|
|
try {
|
|
const search = await getCodeSearch();
|
|
const ids = await search.indexFile(params.filePath as string, params.content as string, params.language as string);
|
|
return { content: [{ type: 'text', text: `✅ Indexed ${params.filePath}\nCreated ${ids.length} searchable chunks` }] };
|
|
} catch (error) {
|
|
return { content: [{ type: 'text', text: `❌ Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
|
|
}
|
|
})
|
|
.build(),
|
|
|
|
new MCPToolBuilder('code-search')
|
|
.withDescription('Search code using natural language (<1ms with HNSW)')
|
|
.addStringParam('query', 'Natural language search query', { required: true })
|
|
.addNumberParam('k', 'Number of results', { default: 5 })
|
|
.addNumberParam('minSimilarity', 'Minimum similarity', { default: 0.3 })
|
|
.withHandler(async (params) => {
|
|
try {
|
|
const search = await getCodeSearch();
|
|
const results = await search.search(params.query as string, { k: params.k as number, minSimilarity: params.minSimilarity as number });
|
|
if (results.length === 0) return { content: [{ type: 'text', text: '🔍 No matching code found.' }] };
|
|
const output = results.map((r, i) => `**${i + 1}. ${r.chunk.filePath}:${r.chunk.startLine}** (${(r.similarity * 100).toFixed(1)}%)\n ${r.chunk.symbolType ? `[${r.chunk.symbolType}] ` : ''}${r.chunk.symbolName || ''}`).join('\n\n');
|
|
return { content: [{ type: 'text', text: `🔍 **Found ${results.length} matches:**\n\n${output}` }] };
|
|
} catch (error) {
|
|
return { content: [{ type: 'text', text: `❌ Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
|
|
}
|
|
})
|
|
.build(),
|
|
|
|
new MCPToolBuilder('code-similar')
|
|
.withDescription('Find code similar to a given snippet')
|
|
.addStringParam('code', 'Code snippet', { required: true })
|
|
.addNumberParam('k', 'Number of results', { default: 5 })
|
|
.withHandler(async (params) => {
|
|
try {
|
|
const search = await getCodeSearch();
|
|
const results = await search.findSimilar(params.code as string, params.k as number);
|
|
if (results.length === 0) return { content: [{ type: 'text', text: '🔍 No similar code found.' }] };
|
|
const output = results.map((r, i) => `**${i + 1}. ${r.chunk.filePath}:${r.chunk.startLine}** (${(r.similarity * 100).toFixed(1)}% similar)`).join('\n');
|
|
return { content: [{ type: 'text', text: `🔍 **Similar code:**\n\n${output}` }] };
|
|
} catch (error) {
|
|
return { content: [{ type: 'text', text: `❌ Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
|
|
}
|
|
})
|
|
.build(),
|
|
|
|
new MCPToolBuilder('code-stats')
|
|
.withDescription('Get code index statistics')
|
|
.withHandler(async () => {
|
|
const search = await getCodeSearch();
|
|
const stats = search.getStats();
|
|
return { content: [{ type: 'text', text: `📊 **Code Index:**\n\n**Chunks:** ${stats.totalChunks}\n**Files:** ${stats.totalFiles}\n**Backend:** @ruvector/wasm HNSW` }] };
|
|
})
|
|
.build(),
|
|
])
|
|
.withHooks([
|
|
new HookBuilder(HookEvent.PostFileWrite)
|
|
.withName('code-auto-index')
|
|
.withDescription('Auto-index code files on write')
|
|
.withPriority(HookPriority.Low)
|
|
.when((ctx) => {
|
|
const data = ctx.data as { filePath?: string } | undefined;
|
|
if (!data?.filePath) return false;
|
|
const ext = data.filePath.split('.').pop()?.toLowerCase();
|
|
return ['ts', 'js', 'tsx', 'jsx', 'py', 'rs', 'go'].includes(ext || '');
|
|
})
|
|
.handle(async (ctx) => {
|
|
const data = ctx.data as { filePath: string; content?: string };
|
|
if (!data.content) return { success: true };
|
|
const langMap: Record<string, string> = { ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', py: 'python', rs: 'rust', go: 'go' };
|
|
const ext = data.filePath.split('.').pop()?.toLowerCase() || '';
|
|
try {
|
|
const search = await getCodeSearch();
|
|
await search.indexFile(data.filePath, data.content, langMap[ext] || 'typescript');
|
|
} catch { /* silent */ }
|
|
return { success: true };
|
|
})
|
|
.build(),
|
|
])
|
|
.onInitialize(async (ctx) => {
|
|
ctx.logger.info('Semantic Code Search initializing with @ruvector/wasm...');
|
|
await getCodeSearch();
|
|
ctx.logger.info('Semantic Code Search ready - HNSW indexing enabled');
|
|
})
|
|
.build();
|
|
|
|
export default semanticCodeSearchPlugin;
|