├── data └── query_cache.db ├── src ├── types │ ├── paging.ts │ ├── global.d.ts │ ├── transformers.d.ts │ ├── errors.ts │ ├── tree-sitter-modules.d.ts │ └── agent.ts ├── vendor │ └── jscpd │ │ ├── core │ │ ├── validators │ │ │ ├── index.ts │ │ │ ├── lines-length-clone.validator.ts │ │ │ └── validator.ts │ │ ├── interfaces │ │ │ ├── token-location.interface.ts │ │ │ ├── source-validator.interface.ts │ │ │ ├── blamed-lines.interface.ts │ │ │ ├── validation-result.interface.ts │ │ │ ├── clone-validator.interface.ts │ │ │ ├── tokenizer.interface.ts │ │ │ ├── store.interface.ts │ │ │ ├── map-frame.interface.ts │ │ │ ├── tokens-map.interface.ts │ │ │ ├── token.interface.ts │ │ │ ├── subscriber.interface.ts │ │ │ ├── index.ts │ │ │ ├── clone.interface.ts │ │ │ ├── statistic.interface.ts │ │ │ └── options.interface.ts │ │ ├── index.ts │ │ ├── mode.ts │ │ ├── options.ts │ │ ├── store │ │ │ └── memory.ts │ │ ├── detector.ts │ │ ├── rabin-karp.ts │ │ └── statistic.ts │ │ ├── index.ts │ │ ├── in-files-detector.ts │ │ └── tokenizer │ │ ├── simple-tokenizer.ts │ │ └── tokens-map.ts ├── __mocks__ │ ├── p-limit.cjs │ ├── connection-pool.cjs │ └── nanoid.cjs ├── utils │ ├── entity-summary.ts │ ├── cursor.ts │ ├── provider-logger.ts │ ├── logger-types.ts │ ├── tool-response.ts │ ├── tmp-log.ts │ ├── stdio-console.ts │ └── source-snippet.ts ├── tools │ ├── hybrid-ranking.ts │ ├── __tests__ │ │ ├── lerna-project-graph.test.ts │ │ └── lerna-graph-ingest.test.ts │ ├── get-entity-source.ts │ ├── graph-query.ts │ ├── analyze-code-impact.ts │ ├── list-entity-relationships.ts │ ├── resolve-entity.ts │ └── agent-metrics.ts ├── semantic │ └── providers │ │ ├── base.ts │ │ ├── memory-provider.ts │ │ ├── http-embedding-helpers.ts │ │ ├── transformers-provider.ts │ │ ├── factory.ts │ │ ├── openai-provider.ts │ │ ├── http-engine.ts │ │ └── cloudru-provider.ts ├── config │ └── logging-config.ts ├── storage │ └── graph-storage-factory.ts ├── parsers │ └── markdown-analyzer.ts └── test-fixtures │ └── go │ └── sample.go ├── tests ├── fixtures │ ├── entity-source.ts │ └── jscpd-clones │ │ ├── alpha.ts │ │ └── beta.ts ├── integration │ ├── tool-envelope.test.ts │ ├── query-paging-shape.test.ts │ ├── query-shape.test.ts │ ├── tool-descriptions.test.ts │ ├── tool-envelope-core.test.ts │ └── jscpd-tool.test.ts ├── tools │ ├── hybrid-ranking.test.ts │ ├── bus-tools.test.ts │ ├── pagination.test.ts │ ├── resolve-entity.test.ts │ ├── get-entity-source.test.ts │ ├── agent-metrics.test.ts │ ├── list-entity-relationships-depth.test.ts │ └── analyze-code-impact-depth.test.ts ├── parsers │ ├── markdown-analyzer.test.ts │ ├── parser-cache.test.ts │ ├── tree-sitter-parser.test.ts │ └── kotlin-analyzer.test.ts ├── utils │ ├── source-snippet.test.ts │ └── index-file-collection.test.ts ├── agents │ ├── base-agent.test.ts │ └── resource-adjustment.test.ts └── test-mcp-direct.js ├── tsconfig.test.json ├── biome.json ├── RELEASE_NOTES_2.7.12.md ├── RELEASE_NOTES_2.7.10.md ├── RELEASE_NOTES_2.7.11.md ├── count_votes.py ├── jest.setup.js ├── Makefile ├── LICENSE ├── examples ├── c-test-files │ ├── basic_functions.c │ └── structures.c └── cpp-test-files │ ├── basic_classes.cpp │ └── math_utils.hpp ├── tsconfig.json ├── tsup.config.ts ├── jest.config.js ├── scripts ├── test-wasm-resolution.js └── run-tests.js ├── RELEASE_NOTES_2.7.9.md ├── generate_microsoft_report.py ├── .gitignore ├── RELEASE_NOTES_2.7.15.md ├── package.json ├── process_plugins.py ├── generate_publisher_report.py ├── scrape_marketplace.py ├── classify_plugins.py └── config ├── cloud_prod.yaml └── ollama.yaml /data/query_cache.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/er77/code-graph-rag-mcp/HEAD/data/query_cache.db -------------------------------------------------------------------------------- /src/types/paging.ts: -------------------------------------------------------------------------------- 1 | export type Page = { 2 | items: T[]; 3 | nextCursor: string | null; 4 | total?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lines-length-clone.validator"; 2 | export * from "./validator"; 3 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import type Database from "better-sqlite3"; 2 | 3 | declare global { 4 | var testDb: Database.Database | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/token-location.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITokenLocation { 2 | line: number; 3 | column?: number; 4 | position?: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/vendor/jscpd/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./core"; 2 | export * from "./files"; 3 | export * from "./in-files-detector"; 4 | export * from "./tokenizer/simple-tokenizer"; 5 | -------------------------------------------------------------------------------- /tests/fixtures/entity-source.ts: -------------------------------------------------------------------------------- 1 | export function add(a: number, b: number) { 2 | return a + b; 3 | } 4 | 5 | export function mul(a: number, b: number) { 6 | return a * b; 7 | } 8 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/source-validator.interface.ts: -------------------------------------------------------------------------------- 1 | import type { IClone } from ".."; 2 | 3 | export interface ISourceValidator { 4 | validate(clone: IClone): boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/__mocks__/p-limit.cjs: -------------------------------------------------------------------------------- 1 | function pLimit(_concurrency) { 2 | return async (fn, ...args) => { 3 | return fn(...args); 4 | }; 5 | } 6 | 7 | module.exports = pLimit; 8 | module.exports.pLimit = pLimit; 9 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/blamed-lines.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IBlamedLines { 2 | [line: string]: { 3 | rev: string; 4 | author: string; 5 | date: string; 6 | line: string; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./detector"; 2 | export * from "./interfaces"; 3 | export * from "./mode"; 4 | export * from "./options"; 5 | export * from "./statistic"; 6 | export * from "./store/memory"; 7 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/validation-result.interface.ts: -------------------------------------------------------------------------------- 1 | import type { IClone } from ".."; 2 | 3 | export interface IValidationResult { 4 | status: boolean; 5 | message?: string[]; 6 | clone?: IClone; 7 | } 8 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/clone-validator.interface.ts: -------------------------------------------------------------------------------- 1 | import type { IClone, IOptions, IValidationResult } from ".."; 2 | 3 | export interface ICloneValidator { 4 | validate(clone: IClone, options: IOptions): IValidationResult; 5 | } 6 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/tokenizer.interface.ts: -------------------------------------------------------------------------------- 1 | import type { IOptions, ITokensMap } from "."; 2 | 3 | export interface ITokenizer { 4 | generateMaps(id: string, data: string, format: string, options: Partial): ITokensMap[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/store.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IStore { 2 | namespace(name: string): void; 3 | 4 | get(key: string): Promise; 5 | 6 | set(key: string, value: TValue): Promise; 7 | 8 | close(): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/map-frame.interface.ts: -------------------------------------------------------------------------------- 1 | import type { IToken } from "."; 2 | 3 | export interface IMapFrame { 4 | id: string; 5 | sourceId: string; 6 | start: IToken; 7 | end: IToken; 8 | isClone?: boolean; 9 | localDuplicate?: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/transformers.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@xenova/transformers" { 2 | export function pipeline( 3 | task: string, 4 | model: string, 5 | options?: { 6 | quantized?: boolean; 7 | progress_callback?: (progress: any) => void; 8 | [key: string]: any; 9 | }, 10 | ): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/tokens-map.interface.ts: -------------------------------------------------------------------------------- 1 | import type { IMapFrame } from "."; 2 | 3 | export interface ITokensMap { 4 | getFormat(): string; 5 | 6 | getLinesCount(): number; 7 | 8 | getTokensCount(): number; 9 | 10 | getId(): string; 11 | 12 | next(): IteratorResult; 13 | } 14 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/token.interface.ts: -------------------------------------------------------------------------------- 1 | import type { ITokenLocation } from "."; 2 | 3 | export interface IToken { 4 | type: string; 5 | value: string; 6 | length: number; 7 | format: string; 8 | range: [number, number]; 9 | loc?: { 10 | start: ITokenLocation; 11 | end: ITokenLocation; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "strict": false, 5 | "strictNullChecks": false, 6 | "noUnusedLocals": false, 7 | "noUnusedParameters": false, 8 | "skipLibCheck": true 9 | }, 10 | "include": ["src/**/__tests__/**/*.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/jscpd-clones/alpha.ts: -------------------------------------------------------------------------------- 1 | export function add(a: number, b: number): number { 2 | const total = a + b; 3 | if (total > 100) { 4 | return total - 100; 5 | } 6 | return total; 7 | } 8 | 9 | export function multiply(a: number, b: number): number { 10 | let result = 0; 11 | for (let i = 0; i < b; i++) { 12 | result += a; 13 | } 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/jscpd-clones/beta.ts: -------------------------------------------------------------------------------- 1 | export function add(a: number, b: number): number { 2 | const total = a + b; 3 | if (total > 100) { 4 | return total - 100; 5 | } 6 | return total; 7 | } 8 | 9 | export function multiply(a: number, b: number): number { 10 | let result = 0; 11 | for (let i = 0; i < b; i++) { 12 | result += a; 13 | } 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/subscriber.interface.ts: -------------------------------------------------------------------------------- 1 | import type { DetectorEvents, IClone, ITokensMap, IValidationResult } from ".."; 2 | 3 | export interface ISubscriber { 4 | subscribe(): Partial>; 5 | } 6 | 7 | export type IHandler = (payload: IEventPayload) => void; 8 | 9 | export interface IEventPayload { 10 | clone?: IClone; 11 | source?: ITokensMap; 12 | validation?: IValidationResult; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/entity-summary.ts: -------------------------------------------------------------------------------- 1 | import type { Entity } from "../types/storage.js"; 2 | 3 | export interface EntitySummary { 4 | id: string; 5 | name: string; 6 | type: string; 7 | filePath: string; 8 | location: Entity["location"]; 9 | metadata: Entity["metadata"]; 10 | } 11 | 12 | export function mapEntitySummary(entity: Entity): EntitySummary { 13 | return { 14 | id: entity.id, 15 | name: entity.name, 16 | type: entity.type, 17 | filePath: entity.filePath, 18 | location: entity.location, 19 | metadata: entity.metadata, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/cursor.ts: -------------------------------------------------------------------------------- 1 | export function encodeCursor(payload: Record): string { 2 | return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url"); 3 | } 4 | 5 | export function decodeCursor>(cursor?: string): T | null { 6 | if (!cursor) return null; 7 | try { 8 | const raw = Buffer.from(cursor, "base64url").toString("utf8"); 9 | const parsed = JSON.parse(raw); 10 | if (parsed && typeof parsed === "object") return parsed as T; 11 | return null; 12 | } catch { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/integration/tool-envelope.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { join } from "node:path"; 3 | import { describe, expect, it } from "@jest/globals"; 4 | 5 | describe("Tool response envelope (smoke)", () => { 6 | it("uses toolOk/toolFail helpers in dispatcher", () => { 7 | const src = readFileSync(join(process.cwd(), "src", "index.ts"), "utf8"); 8 | expect(src).toContain("toolOk("); 9 | expect(src).toContain("toolFail("); 10 | expect(src).toContain('toolFail("agent_busy"'); 11 | expect(src).toContain('toolFail("tool_error"'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/validators/lines-length-clone.validator.ts: -------------------------------------------------------------------------------- 1 | import type { IClone, ICloneValidator, IOptions, IValidationResult } from ".."; 2 | 3 | export class LinesLengthCloneValidator implements ICloneValidator { 4 | validate(clone: IClone, options: IOptions): IValidationResult { 5 | const lines = clone.duplicationA.end.line - clone.duplicationA.start.line; 6 | const status = lines >= Number(options?.minLines); 7 | 8 | return { 9 | status, 10 | message: status ? ["ok"] : [`Lines of code less than limit (${lines} < ${options.minLines})`], 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./blamed-lines.interface"; 2 | export * from "./clone.interface"; 3 | export * from "./clone-validator.interface"; 4 | export * from "./map-frame.interface"; 5 | export * from "./options.interface"; 6 | export * from "./statistic.interface"; 7 | export * from "./store.interface"; 8 | export * from "./subscriber.interface"; 9 | export * from "./token.interface"; 10 | export * from "./token-location.interface"; 11 | export * from "./tokenizer.interface"; 12 | export * from "./tokens-map.interface"; 13 | export * from "./validation-result.interface"; 14 | -------------------------------------------------------------------------------- /src/utils/provider-logger.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderLogger } from "../semantic/providers/base.js"; 2 | import type { RotatedLogger } from "./logger.js"; 3 | 4 | export function makeProviderLogger(l: RotatedLogger, category: string): ProviderLogger { 5 | return { 6 | debug: (msg, data, requestId) => l.debug(category, msg, data, requestId), 7 | info: (msg, data, requestId) => l.info(category, msg, data, requestId), 8 | warn: (msg, data, requestId) => l.warn(category, msg, data, requestId), 9 | error: (msg, data, requestId, err) => l.error(category, msg, data, requestId, err), 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/validators/validator.ts: -------------------------------------------------------------------------------- 1 | import type { IClone, ICloneValidator, IOptions, IValidationResult } from ".."; 2 | 3 | export function runCloneValidators(clone: IClone, options: IOptions, validators: ICloneValidator[]): IValidationResult { 4 | const acc: IValidationResult = { status: true, message: [], clone }; 5 | 6 | for (const validator of validators) { 7 | const res = validator.validate(clone, options); 8 | acc.status = acc.status && res.status; 9 | 10 | if (res.message && acc.message) { 11 | acc.message.push(...res.message); 12 | } 13 | } 14 | 15 | return acc; 16 | } 17 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/clone.interface.ts: -------------------------------------------------------------------------------- 1 | import type { IBlamedLines, ITokenLocation } from ".."; 2 | 3 | export interface IClone { 4 | format: string; 5 | isNew?: boolean; 6 | foundDate?: number; 7 | duplicationA: { 8 | sourceId: string; 9 | start: ITokenLocation; 10 | end: ITokenLocation; 11 | range: [number, number]; 12 | fragment?: string; 13 | blame?: IBlamedLines; 14 | }; 15 | duplicationB: { 16 | sourceId: string; 17 | start: ITokenLocation; 18 | end: ITokenLocation; 19 | range: [number, number]; 20 | fragment?: string; 21 | blame?: IBlamedLines; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/statistic.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IStatisticRow { 2 | lines: number; 3 | tokens: number; 4 | sources: number; 5 | duplicatedLines: number; 6 | duplicatedTokens: number; 7 | clones: number; 8 | percentage: number; 9 | percentageTokens: number; 10 | newDuplicatedLines: number; 11 | newClones: number; 12 | } 13 | 14 | export interface IStatisticFormat { 15 | sources: Record; 16 | total: IStatisticRow; 17 | } 18 | 19 | export interface IStatistic { 20 | total: IStatisticRow; 21 | detectionDate: string; 22 | formats: Record; 23 | } 24 | -------------------------------------------------------------------------------- /tests/integration/query-paging-shape.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { join } from "node:path"; 3 | import { describe, expect, it } from "@jest/globals"; 4 | 5 | describe("query tool paging and ranking signals (source-level)", () => { 6 | it("includes paging metadata and matchType annotation", () => { 7 | const src = readFileSync(join(process.cwd(), "src", "index.ts"), "utf8"); 8 | expect(src).toContain('case "query":'); 9 | expect(src).toContain("paging:"); 10 | expect(src).toContain("matchType"); 11 | expect(src).toContain("cursor:"); 12 | expect(src).toContain("pageSize:"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/query-shape.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { join } from "node:path"; 3 | import { describe, expect, it } from "@jest/globals"; 4 | 5 | describe("query tool output contract", () => { 6 | it("includes Page-shaped semantic + structural within data", () => { 7 | const src = readFileSync(join(process.cwd(), "src", "index.ts"), "utf8"); 8 | expect(src).toContain('case "query"'); 9 | expect(src).toContain("semantic: {"); 10 | expect(src).toContain("items:"); 11 | expect(src).toContain("nextCursor"); 12 | expect(src).toContain("structural: {"); 13 | expect(src).toContain("stats:"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/utils/logger-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logger Types and Interfaces 3 | * 4 | */ 5 | 6 | export enum LogLevel { 7 | DEBUG = 0, 8 | INFO = 1, 9 | WARN = 2, 10 | ERROR = 3, 11 | CRITICAL = 4, 12 | } 13 | 14 | export interface LoggerConfig { 15 | logDir: string; 16 | maxFileSize: number; // bytes 17 | maxFiles: number; 18 | logLevel: LogLevel; 19 | enableRotation: boolean; 20 | enableTimestamp: boolean; 21 | enableStackTrace: boolean; 22 | } 23 | 24 | export interface LogEntry { 25 | timestamp: string; 26 | level: LogLevel; 27 | category: string; 28 | message: string; 29 | data?: any; 30 | stackTrace?: string; 31 | requestId?: string; 32 | duration?: number; 33 | } 34 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", 3 | "files": { 4 | "ignoreUnknown": false, 5 | "includes": ["src/**", "tests/**", "*.ts", "*.js", "*.json"] 6 | }, 7 | "formatter": { 8 | "indentStyle": "space", 9 | "lineWidth": 120 10 | }, 11 | "assist": { "actions": { "source": { "organizeImports": "on" } } }, 12 | "linter": { 13 | "enabled": true, 14 | "rules": { 15 | "recommended": true, 16 | "suspicious": { 17 | "noExplicitAny": "off", 18 | "noDuplicateCase": "warn", 19 | "noAssignInExpressions": "off" 20 | }, 21 | "style": { 22 | "noNonNullAssertion": "off" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/types/errors.ts: -------------------------------------------------------------------------------- 1 | import type { AgentStatus } from "./agent.js"; 2 | 3 | export interface AgentBusyDetails { 4 | agentId: string; 5 | status: AgentStatus; 6 | reason: "not_idle" | "queue_full" | "memory_limit" | "unsupported_task" | "unknown"; 7 | queueLength?: number; 8 | maxQueue?: number; 9 | retryAfterMs?: number; 10 | taskId?: string; 11 | memoryUsageMB?: number; 12 | memoryLimitMB?: number; 13 | } 14 | 15 | export class AgentBusyError extends Error { 16 | public readonly details: AgentBusyDetails; 17 | 18 | constructor(details: AgentBusyDetails) { 19 | super(`Agent ${details.agentId} is busy (${details.reason})`); 20 | this.name = "AgentBusyError"; 21 | this.details = details; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/tools/hybrid-ranking.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import { rerankSemanticHits } from "../../src/tools/hybrid-ranking.js"; 3 | 4 | describe("rerankSemanticHits", () => { 5 | it("boosts hits that match structural file set", () => { 6 | const hits = [ 7 | { id: "1", similarity: 0.9, metadata: { path: "/a.ts" }, content: "A" }, 8 | { id: "2", similarity: 0.92, metadata: { path: "/b.ts" }, content: "B" }, 9 | ]; 10 | const structural = new Set(["/a.ts"]); 11 | const ranked = rerankSemanticHits(hits as any, structural, (p) => p); 12 | 13 | expect((ranked[0]!.hit as any).id).toBe("1"); 14 | expect(ranked[0]!.rankingSignals.structuralBoost).toBeGreaterThan(0); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__mocks__/connection-pool.cjs: -------------------------------------------------------------------------------- 1 | // Mock ConnectionPool for testing (CommonJS) 2 | 3 | class ConnectionPool { 4 | constructor(config) { 5 | this.config = config; 6 | this.initialized = false; 7 | } 8 | 9 | async initialize() { 10 | this.initialized = true; 11 | return Promise.resolve(); 12 | } 13 | 14 | async acquire() { 15 | // Return mock connection immediately 16 | return { 17 | id: "mock-conn", 18 | db: global.testDb || null, 19 | inUse: true, 20 | lastUsed: Date.now(), 21 | }; 22 | } 23 | 24 | release(connection) { 25 | if (connection) connection.inUse = false; 26 | } 27 | 28 | async shutdown() { 29 | this.initialized = false; 30 | return Promise.resolve(); 31 | } 32 | } 33 | 34 | module.exports = { ConnectionPool }; 35 | -------------------------------------------------------------------------------- /RELEASE_NOTES_2.7.12.md: -------------------------------------------------------------------------------- 1 | # Release Notes — `@er77/code-graph-rag-mcp` v2.7.12 (2025-12-15) 2 | 3 | ## Summary 4 | 5 | v2.7.12 removes a noisy npm deprecation warning (`boolean@3.2.0`) during install by no longer auto-installing `onnxruntime-node` by default. 6 | 7 | ## What’s Changed 8 | 9 | - Dependencies: `onnxruntime-node` is now an **optional peer dependency** (not auto-installed), so installs no longer pull in the deprecated `boolean` package. 10 | 11 | ## Install / Upgrade 12 | 13 | - npm: `npm install -g @er77/code-graph-rag-mcp@2.7.12` 14 | - local artifact: `npm install -g ./er77-code-graph-rag-mcp-2.7.12.tgz` 15 | 16 | Node.js: `>=24` 17 | 18 | ## Notes 19 | 20 | - If you want the optional ONNX acceleration path, install it explicitly in the global install directory: 21 | - `npm install -g onnxruntime-node` 22 | 23 | -------------------------------------------------------------------------------- /tests/tools/bus-tools.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import { knowledgeBus } from "../../src/core/knowledge-bus.js"; 3 | 4 | describe("KnowledgeBus tooling", () => { 5 | it("returns stats with topic and entry counts", () => { 6 | knowledgeBus.publish("test:topic", { value: 1 }, "test"); 7 | const stats = knowledgeBus.getStats(); 8 | 9 | expect(stats.topicCount).toBeGreaterThanOrEqual(1); 10 | expect(stats.entryCount).toBeGreaterThanOrEqual(1); 11 | 12 | knowledgeBus.clearTopic("test:topic"); 13 | }); 14 | 15 | it("clears specific topics", () => { 16 | knowledgeBus.publish("test:clear", { value: 2 }, "test"); 17 | knowledgeBus.clearTopic("test:clear"); 18 | 19 | const entries = knowledgeBus.query("test:clear"); 20 | expect(entries.length).toBe(0); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /RELEASE_NOTES_2.7.10.md: -------------------------------------------------------------------------------- 1 | # Release Notes — `@er77/code-graph-rag-mcp` v2.7.10 (2025-12-15) 2 | 3 | ## Summary 4 | 5 | v2.7.10 is a small patch release that fixes sqlite-vec extension loading for global installs (independent of project `cwd`). 6 | 7 | ## What’s Changed 8 | 9 | - sqlite-vec loading: `VectorStore` now loads sqlite-vec via `sqlite-vec`’s `getLoadablePath()` first (global-install safe), with the existing fallback probes preserved. 10 | 11 | ## Install / Upgrade 12 | 13 | - npm: `npm install -g @er77/code-graph-rag-mcp@2.7.10` 14 | - local artifact: `npm install -g ./er77-code-graph-rag-mcp-2.7.10.tgz` 15 | 16 | Node.js: `>=24` 17 | 18 | ## Notes 19 | 20 | - If sqlite-vec still can’t load, the server will run in fallback mode (slower semantic search). Check `/tmp/code-graph-rag-mcp/mcp-server-YYYY-MM-DD.log` for the exact load error. 21 | 22 | -------------------------------------------------------------------------------- /RELEASE_NOTES_2.7.11.md: -------------------------------------------------------------------------------- 1 | # Release Notes — `@er77/code-graph-rag-mcp` v2.7.11 (2025-12-15) 2 | 3 | ## Summary 4 | 5 | v2.7.11 makes database storage **per-repo by default** so multiple codebases don’t share or mix a single SQLite database. 6 | 7 | ## What’s Changed 8 | 9 | - Database isolation: default `database.path` is now `./.code-graph-rag/vectors.db` (relative to the workspace root after `process.chdir()`). 10 | - Index hygiene: `.code-graph-rag/**` is always excluded from indexing. 11 | 12 | ## Install / Upgrade 13 | 14 | - npm: `npm install -g @er77/code-graph-rag-mcp@2.7.11` 15 | - local artifact: `npm install -g ./er77-code-graph-rag-mcp-2.7.11.tgz` 16 | 17 | Node.js: `>=24` 18 | 19 | ## Notes 20 | 21 | - Add `/.code-graph-rag/` to your repo’s `.gitignore`. 22 | - To override per-project DB location, set `DATABASE_PATH` or `database.path` in YAML. 23 | 24 | -------------------------------------------------------------------------------- /count_votes.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | 4 | def count_plugins_with_votes(file_path): 5 | try: 6 | with open(file_path, 'r', encoding='utf-8') as f: 7 | plugins = json.load(f) 8 | 9 | count = 0 10 | for plugin in plugins: 11 | if plugin.get('vote_count', 0) > 0: 12 | count += 1 13 | return count 14 | except FileNotFoundError: 15 | return "Error: plugins.json not found." 16 | except json.JSONDecodeError: 17 | return "Error: Could not decode plugins.json. Invalid JSON format." 18 | except Exception as e: 19 | return f"An unexpected error occurred: {e}" 20 | 21 | if __name__ == "__main__": 22 | file_path = 'plugins.json' 23 | num_plugins_with_votes = count_plugins_with_votes(file_path) 24 | print(f"Number of plugins with user votes: {num_plugins_with_votes}") 25 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | const { jest } = await import("@jest/globals"); 2 | const path = await import("node:path"); 3 | const fs = await import("node:fs"); 4 | 5 | if (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID) { 6 | // Ensure tests don't write DB artifacts outside the repo (e.g. ~/.code-graph-rag). 7 | if (!process.env.DATABASE_PATH) { 8 | const workerId = process.env.JEST_WORKER_ID || "0"; 9 | const tmpDir = path.join(process.cwd(), "tmp"); 10 | try { 11 | fs.mkdirSync(tmpDir, { recursive: true }); 12 | } catch { 13 | // best-effort; tests may still override DATABASE_PATH explicitly 14 | } 15 | process.env.DATABASE_PATH = path.join(tmpDir, `test-vectors-${workerId}.db`); 16 | } 17 | 18 | global.console = { 19 | ...console, 20 | log: jest.fn(), 21 | debug: jest.fn(), 22 | info: jest.fn(), 23 | warn: jest.fn(), 24 | error: console.error, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /tests/tools/pagination.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import { decodeCursor, encodeCursor } from "../../src/utils/cursor.js"; 3 | 4 | describe("cursor pagination helpers", () => { 5 | it("round-trips cursor payloads", () => { 6 | const cursor = encodeCursor({ o: 10, q: "abc" }); 7 | const decoded = decodeCursor<{ o: number; q: string }>(cursor); 8 | expect(decoded).toEqual({ o: 10, q: "abc" }); 9 | }); 10 | 11 | it("supports offset-based paging", () => { 12 | const items = Array.from({ length: 12 }, (_, i) => i); 13 | const pageSize = 5; 14 | 15 | const page1 = items.slice(0, pageSize); 16 | const c1 = encodeCursor({ o: pageSize }); 17 | const off1 = decodeCursor<{ o: number }>(c1)!.o; 18 | const page2 = items.slice(off1, off1 + pageSize); 19 | 20 | expect(page1).toEqual([0, 1, 2, 3, 4]); 21 | expect(page2).toEqual([5, 6, 7, 8, 9]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/utils/tool-response.ts: -------------------------------------------------------------------------------- 1 | export type ToolOk = { 2 | success: true; 3 | data: T; 4 | meta?: Record; 5 | warnings?: string[]; 6 | }; 7 | 8 | export type ToolFail = { 9 | success: false; 10 | errorType: string; 11 | error: string; 12 | details?: unknown; 13 | meta?: Record; 14 | }; 15 | 16 | export type ToolEnvelope = ToolOk | ToolFail; 17 | 18 | export function toolOk(data: T, meta?: Record, warnings?: string[]): ToolOk { 19 | return warnings?.length ? { success: true, data, meta, warnings } : { success: true, data, meta }; 20 | } 21 | 22 | export function toolFail( 23 | errorType: string, 24 | error: string, 25 | details?: unknown, 26 | meta?: Record, 27 | ): ToolFail { 28 | return details !== undefined 29 | ? { success: false, errorType, error, details, meta } 30 | : { success: false, errorType, error, meta }; 31 | } 32 | -------------------------------------------------------------------------------- /src/__mocks__/nanoid.cjs: -------------------------------------------------------------------------------- 1 | // Mock implementation of nanoid for testing (CommonJS) 2 | 3 | function nanoid(size = 21) { 4 | const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"; 5 | let result = ""; 6 | for (let i = 0; i < size; i++) { 7 | result += chars[Math.floor(Math.random() * chars.length)]; 8 | } 9 | return result; 10 | } 11 | 12 | function customAlphabet(alphabet, defaultSize = 21) { 13 | return (size = defaultSize) => { 14 | let result = ""; 15 | for (let i = 0; i < size; i++) { 16 | result += alphabet[Math.floor(Math.random() * alphabet.length)]; 17 | } 18 | return result; 19 | }; 20 | } 21 | 22 | function urlAlphabet(size = 21) { 23 | return nanoid(size); 24 | } 25 | 26 | async function nanoidAsync(size = 21) { 27 | return Promise.resolve(nanoid(size)); 28 | } 29 | 30 | module.exports = { 31 | nanoid, 32 | customAlphabet, 33 | urlAlphabet, 34 | nanoidAsync, 35 | }; 36 | -------------------------------------------------------------------------------- /tests/parsers/markdown-analyzer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import { TreeSitterParser } from "../../src/parsers/tree-sitter-parser"; 3 | 4 | describe("Markdown indexing (TreeSitterParser)", () => { 5 | it("extracts document + headings with contains relationships", async () => { 6 | const parser = new TreeSitterParser(); 7 | await parser.initialize(); 8 | 9 | const md = `# Title 10 | 11 | Some text 12 | 13 | ## Section A 14 | ### Sub A1 15 | `; 16 | 17 | const result = await parser.parse("README.md", md, "md-hash-1"); 18 | 19 | expect(result.language).toBe("markdown"); 20 | expect(result.entities.some((e) => e.type === "document")).toBe(true); 21 | expect(result.entities.some((e) => e.type === "heading" && e.name === "Title")).toBe(true); 22 | expect(result.entities.some((e) => e.type === "heading" && e.name === "Section A")).toBe(true); 23 | expect(result.relationships?.some((r) => r.type === "contains")).toBe(true); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/mode.ts: -------------------------------------------------------------------------------- 1 | import type { IOptions, IToken } from "./interfaces"; 2 | 3 | export type IMode = (token: IToken, options?: IOptions) => boolean; 4 | 5 | export function strict(token: IToken): boolean { 6 | return token.type !== "ignore"; 7 | } 8 | 9 | export function mild(token: IToken): boolean { 10 | return strict(token) && token.type !== "empty" && token.type !== "new_line"; 11 | } 12 | 13 | export function weak(token: IToken): boolean { 14 | return mild(token) && token.format !== "comment" && token.type !== "comment" && token.type !== "block-comment"; 15 | } 16 | 17 | const MODES: { [name: string]: IMode } = { 18 | mild, 19 | strict, 20 | weak, 21 | }; 22 | 23 | export function getModeByName(name: string): IMode { 24 | if (name in MODES) { 25 | return MODES[name] as IMode; 26 | } 27 | throw new Error(`Mode ${name} does not supported yet.`); 28 | } 29 | 30 | export function getModeHandler(mode: string | IMode): IMode { 31 | return typeof mode === "string" ? getModeByName(mode) : mode; 32 | } 33 | -------------------------------------------------------------------------------- /src/tools/hybrid-ranking.ts: -------------------------------------------------------------------------------- 1 | export type HybridRankedHit = { 2 | hit: T; 3 | finalScore: number; 4 | rankingSignals: { semanticScore: number; structuralBoost: number }; 5 | }; 6 | 7 | export function rerankSemanticHits< 8 | T extends { metadata?: any; similarity?: number; score?: number; path?: string; filePath?: string }, 9 | >(hits: T[], structuralFileSet: Set, normalizePath: (p: string) => string): HybridRankedHit[] { 10 | return hits 11 | .map((hit) => { 12 | const meta = hit.metadata ?? {}; 13 | const rawPath = String(meta?.path ?? hit.path ?? hit.filePath ?? ""); 14 | const normalizedPath = rawPath ? normalizePath(rawPath) : ""; 15 | const semanticScore = Number(hit.similarity ?? hit.score ?? 0) || 0; 16 | const structuralBoost = normalizedPath && structuralFileSet.has(normalizedPath) ? 0.15 : 0; 17 | return { hit, finalScore: semanticScore + structuralBoost, rankingSignals: { semanticScore, structuralBoost } }; 18 | }) 19 | .sort((a, b) => b.finalScore - a.finalScore); 20 | } 21 | -------------------------------------------------------------------------------- /src/semantic/providers/base.ts: -------------------------------------------------------------------------------- 1 | export type ProviderKind = "memory" | "transformers" | "ollama" | "openai" | "cloudru"; 2 | 3 | export interface ProviderInfo { 4 | name: ProviderKind | string; 5 | model: string; 6 | dimension?: number; 7 | supportsBatch: boolean; 8 | maxBatchSize?: number; 9 | } 10 | 11 | export interface EmbedOptions { 12 | signal?: AbortSignal; 13 | requestId?: string; 14 | } 15 | 16 | export interface ProviderLogger { 17 | debug(msg: string, data?: any, requestId?: string): void; 18 | info(msg: string, data?: any, requestId?: string): void; 19 | warn(msg: string, data?: any, requestId?: string): void; 20 | error(msg: string, data?: any, requestId?: string, err?: Error): void; 21 | } 22 | 23 | export interface EmbeddingProvider { 24 | info: ProviderInfo; 25 | initialize(): Promise; 26 | getDimension(): number | undefined; 27 | embed(text: string, opts?: EmbedOptions): Promise; 28 | embedBatch?(texts: string[], opts?: EmbedOptions): Promise; 29 | close?(): Promise; 30 | } 31 | -------------------------------------------------------------------------------- /tests/utils/source-snippet.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import { extractSourceSnippetFromText } from "../../src/utils/source-snippet.js"; 3 | 4 | describe("extractSourceSnippetFromText", () => { 5 | it("extracts a snippet with context lines", () => { 6 | const text = ["l1", "l2", "l3", "l4", "l5", "l6"].join("\n"); 7 | const res = extractSourceSnippetFromText({ text, startLine: 3, endLine: 4, contextLines: 1, maxBytes: 1000 }); 8 | 9 | expect(res.snippet).toBe(["l2", "l3", "l4", "l5"].join("\n")); 10 | expect(res.snippetRange).toEqual({ startLine: 2, endLine: 5 }); 11 | expect(res.entityRange).toEqual({ startLine: 3, endLine: 4 }); 12 | expect(res.truncated).toBe(false); 13 | }); 14 | 15 | it("truncates large snippets by maxBytes", () => { 16 | const text = "a".repeat(10_000); 17 | const res = extractSourceSnippetFromText({ text, startLine: 1, endLine: 1, contextLines: 0, maxBytes: 1024 }); 18 | expect(res.truncated).toBe(true); 19 | expect(res.snippet.length).toBeLessThanOrEqual(1024); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/options.ts: -------------------------------------------------------------------------------- 1 | import type { IOptions, TOption } from "./interfaces"; 2 | import { getModeHandler } from "./mode"; 3 | 4 | export function getDefaultOptions(): IOptions { 5 | return { 6 | executionId: new Date().toISOString(), 7 | path: [process.cwd()], 8 | mode: getModeHandler("mild"), 9 | minLines: 5, 10 | maxLines: 1000, 11 | maxSize: "100kb", 12 | minTokens: 50, 13 | output: "./report", 14 | reporters: ["console"], 15 | ignore: [], 16 | threshold: undefined, 17 | formatsExts: {}, 18 | debug: false, 19 | silent: false, 20 | blame: false, 21 | cache: true, 22 | absolute: false, 23 | noSymlinks: false, 24 | skipLocal: false, 25 | ignoreCase: false, 26 | gitignore: false, 27 | reportersOptions: {}, 28 | exitCode: 0, 29 | }; 30 | } 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | export function getOption(name: TOption, options?: IOptions): any { 34 | const defaultOptions = getDefaultOptions(); 35 | return options ? options[name] || defaultOptions[name] : defaultOptions[name]; 36 | } 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | .ONESHELL: 3 | .SHELLFLAGS := -eu -o pipefail -c 4 | .DELETE_ON_ERROR: 5 | .DEFAULT_GOAL := help 6 | MAKEFLAGS += --warn-undefined-variables 7 | MAKEFLAGS += --no-builtin-rules 8 | 9 | .PHONY: help 10 | help: ## Prints help for targets with comments 11 | @echo "Targets:" 12 | @cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 13 | 14 | .PHONY: check-requirements 15 | check-requirements: ## Ensure that all requirements are installed 16 | @command -v pre-commit > /dev/null 2>&1 || (echo "pre-commit not installed") 17 | 18 | .PHONY: hooks 19 | hooks: install-hooks ## Run pre-commit hooks on all files 20 | pre-commit run --color=always --all-files --hook-stage commit 21 | 22 | .PHONY: install-hooks 23 | install-hooks: .git/hooks/pre-commit ## Install pre-commit hooks 24 | 25 | .git/hooks/pre-commit: .pre-commit-config.yaml 26 | pre-commit install 27 | 28 | .PHONY: package 29 | package: dist/index.js ## Package using tsup 30 | 31 | dist/index.js: src/*.ts package.json tsconfig.json tsup.config.ts 32 | bun run tsup 33 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/store/memory.ts: -------------------------------------------------------------------------------- 1 | import type { IStore } from ".."; 2 | 3 | export class MemoryStore implements IStore { 4 | private _namespace: string = ""; 5 | 6 | protected values: Record> = {}; 7 | 8 | public namespace(namespace: string): void { 9 | this._namespace = namespace; 10 | this.values[namespace] = this.values[namespace] || {}; 11 | } 12 | 13 | public get(key: string): Promise { 14 | return new Promise((resolve, reject) => { 15 | const namespaceValues = this.values[this._namespace]; 16 | if (namespaceValues && key in namespaceValues) { 17 | resolve(namespaceValues[key] as IMapFrame); 18 | } else { 19 | reject(new Error("not found")); 20 | } 21 | }); 22 | } 23 | 24 | public set(key: string, value: IMapFrame): Promise { 25 | const namespaceValues = this.values[this._namespace] ?? (this.values[this._namespace] = {}); 26 | namespaceValues[key] = value; 27 | return Promise.resolve(value); 28 | } 29 | 30 | close(): void { 31 | this.values = {}; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Cartograph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/interfaces/options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IOptions { 2 | executionId?: string; 3 | minLines?: number; 4 | maxLines?: number; 5 | maxSize?: string; 6 | minTokens?: number; 7 | threshold?: number; 8 | formatsExts?: Record; 9 | output?: string; 10 | path?: string[]; 11 | pattern?: string; 12 | ignorePattern?: string[]; 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | mode?: any; 15 | config?: string; 16 | ignore?: string[]; 17 | format?: string[]; 18 | store?: string; 19 | reporters?: string[]; 20 | listeners?: string[]; 21 | blame?: boolean; 22 | cache?: boolean; 23 | silent?: boolean; 24 | debug?: boolean; 25 | verbose?: boolean; 26 | list?: boolean; 27 | absolute?: boolean; 28 | noSymlinks?: boolean; 29 | skipLocal?: boolean; 30 | ignoreCase?: boolean; 31 | gitignore?: boolean; 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | reportersOptions?: Record; 34 | tokensToSkip?: string[]; 35 | hashFunction?: (value: string) => string; 36 | exitCode?: number; 37 | } 38 | 39 | export type TOption = keyof IOptions; 40 | -------------------------------------------------------------------------------- /examples/c-test-files/basic_functions.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // Simple function 6 | int add(int a, int b) { 7 | return a + b; 8 | } 9 | 10 | // Function with static modifier 11 | static int multiply(int x, int y) { 12 | return x * y; 13 | } 14 | 15 | // Function with extern modifier 16 | extern void print_hello(void); 17 | 18 | // Inline function 19 | inline double square(double n) { 20 | return n * n; 21 | } 22 | 23 | // Function with no parameters 24 | void cleanup(void) { 25 | printf("Cleaning up resources\n"); 26 | } 27 | 28 | // Function with complex parameters 29 | char* concat_strings(const char* str1, const char* str2, size_t max_len) { 30 | char* result = malloc(max_len + 1); 31 | if (result) { 32 | strncpy(result, str1, max_len); 33 | strncat(result, str2, max_len - strlen(str1)); 34 | } 35 | return result; 36 | } 37 | 38 | // Main function 39 | int main(int argc, char* argv[]) { 40 | int result = add(5, 3); 41 | printf("Result: %d\n", result); 42 | 43 | double sq = square(4.5); 44 | printf("Square: %.2f\n", sq); 45 | 46 | cleanup(); 47 | return 0; 48 | } -------------------------------------------------------------------------------- /src/tools/__tests__/lerna-project-graph.test.ts: -------------------------------------------------------------------------------- 1 | import { getLernaProjectGraph } from "../lerna-project-graph.js"; 2 | 3 | describe("getLernaProjectGraph", () => { 4 | it("returns missing-config when the workspace lacks lerna setup", async () => { 5 | const result = await getLernaProjectGraph(process.cwd()); 6 | expect(result.ok).toBe(false); 7 | if (!result.ok) { 8 | expect(result.reason).toBe("missing-config"); 9 | expect(result.message).toMatch(/No lerna\.json/); 10 | expect(result.cached ?? false).toBe(false); 11 | 12 | const cachedResult = await getLernaProjectGraph(process.cwd()); 13 | expect(cachedResult.ok).toBe(false); 14 | if (!cachedResult.ok) { 15 | expect(cachedResult.reason).toBe("missing-config"); 16 | expect(cachedResult.cached).toBe(true); 17 | 18 | const forcedResult = await getLernaProjectGraph(process.cwd(), { force: true }); 19 | expect(forcedResult.ok).toBe(false); 20 | if (!forcedResult.ok) { 21 | expect(forcedResult.reason).toBe("missing-config"); 22 | expect(forcedResult.cached ?? false).toBe(false); 23 | } 24 | } 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/utils/tmp-log.ts: -------------------------------------------------------------------------------- 1 | import { appendFileSync, mkdirSync } from "node:fs"; 2 | import { tmpdir } from "node:os"; 3 | import { join } from "node:path"; 4 | 5 | function safeJsonStringify(value: unknown): string { 6 | try { 7 | return JSON.stringify(value); 8 | } catch { 9 | return '"[unserializable]"'; 10 | } 11 | } 12 | 13 | export function getGlobalTmpLogDir(): string { 14 | return join(tmpdir(), "code-graph-rag-mcp"); 15 | } 16 | 17 | export function getGlobalTmpLogFile(): string { 18 | const dateStr = new Date().toISOString().slice(0, 10); 19 | return join(getGlobalTmpLogDir(), `mcp-server-${dateStr}.log`); 20 | } 21 | 22 | export function appendGlobalTmpLog(message: string, data?: Record): void { 23 | try { 24 | const dir = getGlobalTmpLogDir(); 25 | mkdirSync(dir, { recursive: true }); 26 | const ts = new Date().toISOString(); 27 | const pid = process.pid; 28 | const payload = data ? ` DATA: ${safeJsonStringify(data)}` : ""; 29 | appendFileSync(getGlobalTmpLogFile(), `[${ts}] [PID ${pid}] ${message}${payload}\n`, { encoding: "utf8" }); 30 | } catch { 31 | // best-effort only; never crash MCP startup due to logging 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/config/logging-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Centralized Logging Configuration 3 | * 4 | * Provides configuration for the rotated logging system 5 | */ 6 | 7 | import { resolve } from "node:path"; 8 | import { type LoggerConfig, LogLevel } from "../utils/logger-types.js"; 9 | 10 | // Get the root directory of the project 11 | const projectRoot = process.cwd().includes("examples/") ? resolve(process.cwd(), "../..") : process.cwd(); 12 | 13 | export const LOGGING_CONFIG: LoggerConfig = { 14 | logDir: resolve(projectRoot, "logs_llm"), 15 | maxFileSize: 10 * 1024 * 1024, // 10MB 16 | maxFiles: 20, 17 | logLevel: LogLevel.DEBUG, 18 | enableRotation: true, 19 | enableTimestamp: true, 20 | enableStackTrace: true, 21 | }; 22 | 23 | export const MCP_LOG_CATEGORIES = { 24 | SYSTEM: "SYSTEM", 25 | MCP_REQUEST: "MCP_REQUEST", 26 | MCP_RESPONSE: "MCP_RESPONSE", 27 | MCP_ERROR: "MCP_ERROR", 28 | AGENT_ACTIVITY: "AGENT_ACTIVITY", 29 | PARSE_ACTIVITY: "PARSE_ACTIVITY", 30 | QUERY_ACTIVITY: "QUERY_ACTIVITY", 31 | PERFORMANCE: "PERFORMANCE", 32 | INCIDENT: "INCIDENT", 33 | RECOVERY: "RECOVERY", 34 | } as const; 35 | 36 | export type MCPLogCategory = (typeof MCP_LOG_CATEGORIES)[keyof typeof MCP_LOG_CATEGORIES]; 37 | -------------------------------------------------------------------------------- /tests/integration/tool-descriptions.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { join } from "node:path"; 3 | import { describe, expect, it } from "@jest/globals"; 4 | 5 | function getIndexSource(): string { 6 | return readFileSync(join(process.cwd(), "src", "index.ts"), "utf8"); 7 | } 8 | 9 | function sliceAfter(haystack: string, needle: string, maxLen = 1200): string { 10 | const idx = haystack.indexOf(needle); 11 | if (idx < 0) return ""; 12 | return haystack.slice(idx, Math.min(haystack.length, idx + maxLen)); 13 | } 14 | 15 | describe("MCP tool descriptions (agent guidance)", () => { 16 | it("includes structured guidance for core tools", () => { 17 | const src = getIndexSource(); 18 | 19 | for (const toolName of [ 20 | "index", 21 | "batch_index", 22 | "list_file_entities", 23 | "resolve_entity", 24 | "get_entity_source", 25 | "semantic_search", 26 | "query", 27 | "get_graph_health", 28 | ]) { 29 | const window = sliceAfter(src, `name: "${toolName}"`); 30 | expect(window).toContain("description:"); 31 | expect(window).toContain("Use when:"); 32 | expect(window).toContain("Typical flow:"); 33 | expect(window).toContain("Output:"); 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Target Node.js 18+ with ES modules 4 | "lib": ["ES2022"], 5 | "target": "ES2022", 6 | "module": "ES2022", 7 | "moduleDetection": "force", 8 | "allowJs": true, 9 | 10 | // Module resolution for Node.js 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | 16 | // Output configuration 17 | "outDir": "./dist", 18 | "rootDir": "./src", 19 | "sourceMap": true, 20 | "declaration": true, 21 | "declarationMap": true, 22 | 23 | // Strict type checking 24 | "strict": true, 25 | "skipLibCheck": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "forceConsistentCasingInFileNames": true, 28 | 29 | // Performance optimizations 30 | "incremental": true, 31 | "tsBuildInfoFile": ".tsbuildinfo", 32 | 33 | // Code quality 34 | "noUnusedLocals": true, 35 | "noUnusedParameters": true, 36 | "noImplicitReturns": true, 37 | "noUncheckedIndexedAccess": true 38 | }, 39 | "typeRoots": ["./node_modules/@types", "./src/types"], 40 | "include": ["src/**/*", "src/types/**/*.d.ts"], 41 | "exclude": ["node_modules", "dist", "tmp", "src/__mocks__"] 42 | } 43 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: { 5 | index: "src/index.ts", 6 | }, 7 | sourcemap: true, 8 | clean: true, 9 | format: ["esm"], 10 | platform: "node", 11 | target: "node18", 12 | shims: false, 13 | 14 | // Optimizations for commodity hardware 15 | splitting: false, // Reduce memory usage during build 16 | minify: process.env.NODE_ENV === "production", 17 | treeshake: true, 18 | 19 | // Bundle size optimizations 20 | external: [ 21 | // Keep heavy dependencies external to reduce memory footprint 22 | "@modelcontextprotocol/sdk", 23 | 24 | // Tree-sitter dependencies must remain external (contain WASM files) 25 | "web-tree-sitter", 26 | "tree-sitter-javascript", 27 | "tree-sitter-typescript", 28 | "tree-sitter-python", 29 | "tree-sitter-c", 30 | "tree-sitter-cpp", 31 | "tree-sitter-c-sharp", 32 | "tree-sitter-rust", 33 | "tree-sitter-go", 34 | "tree-sitter-java", 35 | ], 36 | 37 | // Type generation 38 | dts: { 39 | resolve: true, 40 | }, 41 | 42 | // Ensure executable permissions for CLI 43 | onSuccess: async () => { 44 | if (process.platform !== "win32") { 45 | const { chmod } = await import("node:fs/promises"); 46 | await chmod("./dist/index.js", 0o755); 47 | } 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /tests/integration/tool-envelope-core.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { join } from "node:path"; 3 | import { describe, expect, it } from "@jest/globals"; 4 | 5 | function getIndexSource(): string { 6 | return readFileSync(join(process.cwd(), "src", "index.ts"), "utf8"); 7 | } 8 | 9 | function expectRegex(src: string, pattern: RegExp) { 10 | expect(pattern.test(src)).toBe(true); 11 | } 12 | 13 | describe("Core tool handlers use response envelope helpers", () => { 14 | it("wraps core success paths via toolOk()", () => { 15 | const src = getIndexSource(); 16 | 17 | expectRegex(src, /if \(name === "get_version"\)[\s\S]*?asMcpJson\(\s*toolOk\(/); 18 | expectRegex(src, /case "index":[\s\S]*?asMcpJson\(\s*toolOk\(/); 19 | expectRegex(src, /case "batch_index":[\s\S]*?asMcpJson\(\s*toolOk\(/); 20 | expectRegex(src, /case "semantic_search":[\s\S]*?asMcpJson\(\s*toolOk\(/); 21 | expectRegex(src, /case "query":[\s\S]*?asMcpJson\(\s*toolOk\(/); 22 | expectRegex(src, /case "get_graph":[\s\S]*?asMcpJson\(\s*toolOk\(/); 23 | expectRegex(src, /case "get_graph_health":[\s\S]*?asMcpJson\(\s*toolOk\(/); 24 | }); 25 | 26 | it("wraps failure paths via toolFail()", () => { 27 | const src = getIndexSource(); 28 | 29 | expect(src).toContain('toolFail("agent_busy"'); 30 | expect(src).toContain('toolFail("tool_error"'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/integration/jscpd-tool.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | import { runJscpdCloneDetection } from "../../src/tools/jscpd.js"; 5 | 6 | const currentDir = dirname(fileURLToPath(import.meta.url)); 7 | const fixtureRoot = join(currentDir, "../fixtures/jscpd-clones"); 8 | 9 | describe("JSCPD clone detection tool", () => { 10 | it("detects duplicated code blocks within fixtures", async () => { 11 | const result = await runJscpdCloneDetection({ 12 | paths: [fixtureRoot], 13 | formats: ["ts"], 14 | minTokens: 5, 15 | minLines: 3, 16 | }); 17 | 18 | expect(result.clones.length).toBeGreaterThan(0); 19 | expect(result.summary.cloneCount).toBeGreaterThan(0); 20 | expect(result.summary.totalLinesAnalyzed).toBeGreaterThan(0); 21 | expect(result.summary.duplicatedLines).toBeGreaterThan(0); 22 | const firstDetail = result.summary.clones[0]; 23 | expect(firstDetail).toBeDefined(); 24 | expect(firstDetail?.snippetA.length ?? 0).toBeGreaterThan(0); 25 | expect(firstDetail?.snippetB.length ?? 0).toBeGreaterThan(0); 26 | 27 | const firstClone = result.clones[0]; 28 | expect(firstClone.duplicationA.sourceId).toContain("alpha.ts"); 29 | expect(firstClone.duplicationB.sourceId).toContain("beta.ts"); 30 | expect(result.statistic.total.clones).toBeGreaterThan(0); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/tools/get-entity-source.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises"; 2 | import type { GraphStorageImpl } from "../storage/graph-storage.js"; 3 | import type { Entity } from "../types/storage.js"; 4 | import { extractSourceSnippetFromText } from "../utils/source-snippet.js"; 5 | 6 | export type GetEntitySourceResult = { 7 | entity: Entity; 8 | filePath: string; 9 | entityRange: { startLine: number; endLine: number }; 10 | snippetRange: { startLine: number; endLine: number }; 11 | snippet: string; 12 | truncated: boolean; 13 | }; 14 | 15 | export async function getEntitySource(options: { 16 | storage: GraphStorageImpl; 17 | entity: Entity; 18 | filePath: string; 19 | contextLines?: number; 20 | maxBytes?: number; 21 | }): Promise { 22 | const { storage: _storage, entity, filePath, contextLines, maxBytes } = options; 23 | const text = await readFile(filePath, "utf8"); 24 | 25 | const startLine = Number((entity as any).location?.start?.line ?? 1) || 1; 26 | const endLine = Number((entity as any).location?.end?.line ?? startLine) || startLine; 27 | const extracted = extractSourceSnippetFromText({ 28 | text, 29 | startLine, 30 | endLine, 31 | contextLines, 32 | maxBytes, 33 | }); 34 | 35 | return { 36 | entity, 37 | filePath, 38 | entityRange: extracted.entityRange, 39 | snippetRange: extracted.snippetRange, 40 | snippet: extracted.snippet, 41 | truncated: extracted.truncated, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /tests/parsers/parser-cache.test.ts: -------------------------------------------------------------------------------- 1 | import { TreeSitterParser } from "../../src/parsers/tree-sitter-parser"; 2 | 3 | describe("TreeSitterParser cache isolation by internal content hash", () => { 4 | let parser: TreeSitterParser; 5 | 6 | beforeAll(async () => { 7 | parser = new TreeSitterParser(); 8 | await parser.initialize(); 9 | }); 10 | 11 | afterEach(() => { 12 | parser.clearCache(); 13 | }); 14 | 15 | it("should not return cached result for same filePath and same external hash if content differs", async () => { 16 | const filePath = "test.c"; 17 | const extHash = "same-hash"; 18 | 19 | const code1 = ` 20 | #include 21 | int add(int a, int b) { return a + b; } 22 | `; 23 | 24 | const code2 = ` 25 | #include 26 | int sub(int a, int b) { return a - b; } 27 | `; 28 | 29 | const r1 = await parser.parse(filePath, code1, extHash); 30 | const r2 = await parser.parse(filePath, code2, extHash); 31 | 32 | const f1 = r1.entities.find((e) => e.type === "function" && e.name === "add"); 33 | const f2 = r2.entities.find((e) => e.type === "function" && e.name === "sub"); 34 | expect(f1).toBeDefined(); 35 | expect(f2).toBeDefined(); 36 | 37 | const inc1 = (r1.relationships || []).filter((rel) => rel.type === "imports").map((rel) => rel.to); 38 | const inc2 = (r2.relationships || []).filter((rel) => rel.type === "imports").map((rel) => rel.to); 39 | expect(inc1).toContain("stdio.h"); 40 | expect(inc2).toContain("stdlib.h"); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/utils/stdio-console.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MCP stdio servers must not write non-JSON data to stdout. 3 | * 4 | * The MCP SDK transport writes JSON-RPC messages to `process.stdout`. Any logs on stdout 5 | * can corrupt the stream and cause strict clients (e.g., Codex in VSCode) to fail. 6 | * 7 | * This module routes console's stdout-backed methods (log/info/debug/dir/table) to stderr 8 | * to prevent stdout pollution during MCP runs. 9 | */ 10 | 11 | import { appendGlobalTmpLog } from "./tmp-log.js"; 12 | 13 | export function redirectConsoleStdoutToStderr(): void { 14 | const argv = process.argv.slice(2); 15 | if (argv.includes("--help") || argv.includes("-h") || argv.includes("--version") || argv.includes("-v")) { 16 | appendGlobalTmpLog("stdio-console: skip redirect (help/version mode)"); 17 | return; 18 | } 19 | 20 | if (process.env.MCP_STDIO_ALLOW_STDOUT_LOGS === "1") { 21 | appendGlobalTmpLog("stdio-console: MCP_STDIO_ALLOW_STDOUT_LOGS=1 (stdout logs enabled; strict clients may fail)"); 22 | return; 23 | } 24 | 25 | const c = console as any; 26 | 27 | // Ensure any console method that writes to stdout uses stderr instead. 28 | if (c?._stdout && c._stdout !== process.stderr) c._stdout = process.stderr; 29 | if (c?._stderr && c._stderr !== process.stderr) c._stderr = process.stderr; 30 | 31 | appendGlobalTmpLog("stdio-console: redirected console stdout->stderr", { 32 | stdoutIsTTY: Boolean(process.stdout.isTTY), 33 | stderrIsTTY: Boolean(process.stderr.isTTY), 34 | }); 35 | } 36 | 37 | redirectConsoleStdoutToStderr(); 38 | -------------------------------------------------------------------------------- /src/types/tree-sitter-modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "tree-sitter-javascript" { 2 | import type { Language } from "tree-sitter"; 3 | const Lang: Language; 4 | export default Lang; 5 | } 6 | 7 | declare module "tree-sitter-python" { 8 | import type { Language } from "tree-sitter"; 9 | const Lang: Language; 10 | export default Lang; 11 | } 12 | 13 | declare module "tree-sitter-c" { 14 | import type { Language } from "tree-sitter"; 15 | const Lang: Language; 16 | export default Lang; 17 | } 18 | 19 | declare module "tree-sitter-cpp" { 20 | import type { Language } from "tree-sitter"; 21 | const Lang: Language; 22 | export default Lang; 23 | } 24 | 25 | declare module "tree-sitter-rust" { 26 | import type { Language } from "tree-sitter"; 27 | const Lang: Language; 28 | export default Lang; 29 | } 30 | 31 | declare module "tree-sitter-c-sharp" { 32 | import type { Language } from "tree-sitter"; 33 | const Lang: Language; 34 | export default Lang; 35 | } 36 | 37 | declare module "tree-sitter-go" { 38 | import type { Language } from "tree-sitter"; 39 | const Lang: Language; 40 | export default Lang; 41 | } 42 | 43 | declare module "tree-sitter-java" { 44 | import type { Language } from "tree-sitter"; 45 | const Lang: Language; 46 | export default Lang; 47 | } 48 | 49 | declare module "tree-sitter-typescript" { 50 | import type { Language } from "tree-sitter"; 51 | export const typescript: Language; 52 | export const tsx: Language; 53 | const _default: { typescript: Language; tsx: Language }; 54 | export default _default; 55 | } 56 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest').Config} */ 2 | const detectOpenHandles = process.env.JEST_DETECT_OPEN_HANDLES === "1"; 3 | const forceExit = process.env.JEST_FORCE_EXIT === "1"; 4 | 5 | export default { 6 | preset: "ts-jest/presets/default-esm", 7 | testEnvironment: "node", 8 | clearMocks: true, 9 | resetMocks: true, 10 | restoreMocks: true, 11 | resetModules: true, 12 | extensionsToTreatAsEsm: [".ts"], 13 | moduleNameMapper: { 14 | "^(?:\\.{1,2}/)+semantic/(.*)\\.js$": "/src/semantic/$1.ts", 15 | "^(?:\\.{1,2}/)+semantic/(?!.*\\.js$)(.*)$": "/src/semantic/$1.ts", 16 | "^(?:\\.{1,2}/)+storage/(.*)\\.js$": "/src/storage/$1.ts", 17 | "^(?:\\.{1,2}/)+storage/(?!.*\\.js$)(.*)$": "/src/storage/$1.ts", 18 | "^(\\.{1,2}/.*)\\.js$": "$1", 19 | "^nanoid$": "/src/__mocks__/nanoid.cjs", 20 | "^p-limit$": "/src/__mocks__/p-limit.cjs", 21 | ".*/connection-pool\\.js$": "/src/__mocks__/connection-pool.cjs", 22 | }, 23 | transform: { 24 | "^.+\\.tsx?$": [ 25 | "ts-jest", 26 | { 27 | useESM: true, 28 | tsconfig: "tsconfig.test.json", 29 | }, 30 | ], 31 | }, 32 | testMatch: ["**/tests/**/*.test.ts", "**/tests/**/*.spec.ts"], 33 | collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts", "!src/**/__mocks__/**"], 34 | coverageDirectory: "coverage", 35 | coverageReporters: ["text", "lcov", "html"], 36 | verbose: true, 37 | setupFilesAfterEnv: ["/jest.setup.js"], 38 | maxWorkers: 1, 39 | detectOpenHandles, 40 | forceExit, 41 | silent: false, 42 | }; 43 | -------------------------------------------------------------------------------- /tests/agents/base-agent.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import { BaseAgent } from "../../src/agents/base.js"; 3 | import { AgentType } from "../../src/types/agent.js"; 4 | import { AgentBusyError } from "../../src/types/errors.js"; 5 | 6 | class TestAgent extends BaseAgent { 7 | constructor() { 8 | super(AgentType.DEV, { 9 | maxConcurrency: 1, 10 | memoryLimit: 64, 11 | priority: 5, 12 | }); 13 | } 14 | 15 | protected async onInitialize(): Promise {} 16 | protected async onShutdown(): Promise {} 17 | 18 | protected canProcessTask(task: any): boolean { 19 | return task.type === "test"; 20 | } 21 | 22 | protected async processTask(): Promise { 23 | await new Promise((resolve) => setTimeout(resolve, 10)); 24 | return { ok: true }; 25 | } 26 | 27 | protected async handleMessage(): Promise {} 28 | } 29 | 30 | describe("BaseAgent backpressure", () => { 31 | it("throws AgentBusyError with retry hints when agent is busy", async () => { 32 | const agent = new TestAgent(); 33 | await agent.initialize(); 34 | 35 | const taskA = { 36 | id: "a", 37 | type: "test", 38 | priority: 1, 39 | payload: {}, 40 | createdAt: Date.now(), 41 | }; 42 | 43 | const taskB = { 44 | ...taskA, 45 | id: "b", 46 | }; 47 | 48 | const running = agent.process(taskA as any); 49 | 50 | await expect(agent.process(taskB as any)).rejects.toBeInstanceOf(AgentBusyError); 51 | 52 | try { 53 | await running; 54 | } finally { 55 | await agent.shutdown(); 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/vendor/jscpd/in-files-detector.ts: -------------------------------------------------------------------------------- 1 | import type { IClone, IMapFrame, IOptions, IStore, ISubscriber, ITokenizer } from "./core"; 2 | import { Detector } from "./core"; 3 | import type { EntryWithContent } from "./files"; 4 | import { getFilesToDetect } from "./files"; 5 | 6 | export type DetectorSource = EntryWithContent; 7 | 8 | export class InFilesDetector { 9 | constructor( 10 | private readonly tokenizer: ITokenizer, 11 | private readonly store: IStore, 12 | private readonly options: IOptions, 13 | private readonly subscribers: ISubscriber[] = [], 14 | ) {} 15 | 16 | async detectFromOptions(options: IOptions): Promise { 17 | const files = getFilesToDetect(options); 18 | return this.detect(files); 19 | } 20 | 21 | async detect(files: EntryWithContent[]): Promise { 22 | const entries = [...files]; 23 | if (entries.length === 0) return []; 24 | 25 | const detector = new Detector(this.tokenizer, this.store, [], this.options); 26 | for (const subscriber of this.subscribers) { 27 | const hooks = subscriber.subscribe(); 28 | Object.entries(hooks).forEach(([event, handler]) => { 29 | if (handler) { 30 | detector.on(event, handler); 31 | } 32 | }); 33 | } 34 | 35 | const clones: IClone[] = []; 36 | 37 | for (const entry of entries) { 38 | const detected = await detector.detect(entry.path, entry.content, this.resolveFormat(entry.path)); 39 | clones.push(...detected); 40 | } 41 | 42 | return clones; 43 | } 44 | 45 | private resolveFormat(path: string): string { 46 | return path.split(".").pop() ?? "text"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/utils/index-file-collection.test.ts: -------------------------------------------------------------------------------- 1 | import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; 2 | import { tmpdir } from "node:os"; 3 | import { join } from "node:path"; 4 | import { describe, expect, it } from "@jest/globals"; 5 | import { collectIndexableFiles, DEFAULT_INDEX_EXCLUDE_PATTERNS } from "../../src/utils/index-file-collection.js"; 6 | 7 | describe("collectIndexableFiles", () => { 8 | it("includes markdown by default and respects default excludes", () => { 9 | const root = mkdtempSync(join(tmpdir(), "cgr-index-")); 10 | 11 | mkdirSync(join(root, "docs"), { recursive: true }); 12 | mkdirSync(join(root, "src"), { recursive: true }); 13 | mkdirSync(join(root, "node_modules", "left-pad"), { recursive: true }); 14 | 15 | writeFileSync(join(root, "docs", "readme.md"), "# Hello\n"); 16 | writeFileSync(join(root, "src", "app.ts"), "export const x = 1;\n"); 17 | writeFileSync(join(root, "node_modules", "left-pad", "index.js"), "module.exports = {};\n"); 18 | writeFileSync(join(root, "notes.txt"), "ignore me\n"); 19 | 20 | const result = collectIndexableFiles(root, [...DEFAULT_INDEX_EXCLUDE_PATTERNS]); 21 | const rel = (p: string) => 22 | p 23 | .slice(root.length + 1) 24 | .split("\\") 25 | .join("/"); 26 | 27 | const relPaths = new Set(result.files.map(rel)); 28 | expect(relPaths.has("docs/readme.md")).toBe(true); 29 | expect(relPaths.has("src/app.ts")).toBe(true); 30 | expect(relPaths.has("node_modules/left-pad/index.js")).toBe(false); 31 | expect(relPaths.has("notes.txt")).toBe(false); 32 | 33 | expect(result.stats.includedFiles).toBe(result.files.length); 34 | expect(result.stats.scannedFiles).toBeGreaterThanOrEqual(result.files.length); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/agents/resource-adjustment.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@jest/globals"; 2 | import { DevAgent } from "../../src/agents/dev-agent.js"; 3 | import { QueryAgent } from "../../src/agents/query-agent.js"; 4 | import { SemanticAgent } from "../../src/agents/semantic-agent.js"; 5 | import type { KnowledgeEntry } from "../../src/core/knowledge-bus.js"; 6 | 7 | describe("Resource adjustment handlers", () => { 8 | it("scales DevAgent batch size and concurrency", () => { 9 | const agent = new DevAgent(); 10 | const entry = { 11 | data: { 12 | newMemoryLimit: 512, 13 | newAgentLimit: 6, 14 | }, 15 | } as KnowledgeEntry; 16 | 17 | (agent as any).handleResourceAdjustment(entry); 18 | 19 | expect(agent.capabilities.maxConcurrency).toBeGreaterThanOrEqual(1); 20 | expect((agent as any).indexBatchSize).toBeGreaterThanOrEqual(10); 21 | }); 22 | 23 | it("updates QueryAgent concurrency limiter", () => { 24 | const agent = new QueryAgent(); 25 | const entry = { 26 | data: { 27 | newAgentLimit: 4, 28 | }, 29 | } as KnowledgeEntry; 30 | 31 | (agent as any).handleResourceAdjustment(entry); 32 | 33 | expect(agent.capabilities.maxConcurrency).toBeGreaterThanOrEqual(1); 34 | }); 35 | 36 | it("adjusts SemanticAgent concurrency and batch size", () => { 37 | const agent = new SemanticAgent(); 38 | const entry = { 39 | data: { 40 | newMemoryLimit: 480, 41 | newAgentLimit: 4, 42 | }, 43 | } as KnowledgeEntry; 44 | 45 | (agent as any).handleResourceAdjustment(entry); 46 | 47 | expect(agent.capabilities.maxConcurrency).toBeGreaterThanOrEqual(1); 48 | expect((agent as any).embeddingBatchSize).toBeGreaterThanOrEqual(1); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/utils/source-snippet.ts: -------------------------------------------------------------------------------- 1 | export type SourceSnippetResult = { 2 | snippet: string; 3 | snippetRange: { startLine: number; endLine: number }; 4 | entityRange: { startLine: number; endLine: number }; 5 | truncated: boolean; 6 | }; 7 | 8 | export function extractSourceSnippetFromText(params: { 9 | text: string; 10 | startLine: number; 11 | endLine: number; 12 | contextLines?: number; 13 | maxBytes?: number; 14 | }): SourceSnippetResult { 15 | const { text } = params; 16 | const contextLines = Math.max(0, Math.min(200, Number(params.contextLines ?? 5) || 0)); 17 | const maxBytes = Math.max(1024, Math.min(512_000, Number(params.maxBytes ?? 64_000) || 64_000)); 18 | 19 | const lines = text.split(/\r?\n/); 20 | const totalLines = lines.length; 21 | 22 | const entityStart = Math.max(1, Math.min(totalLines, Number(params.startLine) || 1)); 23 | const entityEnd = Math.max(entityStart, Math.min(totalLines, Number(params.endLine) || entityStart)); 24 | 25 | const snippetStart = Math.max(1, entityStart - contextLines); 26 | const snippetEnd = Math.min(totalLines, entityEnd + contextLines); 27 | 28 | const rawSnippet = lines.slice(snippetStart - 1, snippetEnd).join("\n"); 29 | const rawBytes = Buffer.byteLength(rawSnippet, "utf8"); 30 | 31 | if (rawBytes <= maxBytes) { 32 | return { 33 | snippet: rawSnippet, 34 | snippetRange: { startLine: snippetStart, endLine: snippetEnd }, 35 | entityRange: { startLine: entityStart, endLine: entityEnd }, 36 | truncated: false, 37 | }; 38 | } 39 | 40 | const truncatedText = rawSnippet.slice(0, maxBytes); 41 | return { 42 | snippet: truncatedText, 43 | snippetRange: { startLine: snippetStart, endLine: snippetEnd }, 44 | entityRange: { startLine: entityStart, endLine: entityEnd }, 45 | truncated: true, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /examples/c-test-files/structures.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // Basic struct definition 5 | struct Point { 6 | double x; 7 | double y; 8 | }; 9 | 10 | // Typedef struct 11 | typedef struct { 12 | char name[50]; 13 | int age; 14 | float salary; 15 | } Employee; 16 | 17 | // Struct with function pointers 18 | typedef struct { 19 | int (*add)(int, int); 20 | int (*subtract)(int, int); 21 | void (*print_result)(int); 22 | } Calculator; 23 | 24 | // Union definition 25 | union Data { 26 | int integer; 27 | float floating; 28 | char string[20]; 29 | }; 30 | 31 | // Enum definition 32 | enum Status { 33 | PENDING = 0, 34 | APPROVED = 1, 35 | REJECTED = 2, 36 | COMPLETED = 3 37 | }; 38 | 39 | // Enum with custom values 40 | typedef enum { 41 | RED = 0xFF0000, 42 | GREEN = 0x00FF00, 43 | BLUE = 0x0000FF, 44 | WHITE = 0xFFFFFF 45 | } Color; 46 | 47 | // Functions working with structs 48 | struct Point create_point(double x, double y) { 49 | struct Point p; 50 | p.x = x; 51 | p.y = y; 52 | return p; 53 | } 54 | 55 | double distance(struct Point p1, struct Point p2) { 56 | double dx = p1.x - p2.x; 57 | double dy = p1.y - p2.y; 58 | return sqrt(dx * dx + dy * dy); 59 | } 60 | 61 | Employee* create_employee(const char* name, int age, float salary) { 62 | Employee* emp = malloc(sizeof(Employee)); 63 | if (emp) { 64 | strncpy(emp->name, name, sizeof(emp->name) - 1); 65 | emp->name[sizeof(emp->name) - 1] = '\0'; 66 | emp->age = age; 67 | emp->salary = salary; 68 | } 69 | return emp; 70 | } 71 | 72 | void print_employee(const Employee* emp) { 73 | if (emp) { 74 | printf("Employee: %s, Age: %d, Salary: %.2f\n", 75 | emp->name, emp->age, emp->salary); 76 | } 77 | } -------------------------------------------------------------------------------- /src/semantic/providers/memory-provider.ts: -------------------------------------------------------------------------------- 1 | import type { EmbeddingProvider, EmbedOptions, ProviderInfo } from "./base.js"; 2 | 3 | export interface MemoryOptions { 4 | dimension?: number; 5 | } 6 | 7 | const DEFAULT_DIM = 384; 8 | 9 | function hash32(text: string): number { 10 | let hash = 0; 11 | for (let i = 0; i < text.length; i++) { 12 | hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0; 13 | } 14 | return hash >>> 0; 15 | } 16 | 17 | export class MemoryProvider implements EmbeddingProvider { 18 | public info: ProviderInfo; 19 | 20 | constructor(opts: MemoryOptions = {}) { 21 | const dimension = opts?.dimension ?? DEFAULT_DIM; 22 | this.info = { 23 | name: "memory", 24 | model: "deterministic-hash", 25 | dimension, 26 | supportsBatch: true, 27 | }; 28 | } 29 | 30 | async initialize(): Promise {} 31 | 32 | getDimension(): number | undefined { 33 | return this.info.dimension; 34 | } 35 | 36 | private generate(text: string): Float32Array { 37 | const dim = this.info.dimension ?? DEFAULT_DIM; 38 | const embedding = new Float32Array(dim); 39 | let state = hash32(text) || 1; 40 | for (let i = 0; i < dim; i++) { 41 | state = (state * 1664525 + 1013904223) >>> 0; 42 | embedding[i] = state / 0xffffffff - 0.5; 43 | } 44 | let norm = 0; 45 | for (let i = 0; i < dim; i++) { 46 | const v = embedding[i] ?? 0; 47 | norm += v * v; 48 | } 49 | norm = Math.sqrt(norm) || 1; 50 | for (let i = 0; i < dim; i++) { 51 | const v = embedding[i] ?? 0; 52 | embedding[i] = v / norm; 53 | } 54 | return embedding; 55 | } 56 | 57 | async embed(text: string, _opts?: EmbedOptions): Promise { 58 | return this.generate(text); 59 | } 60 | 61 | async embedBatch(texts: string[], _opts?: EmbedOptions): Promise { 62 | return texts.map((t) => this.generate(t)); 63 | } 64 | 65 | async close(): Promise {} 66 | } 67 | -------------------------------------------------------------------------------- /src/tools/graph-query.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tool for querying the graph database directly via GraphStorage API 3 | */ 4 | 5 | import type { GraphStorageImpl } from "../storage/graph-storage.js"; 6 | import type { Entity, Relationship } from "../types/storage.js"; 7 | 8 | function likePattern(input: string): string { 9 | // Minimal escaping for LIKE; wrap with % for contains semantics 10 | const escaped = input.replace(/[%_]/g, (m) => `\\${m}`); 11 | return `%${escaped}%`; 12 | } 13 | 14 | export async function queryGraphEntities( 15 | storage: GraphStorageImpl, 16 | query?: string, 17 | limit: number = 100, 18 | offset: number = 0, 19 | ): Promise<{ 20 | entities: Entity[]; 21 | relationships: Relationship[]; 22 | stats: { 23 | totalEntities: number; 24 | totalRelationships: number; 25 | }; 26 | }> { 27 | // Use the storage’s executeQuery to avoid raw SQL 28 | const q = await storage.executeQuery({ 29 | type: "entity", 30 | limit, 31 | offset, 32 | filters: query 33 | ? { 34 | // Pass LIKE-compatible pattern via RegExp source consumed by storage 35 | name: new RegExp(likePattern(query)), 36 | } 37 | : undefined, 38 | }); 39 | 40 | return { 41 | entities: q.entities, 42 | relationships: q.relationships, 43 | stats: { 44 | totalEntities: q.stats.totalEntities, 45 | totalRelationships: q.stats.totalRelationships, 46 | }, 47 | }; 48 | } 49 | 50 | export async function getGraphStats(storage: GraphStorageImpl): Promise<{ 51 | entities: { total: number; byType: Record }; 52 | relationships: { total: number; byType: Record }; 53 | files: { total: number }; 54 | }> { 55 | // Use storage metrics for reliable totals; byType left empty to avoid raw SQL 56 | const metrics = await storage.getMetrics(); 57 | return { 58 | entities: { total: metrics.totalEntities, byType: {} }, 59 | relationships: { total: metrics.totalRelationships, byType: {} }, 60 | files: { total: metrics.totalFiles }, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/tools/analyze-code-impact.ts: -------------------------------------------------------------------------------- 1 | import type { GraphStorageImpl } from "../storage/graph-storage.js"; 2 | import type { Relationship } from "../types/storage.js"; 3 | 4 | export type AnalyzeCodeImpactTraversal = { 5 | directDependents: Set; 6 | transitiveDependents: Set; 7 | outboundDependencies: Set; 8 | depthUsed: number; 9 | rootRelationships: Relationship[]; 10 | }; 11 | 12 | export async function analyzeCodeImpactTraversal( 13 | storage: GraphStorageImpl, 14 | rootId: string, 15 | depth: number, 16 | ): Promise { 17 | const maxDepth = Math.max(1, Math.min(10, Number(depth ?? 2) || 2)); 18 | const rootRels = await storage.getRelationshipsForEntity(rootId); 19 | 20 | const outboundIds = new Set(); 21 | const directIds = new Set(); 22 | 23 | for (const rel of rootRels) { 24 | if (rel.fromId === rootId) outboundIds.add(rel.toId); 25 | if (rel.toId === rootId) directIds.add(rel.fromId); 26 | } 27 | 28 | const visited = new Set([rootId]); 29 | const queue: Array<{ id: string; level: number }> = Array.from(directIds).map((id) => ({ id, level: 1 })); 30 | for (const id of directIds) visited.add(id); 31 | 32 | const indirectIds = new Set(); 33 | 34 | while (queue.length) { 35 | const current = queue.shift()!; 36 | if (current.level >= maxDepth) continue; 37 | 38 | const rels = await storage.getRelationshipsForEntity(current.id); 39 | for (const rel of rels) { 40 | if (rel.toId !== current.id) continue; 41 | const dependentId = rel.fromId; 42 | if (!dependentId || visited.has(dependentId)) continue; 43 | visited.add(dependentId); 44 | const nextLevel = current.level + 1; 45 | if (nextLevel >= 2) indirectIds.add(dependentId); 46 | queue.push({ id: dependentId, level: nextLevel }); 47 | } 48 | } 49 | 50 | return { 51 | directDependents: directIds, 52 | transitiveDependents: indirectIds, 53 | outboundDependencies: outboundIds, 54 | depthUsed: maxDepth, 55 | rootRelationships: rootRels, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /tests/parsers/tree-sitter-parser.test.ts: -------------------------------------------------------------------------------- 1 | import { TreeSitterParser } from "../../src/parsers/tree-sitter-parser"; 2 | import type { SupportedLanguage } from "../../src/types/parser"; 3 | 4 | describe("Language loaders resolve grammars correctly", () => { 5 | let parser: TreeSitterParser; 6 | 7 | beforeAll(async () => { 8 | parser = new TreeSitterParser(); 9 | await parser.initialize(); 10 | }); 11 | 12 | afterEach(() => { 13 | parser.clearCache(); 14 | }); 15 | 16 | const samples: Array<{ file: string; code: string; expected: SupportedLanguage }> = [ 17 | { file: "a.js", code: "export function f(){ return 1 }", expected: "javascript" }, 18 | { file: "a.jsx", code: "export default function C(){ return
}", expected: "jsx" }, 19 | { file: "a.ts", code: "export function sum(a:number,b:number){return a+b}", expected: "typescript" }, 20 | { file: "a.tsx", code: "export function C(){ return
}", expected: "tsx" }, 21 | { file: "a.py", code: "def foo(x):\n return x", expected: "python" }, 22 | { file: "a.c", code: "int main(){return 0;}", expected: "c" }, 23 | { file: "a.cpp", code: "int main(){return 0;}", expected: "cpp" }, 24 | { file: "a.rs", code: "fn main() {}", expected: "rust" }, 25 | { file: "a.cs", code: "class A{ static void Main(){} }", expected: "csharp" }, 26 | { file: "a.go", code: "package main\nfunc main(){}", expected: "go" }, 27 | { file: "a.java", code: "class A { public static void main(String[] a){} }", expected: "java" }, 28 | { file: "a.kt", code: "package p\nclass A { fun f(x:Int):Int { return x } }", expected: "kotlin" }, 29 | ]; 30 | 31 | it.each(samples)("loads grammar for %s and parses", async ({ file, code, expected }) => { 32 | const res = await parser.parse(file, code, "hash"); 33 | expect(res.language).toBe(expected); 34 | if ( 35 | expected === "javascript" || 36 | expected === "jsx" || 37 | expected === "typescript" || 38 | expected === "tsx" || 39 | expected === "kotlin" 40 | ) { 41 | expect(res.entities.length).toBeGreaterThan(0); 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/parsers/kotlin-analyzer.test.ts: -------------------------------------------------------------------------------- 1 | import { TreeSitterParser } from "../../src/parsers/tree-sitter-parser"; 2 | import type { EntityRelationship } from "../../src/types/parser"; 3 | 4 | describe("KotlinAnalyzer", () => { 5 | let parser: TreeSitterParser; 6 | 7 | beforeAll(async () => { 8 | parser = new TreeSitterParser(); 9 | await parser.initialize(); 10 | }); 11 | 12 | afterEach(() => { 13 | parser.clearCache(); 14 | }); 15 | 16 | it("extracts Kotlin entities and relationships", async () => { 17 | const code = ` 18 | package p.q 19 | 20 | import kotlin.collections.List 21 | import kotlin.io.println as p 22 | 23 | typealias Str = String 24 | 25 | interface I { 26 | fun f(): Int 27 | } 28 | 29 | data class A(val x: Int) : I { 30 | companion object { 31 | const val CONST = 1 32 | } 33 | 34 | override fun f(): Int { 35 | g() 36 | return x 37 | } 38 | } 39 | 40 | fun g() { p("hi") } 41 | 42 | fun Int.ext(): Int = this + 1 43 | `; 44 | 45 | const res = await parser.parse("sample.kt", code, "hash"); 46 | expect(res.language).toBe("kotlin"); 47 | 48 | const ids = new Set(res.entities.map((e) => e.id)); 49 | 50 | expect(ids.has("sample.kt:package:p.q")).toBe(true); 51 | expect(ids.has("sample.kt:typealias:Str")).toBe(true); 52 | expect(ids.has("sample.kt:class:I")).toBe(true); 53 | expect(ids.has("sample.kt:class:A")).toBe(true); 54 | expect(ids.has("sample.kt:class:A:property:x")).toBe(true); 55 | expect(ids.has("sample.kt:class:A.Companion:property:CONST")).toBe(true); 56 | expect(ids.has("sample.kt:class:A:method:f")).toBe(true); 57 | expect(ids.has("sample.kt:function:g")).toBe(true); 58 | 59 | const relationships = (res as any).relationships as EntityRelationship[] | undefined; 60 | expect(relationships?.some((r) => r.type === "imports" && r.to.includes("kotlin.collections.List"))).toBe(true); 61 | expect( 62 | relationships?.some((r) => r.type === "calls" && r.from === "sample.kt:class:A:method:f" && r.to === "g"), 63 | ).toBe(true); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/tools/list-entity-relationships.ts: -------------------------------------------------------------------------------- 1 | import type { GraphStorageImpl } from "../storage/graph-storage.js"; 2 | import type { Entity, Relationship } from "../types/storage.js"; 3 | 4 | export type ListEntityRelationshipsResult = { 5 | root: Entity; 6 | nodes: Map; 7 | relationships: Relationship[]; 8 | depthUsed: number; 9 | relationshipTypesUsed: string[] | null; 10 | }; 11 | 12 | export async function listEntityRelationshipsTraversal( 13 | storage: GraphStorageImpl, 14 | root: Entity, 15 | options: { depth?: number; relationshipTypes?: string[] }, 16 | ): Promise { 17 | const maxDepth = Math.max(1, Math.min(10, Number(options.depth ?? 1) || 1)); 18 | const typeFilter = options.relationshipTypes?.length ? new Set(options.relationshipTypes) : null; 19 | 20 | const visited = new Set(); 21 | const queue: Array<{ id: string; level: number }> = [{ id: root.id, level: 0 }]; 22 | const allRelationships: Relationship[] = []; 23 | const relSeen = new Set(); 24 | 25 | while (queue.length) { 26 | const current = queue.shift()!; 27 | if (visited.has(current.id)) continue; 28 | visited.add(current.id); 29 | 30 | const rels = await storage.getRelationshipsForEntity(current.id); 31 | for (const rel of rels) { 32 | if (typeFilter && !typeFilter.has(rel.type)) continue; 33 | const key = rel.id || `${rel.fromId}|${rel.toId}|${rel.type}`; 34 | if (!relSeen.has(key)) { 35 | relSeen.add(key); 36 | allRelationships.push(rel); 37 | } 38 | 39 | const neighborId = rel.fromId === current.id ? rel.toId : rel.fromId; 40 | if (!neighborId || visited.has(neighborId)) continue; 41 | if (current.level + 1 <= maxDepth) { 42 | queue.push({ id: neighborId, level: current.level + 1 }); 43 | } 44 | } 45 | } 46 | 47 | const nodes = new Map(); 48 | for (const id of visited) { 49 | const e = id === root.id ? root : await storage.getEntity(id); 50 | if (e) nodes.set(id, e); 51 | } 52 | 53 | return { 54 | root, 55 | nodes, 56 | relationships: allRelationships, 57 | depthUsed: maxDepth, 58 | relationshipTypesUsed: typeFilter ? Array.from(typeFilter) : null, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/storage/graph-storage-factory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Graph Storage Factory - Singleton GraphStorage instance 3 | * 4 | * Ensures all components use the same GraphStorageImpl instance 5 | * to prevent database state mismatch issues. 6 | * 7 | * TASK-034: Fix circular bug by ensuring single GraphStorage instance 8 | */ 9 | 10 | import { GraphStorageImpl } from "./graph-storage.js"; 11 | import { runMigrations } from "./schema-migrations.js"; 12 | import type { SQLiteManager } from "./sqlite-manager.js"; 13 | import { getSQLiteManager } from "./sqlite-manager.js"; 14 | 15 | let graphStorage: GraphStorageImpl | null = null; 16 | let boundManager: SQLiteManager | null = null; 17 | let initialization: { manager: SQLiteManager; promise: Promise } | null = null; 18 | 19 | export async function getGraphStorage(sqliteManager?: SQLiteManager): Promise { 20 | const manager = sqliteManager ?? getSQLiteManager(); 21 | 22 | if (!graphStorage || boundManager !== manager) { 23 | if (initialization && initialization.manager === manager) { 24 | return initialization.promise; 25 | } 26 | 27 | const promise = (async () => { 28 | console.log("[GraphStorageFactory] Creating NEW GraphStorage singleton instance"); 29 | if (!manager.isOpen()) { 30 | manager.initialize(); 31 | } 32 | runMigrations(manager); 33 | graphStorage = new GraphStorageImpl(manager); 34 | boundManager = manager; 35 | await graphStorage.initialize(); 36 | return graphStorage; 37 | })(); 38 | 39 | initialization = { manager, promise }; 40 | try { 41 | return await promise; 42 | } finally { 43 | if (initialization?.manager === manager) { 44 | initialization = null; 45 | } 46 | } 47 | } 48 | 49 | console.log("[GraphStorageFactory] Returning EXISTING GraphStorage singleton instance"); 50 | 51 | await graphStorage.initialize(); 52 | return graphStorage; 53 | } 54 | 55 | export async function initializeGraphStorage(sqliteManager: SQLiteManager): Promise { 56 | return getGraphStorage(sqliteManager); 57 | } 58 | 59 | export function resetGraphStorage(): void { 60 | graphStorage = null; 61 | boundManager = null; 62 | initialization = null; 63 | } 64 | -------------------------------------------------------------------------------- /tests/tools/resolve-entity.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, rmSync } from "node:fs"; 2 | import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; 3 | import { IndexerAgent } from "../../src/agents/indexer-agent.js"; 4 | import { getGraphStorage, resetGraphStorage } from "../../src/storage/graph-storage-factory.js"; 5 | import { getSQLiteManager, resetSQLiteManager } from "../../src/storage/sqlite-manager.js"; 6 | import { resolveEntityCandidates } from "../../src/tools/resolve-entity.js"; 7 | import { AgentStatus } from "../../src/types/agent.js"; 8 | import type { ParsedEntity } from "../../src/types/parser.js"; 9 | 10 | const TEST_DB_PATH = "./data/test-tool-resolve.db"; 11 | const CLEANUP_PATHS = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]; 12 | 13 | function e(name: string, line: number): ParsedEntity { 14 | return { 15 | name, 16 | type: "function", 17 | location: { 18 | start: { line, column: 0, index: line * 10 }, 19 | end: { line: line + 1, column: 0, index: line * 10 + 5 }, 20 | }, 21 | } as any; 22 | } 23 | 24 | describe("resolveEntityCandidates", () => { 25 | let agent: IndexerAgent; 26 | 27 | beforeEach(async () => { 28 | for (const p of CLEANUP_PATHS) { 29 | if (existsSync(p)) rmSync(p); 30 | } 31 | resetGraphStorage(); 32 | resetSQLiteManager(); 33 | const sqlite = getSQLiteManager({ path: TEST_DB_PATH }); 34 | agent = new IndexerAgent(sqlite); 35 | await agent.initialize(); 36 | }); 37 | 38 | afterEach(async () => { 39 | if (agent && agent.status !== AgentStatus.SHUTDOWN) await agent.shutdown(); 40 | for (const p of CLEANUP_PATHS) { 41 | if (existsSync(p)) rmSync(p); 42 | } 43 | resetGraphStorage(); 44 | resetSQLiteManager(); 45 | }); 46 | 47 | it("boosts candidates by filePathHint", async () => { 48 | await agent.indexEntities([e("Foo", 1)], "/tmp/a.ts"); 49 | await agent.indexEntities([e("Foo", 1)], "/tmp/b.ts"); 50 | 51 | const storage = await getGraphStorage(getSQLiteManager({ path: TEST_DB_PATH })); 52 | const candidates = await resolveEntityCandidates({ 53 | storage, 54 | name: "Foo", 55 | filePathHint: "/tmp/b.ts", 56 | limit: 10, 57 | }); 58 | 59 | expect(candidates[0]!.entity.filePath).toBe("/tmp/b.ts"); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/detector.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "node:events"; 2 | import type { IClone, ICloneValidator, IMapFrame, IOptions, IStore, ITokenizer, ITokensMap } from "./interfaces"; 3 | import { mild } from "./mode"; 4 | import { RabinKarp } from "./rabin-karp"; 5 | import { LinesLengthCloneValidator } from "./validators"; 6 | 7 | export type DetectorEvents = "CLONE_FOUND" | "CLONE_SKIPPED" | "START_DETECTION"; 8 | 9 | export class Detector extends EventEmitter { 10 | private algorithm: RabinKarp; 11 | 12 | constructor( 13 | private readonly tokenizer: ITokenizer, 14 | private readonly store: IStore, 15 | private readonly cloneValidators: ICloneValidator[] = [], 16 | private readonly options: IOptions, 17 | ) { 18 | super(); 19 | this.initCloneValidators(); 20 | this.algorithm = new RabinKarp(this.options, this, this.cloneValidators); 21 | this.options.minTokens = this.options.minTokens || 50; 22 | this.options.maxLines = this.options.maxLines || 500; 23 | this.options.minLines = this.options.minLines || 5; 24 | this.options.mode = this.options.mode || mild; 25 | } 26 | 27 | public async detect(id: string, text: string, format: string): Promise { 28 | const tokenMaps: ITokensMap[] = this.tokenizer.generateMaps(id, text, format, this.options); 29 | // TODO change stores implementation 30 | this.store.namespace(format); 31 | 32 | // @ts-expect-error 33 | const detect = async (tokenMap: ITokensMap, clones: IClone[]): Promise => { 34 | if (tokenMap) { 35 | this.emit("START_DETECTION", { source: tokenMap }); 36 | return this.algorithm.run(tokenMap, this.store).then((clns: IClone[]) => { 37 | clones.push(...clns); 38 | const nextTokenMap = tokenMaps.pop(); 39 | if (nextTokenMap) { 40 | return detect(nextTokenMap, clones); 41 | } else { 42 | return clones; 43 | } 44 | }); 45 | } 46 | }; 47 | const currentTokensMap = tokenMaps.pop(); 48 | return currentTokensMap ? detect(currentTokensMap, []) : []; 49 | } 50 | 51 | private initCloneValidators(): void { 52 | if (this.options.minLines || this.options.maxLines) { 53 | this.cloneValidators.push(new LinesLengthCloneValidator()); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/vendor/jscpd/tokenizer/simple-tokenizer.ts: -------------------------------------------------------------------------------- 1 | import type { IOptions, IToken, ITokenizer, ITokensMap } from "../core"; 2 | import { createTokensMaps } from "./tokens-map"; 3 | 4 | type ModeHandler = (token: IToken, options?: Partial) => boolean; 5 | 6 | const NEWLINE = /\r?\n/; 7 | 8 | function classifyToken(value: string): { type: string; normalized: string } { 9 | const trimmed = value.trim(); 10 | if (!trimmed.length) { 11 | return { type: "empty", normalized: "" }; 12 | } 13 | 14 | const lower = trimmed.toLowerCase(); 15 | if ( 16 | lower.startsWith("//") || 17 | lower.startsWith("#") || 18 | lower.startsWith("/*") || 19 | lower.startsWith("*") || 20 | lower.startsWith("--") 21 | ) { 22 | return { type: "comment", normalized: trimmed }; 23 | } 24 | 25 | return { type: "code", normalized: trimmed }; 26 | } 27 | 28 | function ensureMode(mode: IOptions["mode"]): ModeHandler { 29 | if (typeof mode === "function") { 30 | return mode as ModeHandler; 31 | } 32 | return (token: IToken): boolean => token.type !== "ignore"; 33 | } 34 | 35 | export class SimpleTokenizer implements ITokenizer { 36 | generateMaps(id: string, data: string, format: string, options: Partial): ITokensMap[] { 37 | const lines = data.split(NEWLINE); 38 | const tokens: IToken[] = []; 39 | let cursor = 0; 40 | 41 | for (let i = 0; i < lines.length; i++) { 42 | const line = lines[i] ?? ""; 43 | const length = line.length; 44 | const { type, normalized } = classifyToken(line); 45 | 46 | const token: IToken = { 47 | type, 48 | value: options.ignoreCase ? normalized.toLowerCase() : normalized, 49 | length, 50 | format, 51 | range: [cursor, cursor + length], 52 | loc: { 53 | start: { line: i + 1, column: 0, position: cursor }, 54 | end: { line: i + 1, column: length, position: cursor + length }, 55 | }, 56 | }; 57 | 58 | tokens.push(token); 59 | cursor += length + 1; // account for newline 60 | } 61 | 62 | const mode = ensureMode(options.mode); 63 | const filtered = tokens.filter((token) => mode(token, options)); 64 | 65 | const minTokens = options.minTokens ?? 50; 66 | if (filtered.length < minTokens) { 67 | return []; 68 | } 69 | 70 | return createTokensMaps(id, filtered, format, options); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scripts/test-wasm-resolution.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Test script to verify WASM path resolution 4 | * 5 | * This script tests that the TreeSitterParser can find WASM files 6 | * in various installation scenarios (local, global, npx). 7 | */ 8 | 9 | import { TreeSitterParser } from "../src/parsers/tree-sitter-parser.js"; 10 | 11 | console.log("🧪 Testing WASM Path Resolution\n"); 12 | console.log("Process info:"); 13 | console.log(` CWD: ${process.cwd()}`); 14 | console.log(` Node version: ${process.version}`); 15 | console.log(` Platform: ${process.platform}`); 16 | console.log(` HOME: ${process.env.HOME || process.env.USERPROFILE}`); 17 | console.log(""); 18 | 19 | async function testWasmResolution() { 20 | try { 21 | console.log("Initializing TreeSitterParser..."); 22 | const parser = new TreeSitterParser(); 23 | 24 | await parser.initialize(); 25 | 26 | console.log("\n✅ SUCCESS: TreeSitterParser initialized successfully!"); 27 | console.log(" All WASM files were resolved correctly."); 28 | 29 | // Test parsing a simple TypeScript file 30 | const testCode = ` 31 | function hello(name: string): string { 32 | return \`Hello, \${name}!\`; 33 | } 34 | 35 | class Greeter { 36 | constructor(private name: string) {} 37 | 38 | greet(): void { 39 | console.log(hello(this.name)); 40 | } 41 | } 42 | `; 43 | 44 | console.log("\n🔍 Testing parse functionality..."); 45 | const result = await parser.parse("test.ts", testCode, "test-hash-123"); 46 | 47 | console.log(`\n✅ Parse successful!`); 48 | console.log(` Entities found: ${result.entities.length}`); 49 | console.log(` Parse time: ${result.parseTimeMs}ms`); 50 | 51 | if (result.entities.length > 0) { 52 | console.log("\n Sample entities:"); 53 | result.entities.slice(0, 3).forEach((entity) => { 54 | console.log(` - ${entity.type}: ${entity.name}`); 55 | }); 56 | } 57 | 58 | console.log("\n🎉 All tests passed! WASM resolution is working correctly."); 59 | process.exit(0); 60 | } catch (error) { 61 | console.error("\n❌ FAILED: Could not initialize parser"); 62 | console.error(" Error:", error.message); 63 | 64 | if (error.message.includes("Cannot find WASM file")) { 65 | console.error("\n💡 This error indicates WASM files could not be found."); 66 | console.error(" The error message above shows all paths that were checked."); 67 | } 68 | 69 | process.exit(1); 70 | } 71 | } 72 | 73 | testWasmResolution(); 74 | -------------------------------------------------------------------------------- /tests/tools/get-entity-source.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, rmSync } from "node:fs"; 2 | import { join } from "node:path"; 3 | import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; 4 | import { IndexerAgent } from "../../src/agents/indexer-agent.js"; 5 | import { getGraphStorage, resetGraphStorage } from "../../src/storage/graph-storage-factory.js"; 6 | import { getSQLiteManager, resetSQLiteManager } from "../../src/storage/sqlite-manager.js"; 7 | import { getEntitySource } from "../../src/tools/get-entity-source.js"; 8 | import { AgentStatus } from "../../src/types/agent.js"; 9 | import type { ParsedEntity } from "../../src/types/parser.js"; 10 | 11 | const TEST_DB_PATH = "./data/test-tool-source.db"; 12 | const CLEANUP_PATHS = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]; 13 | 14 | function e(name: string, line: number): ParsedEntity { 15 | return { 16 | name, 17 | type: "function", 18 | location: { 19 | start: { line, column: 0, index: line * 10 }, 20 | end: { line: line + 1, column: 0, index: line * 10 + 5 }, 21 | }, 22 | } as any; 23 | } 24 | 25 | describe("getEntitySource", () => { 26 | let agent: IndexerAgent; 27 | 28 | beforeEach(async () => { 29 | for (const p of CLEANUP_PATHS) { 30 | if (existsSync(p)) rmSync(p); 31 | } 32 | resetGraphStorage(); 33 | resetSQLiteManager(); 34 | const sqlite = getSQLiteManager({ path: TEST_DB_PATH }); 35 | agent = new IndexerAgent(sqlite); 36 | await agent.initialize(); 37 | }); 38 | 39 | afterEach(async () => { 40 | if (agent && agent.status !== AgentStatus.SHUTDOWN) await agent.shutdown(); 41 | for (const p of CLEANUP_PATHS) { 42 | if (existsSync(p)) rmSync(p); 43 | } 44 | resetGraphStorage(); 45 | resetSQLiteManager(); 46 | }); 47 | 48 | it("returns a snippet with context lines", async () => { 49 | const filePath = join(process.cwd(), "tests", "fixtures", "entity-source.ts"); 50 | await agent.indexEntities([e("add", 1)], filePath); 51 | 52 | const storage = await getGraphStorage(getSQLiteManager({ path: TEST_DB_PATH })); 53 | const entity = (await storage.executeQuery({ type: "entity", filters: { name: /^add$/i }, limit: 1 })).entities[0]!; 54 | 55 | const result = await getEntitySource({ storage, entity, filePath, contextLines: 1, maxBytes: 10000 }); 56 | expect(result.snippet).toContain("export function add"); 57 | expect(result.snippetRange.startLine).toBeLessThanOrEqual(result.entityRange.startLine); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/semantic/providers/http-embedding-helpers.ts: -------------------------------------------------------------------------------- 1 | import type { EmbedOptions, ProviderLogger } from "./base.js"; 2 | import type { HttpEngine, RequestConfig } from "./http-engine.js"; 3 | 4 | function chunkArray(items: T[], size: number): T[][] { 5 | const safeSize = Math.max(1, size); 6 | const out: T[][] = []; 7 | for (let i = 0; i < items.length; i += safeSize) { 8 | out.push(items.slice(i, i + safeSize)); 9 | } 10 | return out; 11 | } 12 | 13 | export async function embedSingleWithEngine( 14 | engine: HttpEngine, 15 | request: RequestConfig, 16 | text: string, 17 | parseSingle: (json: any) => Float32Array, 18 | opts?: EmbedOptions, 19 | ): Promise { 20 | return engine.callSingle(request, text, parseSingle, { signal: opts?.signal }); 21 | } 22 | 23 | export async function embedBatchWithEngine( 24 | engine: HttpEngine, 25 | request: RequestConfig, 26 | texts: string[], 27 | parseBatch: (json: any) => Float32Array[], 28 | parseSingle: (json: any) => Float32Array, 29 | maxBatchSize: number | undefined, 30 | opts?: EmbedOptions, 31 | ): Promise { 32 | const size = Math.max(1, maxBatchSize ?? texts.length); 33 | 34 | if (size < texts.length) { 35 | const parts = await Promise.all( 36 | chunkArray(texts, size).map((c) => engine.callSingle(request, c, parseBatch, { signal: opts?.signal })), 37 | ); 38 | return parts.flat(); 39 | } 40 | 41 | try { 42 | return await engine.callSingle(request, texts, parseBatch, { signal: opts?.signal }); 43 | } catch { 44 | return engine.callBatch(request, texts, parseSingle, { signal: opts?.signal }); 45 | } 46 | } 47 | 48 | export function createHttpEmbeddingMethods(params: { 49 | engine: HttpEngine; 50 | request: RequestConfig; 51 | parseSingle: (json: any) => Float32Array; 52 | parseBatch: (json: any) => Float32Array[]; 53 | maxBatchSize?: number; 54 | logger?: ProviderLogger; 55 | }) { 56 | const { engine, request, parseSingle, parseBatch, maxBatchSize, logger } = params; 57 | 58 | return { 59 | embed(text: string, opts?: EmbedOptions): Promise { 60 | logger?.debug("embed()", { len: text?.length }, opts?.requestId); 61 | return embedSingleWithEngine(engine, request, text, parseSingle, opts); 62 | }, 63 | embedBatch(texts: string[], opts?: EmbedOptions): Promise { 64 | logger?.debug("embedBatch()", { count: texts.length }, opts?.requestId); 65 | return embedBatchWithEngine(engine, request, texts, parseBatch, parseSingle, maxBatchSize, opts); 66 | }, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/tools/resolve-entity.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from "node:path"; 2 | import type { GraphStorageImpl } from "../storage/graph-storage.js"; 3 | import type { Entity, EntityType } from "../types/storage.js"; 4 | 5 | export type ResolveEntityCandidate = { entity: Entity; score: number; reasons: string[] }; 6 | 7 | export async function resolveEntityCandidates(options: { 8 | storage: GraphStorageImpl; 9 | name: string; 10 | filePathHint?: string; 11 | entityTypes?: EntityType[]; 12 | limit: number; 13 | }): Promise { 14 | const { storage, name, filePathHint, entityTypes, limit } = options; 15 | const exactName = name.trim(); 16 | const candidates: Entity[] = []; 17 | 18 | const exactQuery = await storage.executeQuery({ 19 | type: "entity", 20 | filters: { 21 | ...(entityTypes ? { entityType: entityTypes } : {}), 22 | name: new RegExp(`^${escapeRegExp(exactName)}$`, "i"), 23 | }, 24 | limit: 50, 25 | }); 26 | candidates.push(...exactQuery.entities); 27 | 28 | if (candidates.length < limit) { 29 | const fuzzyQuery = await storage.executeQuery({ 30 | type: "entity", 31 | filters: { 32 | ...(entityTypes ? { entityType: entityTypes } : {}), 33 | name: new RegExp(escapeRegExp(exactName), "i"), 34 | }, 35 | limit: 200, 36 | }); 37 | for (const e of fuzzyQuery.entities) { 38 | if (!candidates.some((c) => c.id === e.id)) candidates.push(e); 39 | } 40 | } 41 | 42 | const hintDir = filePathHint ? dirname(filePathHint) : undefined; 43 | return candidates 44 | .map((e) => { 45 | const reasons: string[] = []; 46 | let score = 0; 47 | 48 | if (e.name.toLowerCase() === exactName.toLowerCase()) { 49 | score += 100; 50 | reasons.push("exact_name"); 51 | } else if (e.name.toLowerCase().includes(exactName.toLowerCase())) { 52 | score += 50; 53 | reasons.push("name_contains"); 54 | } 55 | 56 | if (filePathHint && e.filePath === filePathHint) { 57 | score += 60; 58 | reasons.push("file_hint_exact"); 59 | } else if (hintDir && e.filePath.startsWith(hintDir)) { 60 | score += 20; 61 | reasons.push("file_hint_dir"); 62 | } 63 | 64 | score += Math.max(0, 10 - Math.min(10, Math.floor((e.location?.start?.line ?? 0) / 1000))); 65 | 66 | return { entity: e, score, reasons }; 67 | }) 68 | .sort((a, b) => b.score - a.score || String(a.entity.filePath).localeCompare(String(b.entity.filePath))); 69 | } 70 | 71 | function escapeRegExp(input: string): string { 72 | return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 73 | } 74 | -------------------------------------------------------------------------------- /tests/tools/agent-metrics.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; 2 | import { BaseAgent } from "../../src/agents/base.js"; 3 | import { ConductorOrchestrator } from "../../src/agents/conductor-orchestrator.js"; 4 | import { knowledgeBus } from "../../src/core/knowledge-bus.js"; 5 | import { ResourceManager } from "../../src/core/resource-manager.js"; 6 | import { collectAgentMetrics } from "../../src/tools/agent-metrics.js"; 7 | import type { AgentMessage, AgentTask } from "../../src/types/agent.js"; 8 | import { AgentType } from "../../src/types/agent.js"; 9 | 10 | class DummyAgent extends BaseAgent { 11 | constructor() { 12 | super(AgentType.DEV, { 13 | maxConcurrency: 1, 14 | memoryLimit: 128, 15 | priority: 5, 16 | }); 17 | } 18 | 19 | protected async onInitialize(): Promise {} 20 | protected async onShutdown(): Promise {} 21 | 22 | protected canProcessTask(task: AgentTask): boolean { 23 | return task.type === "noop"; 24 | } 25 | 26 | protected async processTask(_task: AgentTask): Promise { 27 | return { ok: true }; 28 | } 29 | 30 | protected async handleMessage(_message: AgentMessage): Promise {} 31 | } 32 | 33 | describe("collectAgentMetrics", () => { 34 | let conductor: ConductorOrchestrator; 35 | let agent: DummyAgent; 36 | let resourceManager: ResourceManager; 37 | 38 | beforeEach(async () => { 39 | (knowledgeBus as any).knowledge?.clear?.(); 40 | (knowledgeBus as any).subscriptions?.clear?.(); 41 | 42 | conductor = new ConductorOrchestrator(); 43 | await conductor.initialize(); 44 | 45 | agent = new DummyAgent(); 46 | await agent.initialize(); 47 | conductor.register(agent); 48 | 49 | resourceManager = new ResourceManager({ 50 | maxMemoryMB: 256, 51 | maxCpuPercent: 80, 52 | maxConcurrentAgents: 2, 53 | maxTaskQueueSize: 10, 54 | }); 55 | }); 56 | 57 | afterEach(async () => { 58 | await agent.shutdown(); 59 | await conductor.shutdown(); 60 | }); 61 | 62 | it("captures conductor, agent, resource, and bus metrics", async () => { 63 | const snapshot = await collectAgentMetrics({ 64 | conductor, 65 | resourceManager, 66 | knowledgeBus, 67 | }); 68 | 69 | expect(snapshot.timestamp).toBeTruthy(); 70 | expect(snapshot.conductor.registeredAgents).toBeGreaterThanOrEqual(1); 71 | expect(snapshot.agents).toHaveLength(1); 72 | expect(snapshot.agents[0]?.id).toBe(agent.id); 73 | expect(snapshot.resources.constraints.maxConcurrentAgents).toBe(2); 74 | expect(snapshot.knowledgeBus.topicCount).toBe(0); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /RELEASE_NOTES_2.7.9.md: -------------------------------------------------------------------------------- 1 | # Release Notes — `@er77/code-graph-rag-mcp` v2.7.9 (2025-12-15) 2 | 3 | ## Summary 4 | 5 | v2.7.9 is a reliability release focused on making Codex MCP setup reliable across projects, reducing stdio handshake 6 | failures, and improving indexing defaults. 7 | 8 | ## What’s Changed 9 | 10 | - Stdio hardening: prevent stdout log pollution during MCP runs so strict clients can complete `initialize`. 11 | - Logging: mirror server logs to `/tmp/code-graph-rag-mcp/mcp-server-YYYY-MM-DD.log` for early-start debugging. 12 | - Indexing defaults: always exclude common build/tmp/vendor directories unless explicitly overridden. 13 | - Batched indexing: add `batch_index` (resumable, progress-returning batches) to avoid strict client tool-call timeouts on big repos. 14 | - Incremental indexing: `incremental:true` now reindexes only changed files and safely replaces per-file graph rows to avoid duplicates. 15 | - Vector DB location: default `database.path` now resolves to `~/.code-graph-rag/vectors.db` (YAML supports leading `~`). 16 | - Graph query fix: RegExp name filtering now resolves entities correctly (supports exact `^...$` and substring matches), 17 | unblocking tools like `list_entity_relationships` and `analyze_code_impact`. 18 | - Codex CLI setup: recommend a global MCP server entry (`codex mcp add ...`) that works from any project folder. 19 | 20 | ## Install / Upgrade 21 | 22 | - npm: `npm install -g @er77/code-graph-rag-mcp@2.7.9` 23 | - local artifact: `npm install -g ./er77-code-graph-rag-mcp-2.7.9.tgz` 24 | 25 | Node.js: `>=24` 26 | 27 | ## Codex CLI Configuration (Global) 28 | 29 | Recommended: 30 | 31 | - `codex mcp remove code-graph-rag` (optional cleanup) 32 | - `codex mcp add code-graph-rag -- code-graph-rag-mcp` 33 | 34 | Alternative (local dev build, no npm/npx): 35 | 36 | - `codex mcp remove code-graph-rag` (optional cleanup) 37 | - `codex mcp add code-graph-rag -- node /absolute/path/to/code-graph-rag-mcp/dist/index.js` 38 | 39 | ## Troubleshooting 40 | 41 | If Codex reports `handshaking with MCP server failed: connection closed: initialize response`: 42 | 43 | - Ensure the server writes JSON-RPC only to `stdout` (logs must go to `stderr`). 44 | - Ensure your configured `command`/`args` are valid (e.g., don’t use `node -y ...`). 45 | - Check server logs: 46 | - primary: `logs_llm/mcp-server-YYYY-MM-DD.log` (when writable) 47 | - global mirror: `/tmp/code-graph-rag-mcp/mcp-server-YYYY-MM-DD.log` (uses `os.tmpdir()`) 48 | 49 | If `clean_index` / `index` time out on large repos under strict clients (15s) and the transport closes: 50 | 51 | - Use `batch_index` with a small `maxFilesPerBatch` and keep calling it with the returned `sessionId` until `done:true`. 52 | -------------------------------------------------------------------------------- /src/semantic/providers/transformers-provider.ts: -------------------------------------------------------------------------------- 1 | import type { EmbeddingProvider, EmbedOptions, ProviderInfo, ProviderLogger } from "./base.js"; 2 | 3 | export interface TransformersOptions { 4 | model: string; //'Xenova/all-MiniLM-L6-v2' 5 | quantized?: boolean; 6 | localPath?: string; 7 | logger?: ProviderLogger; 8 | } 9 | 10 | export class TransformersProvider implements EmbeddingProvider { 11 | public info: ProviderInfo; 12 | private pipeline: any | null = null; 13 | private log?: ProviderLogger; 14 | 15 | constructor(private opts: TransformersOptions) { 16 | this.log = opts.logger; 17 | this.info = { 18 | name: "transformers", 19 | model: opts.model, 20 | supportsBatch: true, 21 | }; 22 | } 23 | 24 | async initialize(): Promise { 25 | this.log?.info("initialize", { 26 | model: this.opts.model, 27 | quantized: this.opts.quantized, 28 | localPath: this.opts.localPath, 29 | }); 30 | 31 | const mod: any = await import("@xenova/transformers"); 32 | const pipeFactory = mod.pipeline as (task: string, model: string, options?: any) => Promise; 33 | 34 | this.pipeline = await pipeFactory("feature-extraction", this.opts.model, { 35 | quantized: this.opts.quantized !== false, 36 | progress_callback: undefined, 37 | local_files_only: !!this.opts.localPath, 38 | }); 39 | 40 | const out = await this.pipeline("warm up", { pooling: "mean", normalize: true }); 41 | this.info.dimension = out?.data?.length ?? this.info.dimension; 42 | 43 | this.log?.info("initialized", { dimension: this.info.dimension }); 44 | } 45 | 46 | getDimension(): number | undefined { 47 | return this.info.dimension; 48 | } 49 | 50 | async embed(text: string, opts?: EmbedOptions): Promise { 51 | this.log?.debug("embed()", { len: text?.length }, opts?.requestId); 52 | 53 | if (!this.pipeline) await this.initialize(); 54 | const out = await this.pipeline?.(text, { pooling: "mean", normalize: true }); 55 | const arr = new Float32Array(out.data); 56 | this.info.dimension = this.info.dimension ?? arr.length; 57 | return arr; 58 | } 59 | 60 | async embedBatch(texts: string[], opts?: EmbedOptions): Promise { 61 | this.log?.debug("embedBatch()", { count: texts.length }, opts?.requestId); 62 | 63 | if (!this.pipeline) await this.initialize(); 64 | const outs = await Promise.all(texts.map((t) => this.pipeline?.(t, { pooling: "mean", normalize: true }))); 65 | return outs.map((o) => new Float32Array(o.data)); 66 | } 67 | 68 | async close(): Promise { 69 | try { 70 | (this.pipeline as any)?.dispose?.(); 71 | } catch {} 72 | this.pipeline = null; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/tools/list-entity-relationships-depth.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, rmSync } from "node:fs"; 2 | import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; 3 | import { IndexerAgent } from "../../src/agents/indexer-agent.js"; 4 | import { getGraphStorage, resetGraphStorage } from "../../src/storage/graph-storage-factory.js"; 5 | import { getSQLiteManager, resetSQLiteManager } from "../../src/storage/sqlite-manager.js"; 6 | import { listEntityRelationshipsTraversal } from "../../src/tools/list-entity-relationships.js"; 7 | import { AgentStatus } from "../../src/types/agent.js"; 8 | import type { ParsedEntity } from "../../src/types/parser.js"; 9 | 10 | const TEST_DB_PATH = "./data/test-tool-relationships.db"; 11 | const CLEANUP_PATHS = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]; 12 | 13 | function e(name: string, line: number): ParsedEntity { 14 | return { 15 | name, 16 | type: "function", 17 | location: { 18 | start: { line, column: 0, index: line * 10 }, 19 | end: { line: line + 1, column: 0, index: line * 10 + 5 }, 20 | }, 21 | } as any; 22 | } 23 | 24 | describe("listEntityRelationshipsTraversal depth", () => { 25 | let agent: IndexerAgent; 26 | 27 | beforeEach(async () => { 28 | for (const p of CLEANUP_PATHS) { 29 | if (existsSync(p)) rmSync(p); 30 | } 31 | resetGraphStorage(); 32 | resetSQLiteManager(); 33 | const sqlite = getSQLiteManager({ path: TEST_DB_PATH }); 34 | agent = new IndexerAgent(sqlite); 35 | await agent.initialize(); 36 | }); 37 | 38 | afterEach(async () => { 39 | if (agent && agent.status !== AgentStatus.SHUTDOWN) await agent.shutdown(); 40 | for (const p of CLEANUP_PATHS) { 41 | if (existsSync(p)) rmSync(p); 42 | } 43 | resetGraphStorage(); 44 | resetSQLiteManager(); 45 | }); 46 | 47 | it("returns more nodes at higher depth", async () => { 48 | const filePath = "/tmp/graph.ts"; 49 | await agent.indexEntities([e("A", 1), e("B", 10), e("C", 20)], filePath, [ 50 | { from: "A", to: "B", type: "calls", metadata: { line: 1 } }, 51 | { from: "B", to: "C", type: "calls", metadata: { line: 10 } }, 52 | ] as any); 53 | 54 | const storage = await getGraphStorage(getSQLiteManager({ path: TEST_DB_PATH })); 55 | const root = (await storage.executeQuery({ type: "entity", filters: { name: /^A$/i }, limit: 1 })).entities[0]!; 56 | 57 | const d1 = await listEntityRelationshipsTraversal(storage, root, { depth: 1 }); 58 | const d2 = await listEntityRelationshipsTraversal(storage, root, { depth: 2 }); 59 | 60 | expect(d1.nodes.size).toBeLessThan(d2.nodes.size); 61 | expect(Array.from(d2.nodes.values()).some((x) => x.name === "C")).toBe(true); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/tools/analyze-code-impact-depth.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, rmSync } from "node:fs"; 2 | import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; 3 | import { IndexerAgent } from "../../src/agents/indexer-agent.js"; 4 | import { getGraphStorage, resetGraphStorage } from "../../src/storage/graph-storage-factory.js"; 5 | import { getSQLiteManager, resetSQLiteManager } from "../../src/storage/sqlite-manager.js"; 6 | import { analyzeCodeImpactTraversal } from "../../src/tools/analyze-code-impact.js"; 7 | import { AgentStatus } from "../../src/types/agent.js"; 8 | import type { ParsedEntity } from "../../src/types/parser.js"; 9 | 10 | const TEST_DB_PATH = "./data/test-tool-impact.db"; 11 | const CLEANUP_PATHS = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]; 12 | 13 | function e(name: string, line: number): ParsedEntity { 14 | return { 15 | name, 16 | type: "function", 17 | location: { 18 | start: { line, column: 0, index: line * 10 }, 19 | end: { line: line + 1, column: 0, index: line * 10 + 5 }, 20 | }, 21 | } as any; 22 | } 23 | 24 | describe("analyzeCodeImpactTraversal depth", () => { 25 | let agent: IndexerAgent; 26 | 27 | beforeEach(async () => { 28 | for (const p of CLEANUP_PATHS) { 29 | if (existsSync(p)) rmSync(p); 30 | } 31 | resetGraphStorage(); 32 | resetSQLiteManager(); 33 | const sqlite = getSQLiteManager({ path: TEST_DB_PATH }); 34 | agent = new IndexerAgent(sqlite); 35 | await agent.initialize(); 36 | }); 37 | 38 | afterEach(async () => { 39 | if (agent && agent.status !== AgentStatus.SHUTDOWN) await agent.shutdown(); 40 | for (const p of CLEANUP_PATHS) { 41 | if (existsSync(p)) rmSync(p); 42 | } 43 | resetGraphStorage(); 44 | resetSQLiteManager(); 45 | }); 46 | 47 | it("honors depth for transitive dependents", async () => { 48 | const filePath = "/tmp/impact.ts"; 49 | await agent.indexEntities([e("A", 1), e("B", 10), e("C", 20)], filePath, [ 50 | { from: "B", to: "A", type: "calls", metadata: { line: 10 } }, 51 | { from: "C", to: "B", type: "calls", metadata: { line: 20 } }, 52 | ] as any); 53 | 54 | const storage = await getGraphStorage(getSQLiteManager({ path: TEST_DB_PATH })); 55 | const root = (await storage.executeQuery({ type: "entity", filters: { name: /^A$/i }, limit: 1 })).entities[0]!; 56 | 57 | const d1 = await analyzeCodeImpactTraversal(storage, root.id, 1); 58 | const d2 = await analyzeCodeImpactTraversal(storage, root.id, 2); 59 | 60 | expect(d1.directDependents.size).toBe(1); 61 | expect(d1.transitiveDependents.size).toBe(0); 62 | expect(d2.directDependents.size).toBe(1); 63 | expect(d2.transitiveDependents.size).toBe(1); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/types/agent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Core agent type definitions for the LiteRAG multi-agent architecture 3 | * Optimized for commodity hardware (4-core CPU, 8GB RAM) 4 | */ 5 | 6 | export enum AgentType { 7 | COORDINATOR = "coordinator", 8 | DEV = "dev", 9 | DORA = "dora", 10 | INDEXER = "indexer", 11 | PARSER = "parser", 12 | QUERY = "query", 13 | SEMANTIC = "semantic", 14 | } 15 | 16 | export enum AgentStatus { 17 | IDLE = "idle", 18 | BUSY = "busy", 19 | ERROR = "error", 20 | SHUTDOWN = "shutdown", 21 | } 22 | 23 | export interface AgentCapabilities { 24 | maxConcurrency: number; 25 | memoryLimit: number; // in MB 26 | cpuAffinity?: number[]; // CPU cores to bind to 27 | priority: number; // 0-10, higher is more important 28 | } 29 | 30 | export interface AgentMessage { 31 | id: string; 32 | from: string; 33 | to: string; 34 | type: string; 35 | payload: T; 36 | timestamp: number; 37 | correlationId?: string; 38 | } 39 | 40 | export interface AgentTask { 41 | id: string; 42 | type: string; 43 | priority: number; 44 | payload: unknown; 45 | createdAt: number; 46 | startedAt?: number; 47 | completedAt?: number; 48 | error?: Error; 49 | result?: unknown; 50 | } 51 | 52 | export interface Agent { 53 | id: string; 54 | type: AgentType; 55 | status: AgentStatus; 56 | capabilities: AgentCapabilities; 57 | 58 | // Lifecycle methods 59 | initialize(): Promise; 60 | shutdown(): Promise; 61 | 62 | // Task processing 63 | canHandle(task: AgentTask): boolean; 64 | process(task: AgentTask): Promise; 65 | 66 | // Communication 67 | send(message: AgentMessage): Promise; 68 | receive(message: AgentMessage): Promise; 69 | 70 | // Resource management 71 | getMemoryUsage(): number; 72 | getCpuUsage(): number; 73 | getTaskQueue(): AgentTask[]; 74 | } 75 | 76 | export interface AgentPool { 77 | agents: Map; 78 | 79 | register(agent: Agent): void; 80 | unregister(agentId: string): void; 81 | 82 | getAgent(id: string): Agent | undefined; 83 | getAgentsByType(type: AgentType): Agent[]; 84 | getAvailableAgent(type: AgentType): Agent | undefined; 85 | 86 | broadcast(message: AgentMessage): Promise; 87 | route(task: AgentTask): Promise; 88 | } 89 | 90 | export interface ResourceConstraints { 91 | maxMemoryMB: number; 92 | maxCpuPercent: number; 93 | maxConcurrentAgents: number; 94 | maxTaskQueueSize: number; 95 | } 96 | 97 | export interface AgentMetrics { 98 | agentId: string; 99 | tasksProcessed: number; 100 | tasksSucceeded: number; 101 | tasksFailed: number; 102 | averageProcessingTime: number; 103 | currentMemoryMB: number; 104 | currentCpuPercent: number; 105 | lastActivity: number; 106 | } 107 | -------------------------------------------------------------------------------- /src/semantic/providers/factory.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CloudRUProviderConfig, 3 | MemoryProviderConfig, 4 | OllamaProviderConfig, 5 | OpenAIProviderConfig, 6 | } from "../../types/semantic.js"; 7 | import { logger as appLogger } from "../../utils/logger.js"; 8 | import { makeProviderLogger } from "../../utils/provider-logger.js"; 9 | import type { EmbeddingProvider, ProviderKind } from "./base.js"; 10 | import { CloudRUProvider } from "./cloudru-provider.js"; 11 | import { MemoryProvider } from "./memory-provider.js"; 12 | import { OllamaProvider } from "./ollama-provider.js"; 13 | import { OpenAIProvider } from "./openai-provider.js"; 14 | import { TransformersProvider } from "./transformers-provider.js"; 15 | 16 | export interface ProviderFactoryOptions { 17 | provider: ProviderKind; 18 | modelName: string; 19 | transformers?: { quantized?: boolean; localPath?: string }; 20 | ollama?: OllamaProviderConfig; 21 | memory?: MemoryProviderConfig; 22 | openai?: OpenAIProviderConfig; 23 | cloudru?: CloudRUProviderConfig; 24 | } 25 | 26 | export function createProvider(opts: ProviderFactoryOptions): EmbeddingProvider { 27 | switch (opts.provider) { 28 | case "transformers": 29 | return new TransformersProvider({ 30 | model: opts.modelName, 31 | quantized: opts.transformers?.quantized, 32 | localPath: opts.transformers?.localPath, 33 | logger: makeProviderLogger(appLogger, "PROVIDER_TRANSFORMERS"), 34 | }); 35 | 36 | case "ollama": 37 | return new OllamaProvider({ 38 | model: opts.modelName, 39 | baseUrl: opts.ollama?.baseUrl, 40 | timeoutMs: opts.ollama?.timeoutMs, 41 | concurrency: opts.ollama?.concurrency, 42 | headers: opts.ollama?.headers, 43 | autoPull: opts.ollama?.autoPull, 44 | warmupText: opts.ollama?.warmupText, 45 | checkServer: opts.ollama?.checkServer, 46 | pullTimeoutMs: opts.ollama?.pullTimeoutMs, 47 | logger: makeProviderLogger(appLogger, "PROVIDER_OLLAMA"), 48 | }); 49 | 50 | case "openai": 51 | if (!opts.openai?.apiKey) throw new Error("OpenAI apiKey is required"); 52 | return new OpenAIProvider({ 53 | model: opts.modelName, 54 | apiKey: opts.openai.apiKey, 55 | baseUrl: opts.openai.baseUrl, 56 | timeoutMs: opts.openai.timeoutMs, 57 | concurrency: opts.openai.concurrency, 58 | dimensions: opts.openai.dimensions, 59 | maxBatchSize: opts.openai.maxBatchSize, 60 | logger: makeProviderLogger(appLogger, "PROVIDER_OPENAI"), 61 | }); 62 | 63 | case "cloudru": 64 | return new CloudRUProvider({ 65 | model: opts.modelName, 66 | apiKey: opts.cloudru?.apiKey, 67 | baseUrl: opts.cloudru?.baseUrl, 68 | timeoutMs: opts.cloudru?.timeoutMs, 69 | concurrency: opts.cloudru?.concurrency, 70 | maxBatchSize: opts.cloudru?.maxBatchSize, 71 | logger: makeProviderLogger(appLogger, "PROVIDER_CLOUDRU"), 72 | }); 73 | default: 74 | return new MemoryProvider({ dimension: opts.memory?.dimension }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/parsers/markdown-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { basename } from "node:path"; 2 | import type { EntityRelationship, ParsedEntity } from "../types/parser.js"; 3 | 4 | function computeLineStartIndices(text: string): number[] { 5 | const starts = [0]; 6 | for (let i = 0; i < text.length; i++) { 7 | if (text[i] === "\n") starts.push(i + 1); 8 | } 9 | return starts; 10 | } 11 | 12 | function locationForLine(lineStarts: number[], line: number, lineText: string) { 13 | const startIdx = lineStarts[Math.max(0, line - 1)] ?? 0; 14 | const endIdx = startIdx + lineText.length; 15 | return { 16 | start: { line, column: 0, index: startIdx }, 17 | end: { line, column: Math.max(0, lineText.length), index: endIdx }, 18 | }; 19 | } 20 | 21 | function slugify(input: string): string { 22 | return input 23 | .trim() 24 | .toLowerCase() 25 | .replace(/[^\p{L}\p{N}\s-]/gu, "") 26 | .replace(/\s+/g, "-") 27 | .replace(/-+/g, "-") 28 | .replace(/^-|-$/g, ""); 29 | } 30 | 31 | export class MarkdownAnalyzer { 32 | analyze(content: string, filePath: string): { entities: ParsedEntity[]; relationships: EntityRelationship[] } { 33 | const entities: ParsedEntity[] = []; 34 | const relationships: EntityRelationship[] = []; 35 | 36 | const lines = content.split(/\r?\n/); 37 | const lineStarts = computeLineStartIndices(content); 38 | 39 | const docId = `${filePath}:document`; 40 | entities.push({ 41 | id: docId, 42 | name: basename(filePath), 43 | type: "document", 44 | filePath, 45 | location: { 46 | start: { line: 1, column: 0, index: 0 }, 47 | end: { line: Math.max(1, lines.length), column: 0, index: content.length }, 48 | }, 49 | metadata: { language: "markdown" }, 50 | } as any); 51 | 52 | const headingStack: Array<{ id: string; level: number }> = []; 53 | 54 | for (let i = 0; i < lines.length; i++) { 55 | const lineNum = i + 1; 56 | const line = lines[i] ?? ""; 57 | const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line); 58 | if (!match) continue; 59 | 60 | const level = match[1]!.length; 61 | const title = match[2]!; 62 | const slug = slugify(title) || `heading-${lineNum}`; 63 | const headingId = `${filePath}:heading:${slug}:${lineNum}`; 64 | 65 | entities.push({ 66 | id: headingId, 67 | name: title, 68 | type: "heading", 69 | filePath, 70 | location: locationForLine(lineStarts, lineNum, line), 71 | metadata: { level, slug, language: "markdown" }, 72 | } as any); 73 | 74 | while (headingStack.length && headingStack[headingStack.length - 1]!.level >= level) { 75 | headingStack.pop(); 76 | } 77 | 78 | const parentId = headingStack.length ? headingStack[headingStack.length - 1]!.id : docId; 79 | relationships.push({ 80 | from: parentId, 81 | to: headingId, 82 | type: "contains", 83 | metadata: { line: lineNum, confidence: 1, isDirectRelation: true }, 84 | }); 85 | 86 | headingStack.push({ id: headingId, level }); 87 | } 88 | 89 | return { entities, relationships }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/tools/__tests__/lerna-graph-ingest.test.ts: -------------------------------------------------------------------------------- 1 | import { GraphStorageImpl } from "../../storage/graph-storage.js"; 2 | import { SQLiteManager } from "../../storage/sqlite-manager.js"; 3 | import { EntityType, RelationType } from "../../types/storage.js"; 4 | import { ingestLernaGraph, LERNA_PACKAGE_FILE_PREFIX } from "../lerna-graph-ingest.js"; 5 | 6 | describe("ingestLernaGraph", () => { 7 | let manager: SQLiteManager; 8 | let storage: GraphStorageImpl; 9 | 10 | beforeEach(async () => { 11 | manager = new SQLiteManager({ memory: true }); 12 | manager.initialize(); 13 | storage = new GraphStorageImpl(manager); 14 | await storage.initialize(); 15 | }); 16 | 17 | afterEach(() => { 18 | manager.close(); 19 | }); 20 | 21 | it("ingests packages and dependency relationships", async () => { 22 | const graph = { 23 | "pkg-a": ["pkg-b"], 24 | "pkg-b": ["pkg-c"], 25 | "pkg-c": [], 26 | }; 27 | 28 | const summary = await ingestLernaGraph(storage, graph); 29 | 30 | expect(summary.packageCount).toBe(3); 31 | expect(summary.relationshipCount).toBe(2); 32 | expect(summary.skippedPackages).toBe(0); 33 | expect(summary.removedPackages).toBe(0); 34 | 35 | const packagePaths = Object.keys(graph).map((name) => `${LERNA_PACKAGE_FILE_PREFIX}${name}`); 36 | const entityQuery = await storage.executeQuery({ 37 | type: "entity", 38 | filters: { filePath: packagePaths }, 39 | limit: packagePaths.length + 5, 40 | }); 41 | 42 | expect(entityQuery.entities).toHaveLength(3); 43 | for (const entity of entityQuery.entities) { 44 | expect(entity.type).toBe(EntityType.PACKAGE); 45 | expect(entity.metadata.source).toBe("lerna"); 46 | } 47 | 48 | const relationshipQuery = await storage.executeQuery({ 49 | type: "relationship", 50 | filters: { relationshipType: RelationType.DEPENDS_ON }, 51 | limit: 10, 52 | }); 53 | 54 | expect(relationshipQuery.relationships).toHaveLength(2); 55 | expect( 56 | relationshipQuery.relationships.every((rel) => rel.type === RelationType.DEPENDS_ON && rel.metadata?.context), 57 | ).toBe(true); 58 | }); 59 | it("removes stale package entities when packages disappear", async () => { 60 | const initial = { 61 | "pkg-old": ["pkg-keep"], 62 | "pkg-keep": [], 63 | }; 64 | await ingestLernaGraph(storage, initial); 65 | 66 | const updated = { 67 | "pkg-keep": [], 68 | }; 69 | const summary = await ingestLernaGraph(storage, updated); 70 | 71 | expect(summary.packageCount).toBe(1); 72 | expect(summary.removedPackages).toBe(1); 73 | 74 | const entityQuery = await storage.executeQuery({ 75 | type: "entity", 76 | filters: { entityType: EntityType.PACKAGE }, 77 | limit: 10, 78 | }); 79 | expect(entityQuery.entities).toHaveLength(1); 80 | expect(entityQuery.entities[0]?.name).toBe("pkg-keep"); 81 | 82 | const relationshipQuery = await storage.executeQuery({ 83 | type: "relationship", 84 | filters: { relationshipType: RelationType.DEPENDS_ON }, 85 | limit: 10, 86 | }); 87 | expect(relationshipQuery.relationships).toHaveLength(0); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/test-fixtures/go/sample.go: -------------------------------------------------------------------------------- 1 | // Sample Go code for testing analyzer 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Constants 10 | const ( 11 | Version = "1.0.0" 12 | MaxConnections = 100 13 | ) 14 | 15 | // User represents a system user 16 | type User struct { 17 | ID int 18 | Name string 19 | Email string 20 | IsActive bool 21 | } 22 | 23 | // UserService interface for user operations 24 | type UserService interface { 25 | GetUser(id int) (*User, error) 26 | CreateUser(user User) error 27 | UpdateUser(user User) error 28 | DeleteUser(id int) error 29 | } 30 | 31 | // UserServiceImpl implements UserService 32 | type UserServiceImpl struct { 33 | users map[int]*User 34 | } 35 | 36 | // NewUserService creates a new user service 37 | func NewUserService() UserService { 38 | return &UserServiceImpl{ 39 | users: make(map[int]*User), 40 | } 41 | } 42 | 43 | // GetUser retrieves a user by ID 44 | func (s *UserServiceImpl) GetUser(id int) (*User, error) { 45 | user, exists := s.users[id] 46 | if !exists { 47 | return nil, fmt.Errorf("user not found: %d", id) 48 | } 49 | return user, nil 50 | } 51 | 52 | // CreateUser creates a new user 53 | func (s *UserServiceImpl) CreateUser(user User) error { 54 | if _, exists := s.users[user.ID]; exists { 55 | return fmt.Errorf("user already exists: %d", user.ID) 56 | } 57 | s.users[user.ID] = &user 58 | return nil 59 | } 60 | 61 | // UpdateUser updates an existing user 62 | func (s *UserServiceImpl) UpdateUser(user User) error { 63 | if _, exists := s.users[user.ID]; !exists { 64 | return fmt.Errorf("user not found: %d", user.ID) 65 | } 66 | s.users[user.ID] = &user 67 | return nil 68 | } 69 | 70 | // DeleteUser deletes a user by ID 71 | func (s *UserServiceImpl) DeleteUser(id int) error { 72 | delete(s.users, id) 73 | return nil 74 | } 75 | 76 | // ProcessUsers processes a list of users concurrently 77 | func ProcessUsers(users []User) { 78 | ch := make(chan User, len(users)) 79 | 80 | // Start goroutines to process users 81 | for _, user := range users { 82 | go func(u User) { 83 | // Process user 84 | u.Name = strings.ToUpper(u.Name) 85 | ch <- u 86 | }(user) 87 | } 88 | 89 | // Collect results 90 | for i := 0; i < len(users); i++ { 91 | processed := <-ch 92 | fmt.Printf("Processed user: %s\n", processed.Name) 93 | } 94 | } 95 | 96 | // Embedded type example 97 | type Admin struct { 98 | User // Embedding User struct 99 | Permissions []string 100 | } 101 | 102 | func main() { 103 | service := NewUserService() 104 | 105 | user := User{ 106 | ID: 1, 107 | Name: "John Doe", 108 | Email: "john@example.com", 109 | IsActive: true, 110 | } 111 | 112 | if err := service.CreateUser(user); err != nil { 113 | fmt.Printf("Error creating user: %v\n", err) 114 | return 115 | } 116 | 117 | retrievedUser, err := service.GetUser(1) 118 | if err != nil { 119 | fmt.Printf("Error getting user: %v\n", err) 120 | return 121 | } 122 | 123 | fmt.Printf("User: %+v\n", retrievedUser) 124 | 125 | // Test goroutines 126 | users := []User{user} 127 | ProcessUsers(users) 128 | } -------------------------------------------------------------------------------- /src/semantic/providers/openai-provider.ts: -------------------------------------------------------------------------------- 1 | import type { EmbeddingProvider, EmbedOptions, ProviderInfo, ProviderLogger } from "./base.js"; 2 | import { createHttpEmbeddingMethods } from "./http-embedding-helpers.js"; 3 | import { HttpEngine } from "./http-engine.js"; 4 | 5 | export interface OpenAIOptions { 6 | baseUrl?: string; 7 | apiKey: string; 8 | model: string; 9 | timeoutMs?: number; 10 | concurrency?: number; 11 | dimensions?: number; 12 | maxBatchSize?: number; 13 | logger?: ProviderLogger; 14 | } 15 | 16 | export class OpenAIProvider implements EmbeddingProvider { 17 | public info: ProviderInfo; 18 | private engine: HttpEngine; 19 | private opts: OpenAIOptions; 20 | private log?: ProviderLogger; 21 | private embedMethods: ReturnType; 22 | 23 | constructor(opts: OpenAIOptions) { 24 | this.opts = { baseUrl: "https://api.openai.com", ...opts }; 25 | this.log = opts.logger; 26 | 27 | this.info = { 28 | name: "openai", 29 | model: opts.model, 30 | supportsBatch: true, 31 | maxBatchSize: opts.maxBatchSize, 32 | }; 33 | 34 | this.engine = new HttpEngine({ 35 | baseUrl: this.opts.baseUrl!, 36 | timeoutMs: this.opts.timeoutMs ?? 10000, 37 | concurrency: this.opts.concurrency ?? 4, 38 | defaultHeaders: { 39 | "Content-Type": "application/json", 40 | Authorization: `Bearer ${this.opts.apiKey}`, 41 | }, 42 | }); 43 | 44 | this.embedMethods = createHttpEmbeddingMethods({ 45 | engine: this.engine, 46 | request: { path: "/v1/embeddings", buildBody: this.buildBody }, 47 | parseSingle: this.parseSingle, 48 | parseBatch: this.parseBatch, 49 | maxBatchSize: this.info.maxBatchSize, 50 | logger: this.log, 51 | }); 52 | } 53 | 54 | async initialize(): Promise {} 55 | 56 | getDimension(): number | undefined { 57 | return this.info.dimension; 58 | } 59 | 60 | private buildBody = (input: string | string[]) => { 61 | const body: any = { model: this.info.model, input }; 62 | if (this.opts.dimensions) body.dimensions = this.opts.dimensions; 63 | return body; 64 | }; 65 | 66 | private parseSingle = (json: any): Float32Array => { 67 | if (!json || !Array.isArray(json.data) || !Array.isArray(json.data[0]?.embedding)) { 68 | throw new Error("OpenAI invalid embedding response"); 69 | } 70 | const arr = new Float32Array(json.data[0].embedding); 71 | this.info.dimension = this.info.dimension ?? arr.length; 72 | return arr; 73 | }; 74 | 75 | private parseBatch = (json: any): Float32Array[] => { 76 | if (!json || !Array.isArray(json.data)) throw new Error("OpenAI invalid batch response"); 77 | const out = json.data.map((d: any) => new Float32Array(d.embedding)); 78 | if (!this.info.dimension && out[0]) this.info.dimension = out[0].length; 79 | return out; 80 | }; 81 | 82 | async embed(text: string, opts?: EmbedOptions): Promise { 83 | return this.embedMethods.embed(text, opts); 84 | } 85 | 86 | async embedBatch(texts: string[], opts?: EmbedOptions): Promise { 87 | return this.embedMethods.embedBatch(texts, opts); 88 | } 89 | 90 | async close(): Promise {} 91 | } 92 | -------------------------------------------------------------------------------- /generate_microsoft_report.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | PLUGIN_DIR = "excel_plugins" 5 | 6 | def parse_markdown(file_path): 7 | with open(file_path, 'r', encoding='utf-8') as f: 8 | content = f.read() 9 | 10 | # Extract Title 11 | title_match = re.search(r'^# (.+)$', content, re.MULTILINE) 12 | title = title_match.group(1) if title_match else "Unknown" 13 | 14 | # Extract Metadata 15 | votes_match = re.search(r'\*\*Rating:\*\* .*?\((\d+) votes\)', content) 16 | rating_match = re.search(r'\*\*Rating:\*\* ([\d\.]+)', content) 17 | publisher_match = re.search(r'\*\*Publisher:\*\* (.*)', content) 18 | 19 | votes = int(votes_match.group(1)) if votes_match else 0 20 | rating = float(rating_match.group(1)) if rating_match else 0.0 21 | publisher = publisher_match.group(1).strip() if publisher_match else "Unknown" 22 | 23 | # Extract Description 24 | desc_start = content.find("## Description") 25 | description = "" 26 | if desc_start != -1: 27 | raw_desc = content[desc_start + 14:].strip() 28 | lines = [line for line in raw_desc.split('\n') if not line.startswith("**Status:**") and not line.startswith("**Download Link:**")] 29 | description = "\n".join(lines).strip() 30 | 31 | return { 32 | "title": title, 33 | "publisher": publisher, 34 | "filename": os.path.basename(file_path), 35 | "votes": votes, 36 | "rating": rating, 37 | "description": description, 38 | "score": votes * rating 39 | } 40 | 41 | def main(): 42 | plugins = [] 43 | 44 | # Load all plugins 45 | files = [f for f in os.listdir(PLUGIN_DIR) if f.endswith('.md')] 46 | for file in files: 47 | path = os.path.join(PLUGIN_DIR, file) 48 | try: 49 | plugins.append(parse_markdown(path)) 50 | except Exception: 51 | continue 52 | 53 | microsoft_plugins = [] 54 | for p in plugins: 55 | if "microsoft" in p["publisher"].lower(): 56 | microsoft_plugins.append(p) 57 | 58 | # Sort by Score 59 | microsoft_plugins.sort(key=lambda x: x["score"], reverse=True) 60 | 61 | report_lines = [] 62 | report_lines.append("# Microsoft-Published Excel Plugins Report\n") 63 | report_lines.append("This report lists all Excel plugins published by Microsoft, ranked by 'Impact Score' (Rating × Votes).\n\n") 64 | 65 | if not microsoft_plugins: 66 | report_lines.append("No plugins found published by Microsoft.\n") 67 | else: 68 | report_lines.append("| Rank | Name | Score | Rating | Votes | Description Summary |") 69 | report_lines.append("|---|---|---|---|---|---|") 70 | for i, p in enumerate(microsoft_plugins): 71 | summary = p["description"][:150].replace('\n', ' ') + "..." if len(p["description"]) > 150 else p["description"] 72 | report_lines.append(f"| {i+1} | {p['title']} | {p['score']:.1f} | {p['rating']} | {p['votes']} | {summary} |") 73 | 74 | with open("MICROSOFT_PLUGINS_REPORT.md", "w", encoding='utf-8') as f: 75 | f.write("\n".join(report_lines)) 76 | 77 | print("Report generated: MICROSOFT_PLUGINS_REPORT.md") 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /src/vendor/jscpd/tokenizer/tokens-map.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "node:crypto"; 2 | 3 | import type { IMapFrame, IOptions, IToken, ITokensMap } from "../core"; 4 | 5 | const TOKEN_HASH_LENGTH = 20; 6 | 7 | const defaultHash = (value: string): string => 8 | createHash("md5").update(value).digest("hex").substring(0, TOKEN_HASH_LENGTH); 9 | 10 | export class TokensMap implements ITokensMap, Iterator, Iterable { 11 | private position = 0; 12 | private readonly tokenHashes: string[]; 13 | private readonly minTokens: number; 14 | 15 | constructor( 16 | private readonly id: string, 17 | private readonly tokens: IToken[], 18 | private readonly format: string, 19 | private readonly options: Partial, 20 | ) { 21 | const rawMinTokens = options.minTokens ?? 50; 22 | this.minTokens = rawMinTokens > 0 ? rawMinTokens : 1; 23 | const hashFn = options.hashFunction ?? defaultHash; 24 | this.tokenHashes = tokens.map((token) => hashFn(token.type + token.value)); 25 | } 26 | 27 | getTokensCount(): number { 28 | if (this.tokens.length === 0) return 0; 29 | const first = this.tokens[0]!; 30 | const last = this.tokens[this.tokens.length - 1]!; 31 | const firstLoc = first.loc?.start; 32 | const lastLoc = last.loc?.end; 33 | if (!firstLoc || !lastLoc) return 0; 34 | const startPos = firstLoc.position ?? 0; 35 | const endPos = lastLoc.position ?? startPos; 36 | return endPos - startPos; 37 | } 38 | 39 | getId(): string { 40 | return this.id; 41 | } 42 | 43 | getLinesCount(): number { 44 | if (this.tokens.length === 0) return 0; 45 | const first = this.tokens[0]!; 46 | const last = this.tokens[this.tokens.length - 1]!; 47 | const firstLoc = first.loc?.start; 48 | const lastLoc = last.loc?.end; 49 | if (!firstLoc || !lastLoc) return 0; 50 | return lastLoc.line - firstLoc.line; 51 | } 52 | 53 | getFormat(): string { 54 | return this.format; 55 | } 56 | 57 | [Symbol.iterator](): Iterator { 58 | return this; 59 | } 60 | 61 | next(): IteratorResult { 62 | if (this.tokens.length < this.minTokens || this.position > this.tokens.length - this.minTokens) { 63 | return { 64 | done: true, 65 | value: false, 66 | }; 67 | } 68 | 69 | if (this.position + this.minTokens - 1 >= this.tokens.length) { 70 | return { 71 | done: true, 72 | value: false, 73 | }; 74 | } 75 | 76 | const hashSlice = this.tokenHashes.slice(this.position, this.position + this.minTokens).join(""); 77 | 78 | const hashFn = this.options.hashFunction ?? defaultHash; 79 | const mapFrameId = hashFn(hashSlice).substring(0, TOKEN_HASH_LENGTH); 80 | 81 | const startToken = this.tokens[this.position]!; 82 | const endToken = this.tokens[this.position + this.minTokens - 1]!; 83 | this.position += 1; 84 | 85 | return { 86 | done: false, 87 | value: { 88 | id: mapFrameId, 89 | sourceId: this.id, 90 | start: startToken, 91 | end: endToken, 92 | }, 93 | }; 94 | } 95 | } 96 | 97 | export function createTokensMaps( 98 | id: string, 99 | tokens: IToken[], 100 | format: string, 101 | options: Partial, 102 | ): TokensMap[] { 103 | if (!tokens.length) return []; 104 | return [new TokensMap(id, tokens, format, options)]; 105 | } 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | CLAUDE.md 177 | /.claude 178 | /logs_llm 179 | vec0.so 180 | vectors.db 181 | vectors.db-shm 182 | vectors.db-wal 183 | ./node_modules 184 | ./dist 185 | ./tmp 186 | 187 | /.github 188 | /.codex 189 | /tmp 190 | /tmp 191 | /.memory_bank/logs_llm 192 | /.memory_bank 193 | /.memory_bank 194 | data/* 195 | vectors.db-shm 196 | vectors.db-wal 197 | data/query_cache.db-wal 198 | data/query_cache.db-shm 199 | *.db-shm 200 | *.db-wal 201 | *.db-shm 202 | *.db-wal 203 | vectors.db-shm 204 | vectors.db-wal 205 | vectors.db-shm 206 | /excel_plugins 207 | *.md 208 | -------------------------------------------------------------------------------- /RELEASE_NOTES_2.7.15.md: -------------------------------------------------------------------------------- 1 | # Release Notes (Consolidated) — `@er77/code-graph-rag-mcp` v2.7.9 → v2.7.18 2 | 3 | This file consolidates recent release notes into a single document for easier tracking. 4 | 5 | ## Install / Upgrade (latest) 6 | 7 | - npm: `npm install -g @er77/code-graph-rag-mcp@2.7.18` 8 | - local artifact: `npm install -g ./er77-code-graph-rag-mcp-2.7.18.tgz` (if present) 9 | 10 | Node.js: `>=24` 11 | 12 | --- 13 | 14 | ## v2.7.15 (2025-12-17) 15 | 16 | - Maintenance release (version bump + rebuild). 17 | 18 | ## v2.7.16 (2025-12-17) 19 | 20 | - Timeout defaults: raise MCP tool-call timeouts to 600s in code defaults and shipped YAML configs to avoid premature `clean_index` / `batch_index` failures on larger repos. 21 | 22 | ## v2.7.17 (2025-12-17) 23 | 24 | - Docs: refresh contributor guidelines (`AGENTS.md`) and update consolidated release notes. 25 | 26 | ## v2.7.18 (2025-12-17) 27 | 28 | - Indexing: stop excluding Markdown by default; `.md` / `.mdx` are now discoverable via `index`, `clean_index`, and `batch_index`. 29 | - Diagnostics: log file-discovery stats for `batch_index` sessions and reduce false-positive “Agent heartbeat stale” warnings when agents are idle. 30 | 31 | ## v2.7.14 (2025-12-17) 32 | 33 | - Indexing: include Markdown files (`.md`, `.mdx`) in the parser/index pipeline. 34 | - New Markdown parsing: emits `document` + `heading` entities and `contains` relationships for heading structure. 35 | 36 | ## v2.7.13 (2025-12-16) 37 | 38 | Agent-quality upgrade for MCP tool usability and reliability: 39 | 40 | - Tool guidance: rewritten tool descriptions with “Use when / Typical flow / Output” structure. 41 | - Response standardization: unified JSON envelope via `toolOk` / `toolFail` (including `agent_busy` failures). 42 | - Pagination: cursor + `pageSize` support for high-cardinality tools (`semantic_search`, `query`, `get_graph`). 43 | - Schema/behavior alignment: `depth` now honored by `list_entity_relationships` and `analyze_code_impact`. 44 | - New grounding tools: 45 | - `resolve_entity` for ranked disambiguation by name (+ `filePathHint` boosting). 46 | - `get_entity_source` for snippet extraction with context + truncation safeguards. 47 | - Query improvements: hybrid results include structural match annotations and paging metadata. 48 | - Tests: added/updated integration + utility tests to prevent schema/behavior drift. 49 | 50 | ## v2.7.12 (2025-12-15) 51 | 52 | - Dependencies: `onnxruntime-node` is now an optional peer dependency (avoids pulling deprecated `boolean@3.2.0`). 53 | 54 | ## v2.7.11 (2025-12-15) 55 | 56 | - Database isolation: default `database.path` is `./.code-graph-rag/vectors.db` (per-repo storage by default). 57 | - Index hygiene: `.code-graph-rag/**` is always excluded from indexing. 58 | 59 | ## v2.7.10 (2025-12-15) 60 | 61 | - sqlite-vec loading: load sqlite-vec via `sqlite-vec`’s `getLoadablePath()` first (global-install safe), with fallbacks. 62 | 63 | ## v2.7.9 (2025-12-15) 64 | 65 | Reliability release focused on stricter MCP clients and big repos: 66 | 67 | - Stdio hardening: prevent stdout log pollution during MCP runs so strict clients can complete `initialize`. 68 | - Logging: mirror server logs to `/tmp/code-graph-rag-mcp/mcp-server-YYYY-MM-DD.log` for early-start debugging. 69 | - Indexing defaults: exclude common build/tmp/vendor directories unless explicitly overridden. 70 | - Batched indexing: `batch_index` (resumable, progress-returning) to avoid strict client tool-call timeouts on big repos. 71 | - Incremental indexing: `incremental:true` reindexes only changed files and safely replaces per-file graph rows. 72 | - Vector DB location: YAML supports `~` and defaults were improved for predictable paths. 73 | - Graph query fix: RegExp name filtering supports exact `^...$` and substring matches (unblocks relationship tools). 74 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/rabin-karp.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from "node:events"; 2 | import type { ITokensMap } from "."; 3 | import type { 4 | IClone, 5 | ICloneValidator, 6 | IMapFrame, 7 | IOptions, 8 | IStore, 9 | ITokenLocation, 10 | IValidationResult, 11 | } from "./interfaces"; 12 | import { runCloneValidators } from "./validators"; 13 | 14 | export class RabinKarp { 15 | constructor( 16 | private readonly options: IOptions, 17 | private readonly eventEmitter: EventEmitter, 18 | private readonly cloneValidators: ICloneValidator[], 19 | ) {} 20 | 21 | public async run(tokenMap: ITokensMap, store: IStore): Promise { 22 | return new Promise((resolve) => { 23 | let mapFrameInStore: any; 24 | let clone: IClone | null = null; 25 | 26 | const clones: IClone[] = []; 27 | 28 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 29 | const loop = (): void => { 30 | const iteration = tokenMap.next(); 31 | 32 | const value = iteration.value; 33 | 34 | if (!value || typeof value === "boolean") { 35 | resolve(clones); 36 | return; 37 | } 38 | 39 | store 40 | .get(value.id) 41 | .then( 42 | (mapFrameFromStore: IMapFrame) => { 43 | mapFrameInStore = mapFrameFromStore; 44 | if (!clone) { 45 | clone = RabinKarp.createClone(tokenMap.getFormat(), value, mapFrameInStore); 46 | } 47 | }, 48 | () => { 49 | if (clone && this.validate(clone)) { 50 | clones.push(clone); 51 | } 52 | clone = null; 53 | if (value.id) { 54 | return store.set(value.id, value); 55 | } 56 | return undefined; 57 | }, 58 | ) 59 | .finally(() => { 60 | if (!iteration.done) { 61 | if (clone) { 62 | clone = RabinKarp.enlargeClone(clone, value, mapFrameInStore); 63 | } 64 | loop(); 65 | } else { 66 | resolve(clones); 67 | } 68 | }); 69 | }; 70 | loop(); 71 | }); 72 | } 73 | 74 | private validate(clone: IClone): boolean { 75 | const validation: IValidationResult = runCloneValidators(clone, this.options, this.cloneValidators); 76 | 77 | if (validation.status) { 78 | this.eventEmitter.emit("CLONE_FOUND", { clone }); 79 | } else { 80 | this.eventEmitter.emit("CLONE_SKIPPED", { clone, validation }); 81 | } 82 | return validation.status; 83 | } 84 | 85 | private static createClone(format: string, mapFrameA: IMapFrame, mapFrameB: IMapFrame): IClone { 86 | return { 87 | format, 88 | foundDate: Date.now(), 89 | duplicationA: { 90 | sourceId: mapFrameA.sourceId, 91 | start: mapFrameA?.start?.loc?.start as ITokenLocation, 92 | end: mapFrameA?.end?.loc?.end as ITokenLocation, 93 | range: [mapFrameA.start.range[0], mapFrameA.end.range[1]], 94 | }, 95 | duplicationB: { 96 | sourceId: mapFrameB.sourceId, 97 | start: mapFrameB?.start?.loc?.start as ITokenLocation, 98 | end: mapFrameB?.end?.loc?.end as ITokenLocation, 99 | range: [mapFrameB.start.range[0], mapFrameB.end.range[1]], 100 | }, 101 | }; 102 | } 103 | 104 | private static enlargeClone(clone: IClone, mapFrameA: IMapFrame, mapFrameB: IMapFrame): IClone { 105 | clone.duplicationA.range[1] = mapFrameA.end.range[1]; 106 | clone.duplicationA.end = mapFrameA?.end?.loc?.end as ITokenLocation; 107 | clone.duplicationB.range[1] = mapFrameB.end.range[1]; 108 | clone.duplicationB.end = mapFrameB?.end?.loc?.end as ITokenLocation; 109 | return clone; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/semantic/providers/http-engine.ts: -------------------------------------------------------------------------------- 1 | import pLimit from "p-limit"; 2 | 3 | export class HttpError extends Error { 4 | constructor( 5 | public status: number, 6 | public statusText: string, 7 | public body?: string, 8 | ) { 9 | super(`HTTP ${status} ${statusText}${body ? `: ${body.slice(0, 300)}` : ""}`); 10 | } 11 | } 12 | 13 | export interface HttpEngineOptions { 14 | baseUrl: string; 15 | timeoutMs?: number; 16 | concurrency?: number; 17 | maxRetries?: number; 18 | backoffMs?: number; 19 | defaultHeaders?: Record; 20 | } 21 | 22 | export interface RequestConfig { 23 | path: string; 24 | method?: "GET" | "POST" | "PUT" | "DELETE"; 25 | headers?: Record; 26 | buildBody?: (input: any) => TBody; 27 | } 28 | 29 | export class HttpEngine { 30 | private baseUrl: string; 31 | private timeoutMs: number; 32 | private maxRetries: number; 33 | private backoffMs: number; 34 | private defaultHeaders: Record; 35 | private limit: ReturnType; 36 | 37 | constructor(opts: HttpEngineOptions) { 38 | this.baseUrl = opts.baseUrl.replace(/\/$/, ""); 39 | this.timeoutMs = opts.timeoutMs ?? 10000; 40 | this.maxRetries = Math.max(0, opts.maxRetries ?? 2); 41 | this.backoffMs = opts.backoffMs ?? 200; 42 | this.defaultHeaders = opts.defaultHeaders ?? { "Content-Type": "application/json" }; 43 | this.limit = pLimit(Math.max(1, opts.concurrency ?? 4)); 44 | } 45 | 46 | private async fetchWithRetry(path: string, init: RequestInit): Promise { 47 | let attempt = 0; 48 | const url = `${this.baseUrl}${path}`; 49 | 50 | while (true) { 51 | const controller = new AbortController(); 52 | const id = setTimeout(() => controller.abort(), this.timeoutMs); 53 | 54 | try { 55 | const res = await fetch(url, { 56 | ...init, 57 | signal: init.signal ?? controller.signal, 58 | }); 59 | 60 | if (res.ok) return res; 61 | 62 | if ((res.status === 429 || (res.status >= 500 && res.status < 600)) && attempt < this.maxRetries) { 63 | attempt++; 64 | await new Promise((r) => setTimeout(r, this.backoffMs * attempt)); 65 | continue; 66 | } 67 | 68 | const body = await res.text().catch(() => ""); 69 | throw new HttpError(res.status, res.statusText, body); 70 | } catch (err) { 71 | if (attempt < this.maxRetries) { 72 | attempt++; 73 | await new Promise((r) => setTimeout(r, this.backoffMs * attempt)); 74 | continue; 75 | } 76 | throw err; 77 | } finally { 78 | clearTimeout(id); 79 | } 80 | } 81 | } 82 | 83 | async callSingle( 84 | config: RequestConfig, 85 | input: any, 86 | parser: (json: any) => TParsed, 87 | opts?: { signal?: AbortSignal }, 88 | ): Promise { 89 | return this.limit(async () => { 90 | const body = config.buildBody ? config.buildBody(input) : input; 91 | const headers = { ...this.defaultHeaders, ...(config.headers ?? {}) }; 92 | 93 | const init: RequestInit = { 94 | method: config.method ?? "POST", 95 | headers, 96 | body: body !== undefined ? JSON.stringify(body) : undefined, 97 | signal: opts?.signal, 98 | }; 99 | 100 | const res = await this.fetchWithRetry(config.path, init); 101 | const text = await res.text(); 102 | const json = text ? JSON.parse(text) : null; 103 | return parser(json); 104 | }); 105 | } 106 | 107 | async callBatch( 108 | config: RequestConfig, 109 | inputs: any[], 110 | parseSingle: (json: any) => TParsed, 111 | opts?: { signal?: AbortSignal }, 112 | ): Promise { 113 | return Promise.all(inputs.map((input) => this.callSingle(config, input, parseSingle, opts))); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/semantic/providers/cloudru-provider.ts: -------------------------------------------------------------------------------- 1 | import type { EmbeddingProvider, EmbedOptions, ProviderInfo, ProviderLogger } from "./base.js"; 2 | import { createHttpEmbeddingMethods } from "./http-embedding-helpers.js"; 3 | import { HttpEngine } from "./http-engine.js"; 4 | 5 | export interface CloudRUOptions { 6 | baseUrl?: string; 7 | apiKey?: string; 8 | model: string; 9 | timeoutMs?: number; 10 | concurrency?: number; 11 | maxBatchSize?: number; 12 | logger?: ProviderLogger; 13 | } 14 | 15 | export class CloudRUProvider implements EmbeddingProvider { 16 | public info: ProviderInfo; 17 | private engine: HttpEngine; 18 | private opts: CloudRUOptions; 19 | private log?: ProviderLogger; 20 | private embedMethods: ReturnType; 21 | 22 | constructor(opts: CloudRUOptions) { 23 | this.opts = { baseUrl: "https://foundation-models.api.cloud.ru", ...opts }; 24 | this.log = opts.logger; 25 | 26 | this.info = { 27 | name: "cloudru", 28 | model: opts.model, 29 | supportsBatch: true, 30 | maxBatchSize: opts.maxBatchSize, 31 | }; 32 | 33 | const headers: Record = { "Content-Type": "application/json" }; 34 | if (this.opts.apiKey) headers.Authorization = `Bearer ${this.opts.apiKey}`; 35 | 36 | this.engine = new HttpEngine({ 37 | baseUrl: this.opts.baseUrl ?? "https://foundation-models.api.cloud.ru", 38 | timeoutMs: this.opts.timeoutMs ?? 10000, 39 | concurrency: this.opts.concurrency ?? 4, 40 | defaultHeaders: headers, 41 | }); 42 | 43 | this.embedMethods = createHttpEmbeddingMethods({ 44 | engine: this.engine, 45 | request: { path: "/v1/embeddings", buildBody: this.buildBody }, 46 | parseSingle: this.parseSingle, 47 | parseBatch: this.parseBatch, 48 | maxBatchSize: this.info.maxBatchSize, 49 | logger: this.log, 50 | }); 51 | } 52 | 53 | async initialize(): Promise { 54 | this.log?.info("initialize", { 55 | model: this.info.model, 56 | baseUrl: this.opts.baseUrl, 57 | apiKey: !!this.opts.apiKey, 58 | }); 59 | } 60 | 61 | getDimension(): number | undefined { 62 | return this.info.dimension; 63 | } 64 | 65 | private buildBody = (input: string | string[]) => ({ model: this.info.model, input }); 66 | 67 | private parseSingle = (json: any): Float32Array => { 68 | if (Array.isArray(json?.data) && Array.isArray(json.data[0]?.embedding)) { 69 | const arr = new Float32Array(json.data[0].embedding); 70 | this.info.dimension = this.info.dimension ?? arr.length; 71 | return arr; 72 | } 73 | if (Array.isArray(json?.embedding)) { 74 | const arr = new Float32Array(json.embedding); 75 | this.info.dimension = this.info.dimension ?? arr.length; 76 | return arr; 77 | } 78 | if (json?.error) throw new Error(`CloudRU error: ${JSON.stringify(json.error)}`); 79 | throw new Error(`CloudRU invalid embedding response`); 80 | }; 81 | 82 | private parseBatch = (json: any): Float32Array[] => { 83 | if (Array.isArray(json?.data)) { 84 | const out = json.data.map((d: any) => new Float32Array(d.embedding)); 85 | if (!this.info.dimension && out[0]) this.info.dimension = out[0].length; 86 | return out; 87 | } 88 | if (Array.isArray(json?.embedding) && Array.isArray(json.embedding[0])) { 89 | const out = json.embedding.map((e: any) => new Float32Array(e)); 90 | if (!this.info.dimension && out[0]) this.info.dimension = out[0].length; 91 | return out; 92 | } 93 | throw new Error("CloudRU invalid batch embedding response"); 94 | }; 95 | 96 | async embed(text: string, opts?: EmbedOptions): Promise { 97 | return this.embedMethods.embed(text, opts); 98 | } 99 | 100 | async embedBatch(texts: string[], opts?: EmbedOptions): Promise { 101 | return this.embedMethods.embedBatch(texts, opts); 102 | } 103 | 104 | async close(): Promise {} 105 | } 106 | -------------------------------------------------------------------------------- /scripts/run-tests.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { spawn } from "node:child_process"; 3 | import { readdirSync } from "node:fs"; 4 | import { join } from "node:path"; 5 | 6 | const NPX = process.platform === "win32" ? "npx.cmd" : "npx"; 7 | 8 | function listTests(extraArgs) { 9 | const testsDir = join(process.cwd(), "tests"); 10 | const tests = []; 11 | 12 | const visit = (dir) => { 13 | for (const entry of readdirSync(dir, { withFileTypes: true })) { 14 | const fullPath = join(dir, entry.name); 15 | if (entry.isDirectory()) { 16 | visit(fullPath); 17 | continue; 18 | } 19 | if (!entry.isFile()) continue; 20 | if (fullPath.endsWith(".test.ts") || fullPath.endsWith(".spec.ts")) { 21 | tests.push(fullPath); 22 | } 23 | } 24 | }; 25 | 26 | visit(testsDir); 27 | tests.sort(); 28 | 29 | // Best-effort filtering to match common Jest CLI args. 30 | const positionalPatterns = extraArgs.filter((arg) => !arg.startsWith("-")); 31 | if (positionalPatterns.length) { 32 | return tests.filter((filePath) => positionalPatterns.some((pattern) => filePath.includes(pattern))); 33 | } 34 | 35 | const testPathPatternIndex = extraArgs.findIndex((arg) => arg === "--testPathPattern"); 36 | const testPathPatternArg = extraArgs.find((arg) => arg.startsWith("--testPathPattern=")); 37 | const testPathPattern = 38 | testPathPatternIndex >= 0 ? extraArgs[testPathPatternIndex + 1] : testPathPatternArg?.split("=", 2)[1]; 39 | 40 | if (testPathPattern) { 41 | try { 42 | const regex = new RegExp(testPathPattern); 43 | return tests.filter((filePath) => regex.test(filePath)); 44 | } catch { 45 | return tests.filter((filePath) => filePath.includes(testPathPattern)); 46 | } 47 | } 48 | 49 | return tests; 50 | } 51 | 52 | function runTestFile(testPath, extraArgs) { 53 | return new Promise((resolve) => { 54 | const args = ["jest", "--runInBand", ...extraArgs, testPath]; 55 | const child = spawn(NPX, args, { stdio: "inherit", env: process.env }); 56 | child.on("exit", (code, signal) => { 57 | resolve({ file: testPath, code: code ?? 0, signal: signal ?? null }); 58 | }); 59 | }); 60 | } 61 | 62 | async function main() { 63 | const passthroughArgs = process.argv.slice(2); 64 | 65 | const isWatchMode = passthroughArgs.some((arg) => arg === "--watch" || arg === "--watchAll"); 66 | if (isWatchMode) { 67 | const args = ["jest", "--runInBand", ...passthroughArgs]; 68 | const child = spawn(NPX, args, { stdio: "inherit", env: process.env }); 69 | child.on("exit", (code) => { 70 | process.exitCode = code === 0 ? 0 : 1; 71 | }); 72 | return; 73 | } 74 | 75 | const tests = listTests(passthroughArgs); 76 | if (!tests.length) return; 77 | 78 | const concurrency = Number(process.env.JEST_PER_FILE_CONCURRENCY || 1); 79 | console.log(`Running ${tests.length} test files in separate processes (concurrency=${concurrency})`); 80 | 81 | const queue = tests.slice(); 82 | let passed = 0; 83 | let failed = 0; 84 | const failures = []; 85 | 86 | const workers = Array.from({ length: concurrency }, async () => { 87 | while (queue.length) { 88 | const file = queue.shift(); 89 | if (!file) break; 90 | const result = await runTestFile(file, passthroughArgs); 91 | if (result.code === 0) { 92 | passed++; 93 | } else { 94 | failed++; 95 | const reason = result.signal ? `${file} (signal ${result.signal})` : file; 96 | failures.push(reason); 97 | } 98 | } 99 | }); 100 | 101 | await Promise.all(workers); 102 | 103 | if (failures.length) { 104 | process.exitCode = 1; 105 | } 106 | 107 | console.log("\nJest per-file summary:"); 108 | console.log(` Passed: ${passed}`); 109 | console.log(` Failed: ${failed}`); 110 | console.log(` Total: ${tests.length}`); 111 | if (failures.length) { 112 | console.log(" Failed files:"); 113 | for (const f of failures) console.log(` - ${f}`); 114 | } 115 | } 116 | 117 | main(); 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@er77/code-graph-rag-mcp", 3 | "version": "2.7.18", 4 | "description": "Multi-agent LiteRAG MCP server for advanced code graph analysis", 5 | "license": "MIT", 6 | "author": "er77", 7 | "homepage": "https://github.com/er77/code-graph-rag-mcp#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/er77/code-graph-rag-mcp.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/er77/code-graph-rag-mcp/issues" 14 | }, 15 | "keywords": [ 16 | "mcp", 17 | "model-context-protocol", 18 | "code-analysis", 19 | "graph", 20 | "rag", 21 | "llm", 22 | "agents", 23 | "literag", 24 | "typescript", 25 | "sqlite", 26 | "vector-search", 27 | "semantic-search", 28 | "claude", 29 | "tree-sitter" 30 | ], 31 | "type": "module", 32 | "main": "dist/index.js", 33 | "bin": { 34 | "code-graph-rag-mcp": "dist/index.js" 35 | }, 36 | "files": [ 37 | "dist", 38 | "README.md", 39 | "LICENSE", 40 | "PERFORMANCE_GUIDE.md", 41 | "SQLITE_VEC_INSTALLATION.md", 42 | "CLAUDE.md" 43 | ], 44 | "scripts": { 45 | "build": "tsup", 46 | "build:watch": "tsup --watch", 47 | "typecheck": "tsc --noEmit", 48 | "lint": "biome check .", 49 | "lint:fix": "biome check --write .", 50 | "format": "biome format --write .", 51 | "test": "cross-env NODE_OPTIONS=--experimental-vm-modules PARSER_DISABLE_CACHE=1 node scripts/run-tests.js", 52 | "test:quiet": "cross-env NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test node scripts/run-tests.js --silent", 53 | "test:verbose": "cross-env NODE_OPTIONS=--experimental-vm-modules node scripts/run-tests.js --verbose", 54 | "test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules node scripts/run-tests.js --watch", 55 | "test:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules node scripts/run-tests.js --coverage", 56 | "smoke": "node scripts/smoke-graph-health.js /home/er77/_work_fodler/baserow-develop", 57 | "smoke:clean": "node scripts/smoke-graph-health.js /home/er77/_work_fodler/baserow-develop --clean", 58 | "smoke:semantic": "node scripts/smoke-semantic.js /home/er77/_work_fodler/baserow-develop", 59 | "clean": "rm -rf dist", 60 | "prepublishOnly": "npm run build", 61 | "prepare": "simple-git-hooks" 62 | }, 63 | "simple-git-hooks": { 64 | "pre-commit": "npx lint-staged && npm run typecheck" 65 | }, 66 | "lint-staged": { 67 | "*.{ts,tsx,js,jsx,json,css,md,yml,yaml}": "biome check --write" 68 | }, 69 | "devDependencies": { 70 | "@biomejs/biome": "^2.2.4", 71 | "@commitlint/cli": "^20.1.0", 72 | "@commitlint/config-conventional": "^20.0.0", 73 | "@types/better-sqlite3": "^7.6.13", 74 | "@types/bun": "latest", 75 | "@types/jest": "^29.5.14", 76 | "@types/node": "^20.0.0", 77 | "cross-env": "^10.1.0", 78 | "jest": "^29.7.0", 79 | "lint-staged": "^16.2.4", 80 | "simple-git-hooks": "^2.13.1", 81 | "ts-jest": "^29.4.4", 82 | "tsup": "^8.3.6", 83 | "typescript": "^5.9.2" 84 | }, 85 | "dependencies": { 86 | "@er77/code-graph-rag-mcp": "^2.5.9", 87 | "@modelcontextprotocol/sdk": "^1.5.0", 88 | "better-sqlite3": "^11.10.0", 89 | "lru-cache": "^10.0.0", 90 | "nanoid": "^5.0.0", 91 | "sqlite-vec": "^0.1.6", 92 | "tree-sitter": "0.21.1", 93 | "tree-sitter-c": "0.23.2", 94 | "tree-sitter-c-sharp": "0.21.3", 95 | "tree-sitter-cpp": "0.23.4", 96 | "tree-sitter-go": "0.23.4", 97 | "tree-sitter-java": "0.23.5", 98 | "tree-sitter-javascript": "0.23.0", 99 | "tree-sitter-kotlin": "^0.3.8", 100 | "tree-sitter-python": "0.23.3", 101 | "tree-sitter-rust": "0.21.0", 102 | "tree-sitter-typescript": "0.23.2", 103 | "yaml": "^2.8.1", 104 | "zod": "^3.23.0", 105 | "zod-to-json-schema": "^3.23.0" 106 | }, 107 | "optionalDependencies": { 108 | "@xenova/transformers": "^2.17.2" 109 | }, 110 | "peerDependencies": { 111 | "onnxruntime-node": "^1.23.0" 112 | }, 113 | "peerDependenciesMeta": { 114 | "onnxruntime-node": { 115 | "optional": true 116 | } 117 | }, 118 | "engines": { 119 | "node": ">=24.0.0" 120 | }, 121 | "notes": {} 122 | } 123 | -------------------------------------------------------------------------------- /process_plugins.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import os 4 | import re 5 | import requests 6 | import time 7 | 8 | def sanitize_filename(name): 9 | # Replace invalid characters with underscore 10 | return re.sub(r'[\\/*?:"><|]', '_', name) 11 | 12 | def process_plugins(): 13 | with open('plugins.json', 'r', encoding='utf-8') as f: 14 | plugins = json.load(f) 15 | 16 | output_dir = 'excel_plugins' 17 | os.makedirs(output_dir, exist_ok=True) 18 | 19 | count = 0 20 | download_count = 0 21 | 22 | for plugin in plugins: 23 | try: 24 | votes = plugin.get('vote_count', 0) 25 | if votes is None: votes = 0 26 | 27 | rating = plugin.get('rating', 0) 28 | if rating is None: rating = 0.0 29 | name = plugin.get('name', 'Unknown') 30 | 31 | # Sanitize filename components 32 | safe_name = sanitize_filename(name) 33 | filename_base = f"{votes}_{rating}_{safe_name}" 34 | 35 | # Create MD file 36 | md_path = os.path.join(output_dir, f"{filename_base}.md") 37 | 38 | with open(md_path, 'w', encoding='utf-8') as md_file: 39 | md_file.write(f"# {name}\n\n") 40 | md_file.write(f"**Publisher:** {plugin.get('publisher')}\n") 41 | md_file.write(f"**Rating:** {rating} ({votes} votes)\n") 42 | md_file.write(f"**Categories:** {', '.join(plugin.get('categories', []))}\n") 43 | md_file.write(f"**Industries:** {', '.join(plugin.get('industries', []))}\n") 44 | md_file.write(f"**Marketplace URL:** {plugin.get('url')}\n\n") 45 | md_file.write("## Description\n\n") 46 | md_file.write(f"{plugin.get('description', 'No description available.')}\n\n") 47 | 48 | download_link = plugin.get('download_link') 49 | if download_link: 50 | md_file.write(f"**Download Link:** {download_link}\n") 51 | md_file.write("\n**Status:** Code/Package downloaded.\n") 52 | else: 53 | md_file.write("\n**Status:** No download link available.\n") 54 | 55 | # Download code if link exists 56 | download_link = plugin.get('download_link') 57 | if download_link: 58 | # Determine extension 59 | # Common: .pbiviz, .zip, .xml 60 | ext = ".zip" # Default 61 | if "." in download_link.split("/")[-1]: 62 | potential_ext = "." + download_link.split("/")[-1].split(".")[-1].split("?")[0] 63 | if len(potential_ext) < 10: # Sanity check 64 | ext = potential_ext 65 | 66 | archive_path = os.path.join(output_dir, f"{filename_base}{ext}") 67 | 68 | if not os.path.exists(archive_path): 69 | print(f"Downloading {name} to {archive_path}...") 70 | try: 71 | r = requests.get(download_link, stream=True, timeout=30) 72 | r.raise_for_status() 73 | with open(archive_path, 'wb') as f: 74 | for chunk in r.iter_content(chunk_size=8192): 75 | f.write(chunk) 76 | download_count += 1 77 | # Polite delay only if we actually downloaded 78 | time.sleep(0.5) 79 | except Exception as e: 80 | print(f"Failed to download {name}: {e}") 81 | with open(md_path, 'a') as md_file: 82 | md_file.write(f"\n**Download Error:** Failed to download file. {e}\n") 83 | else: 84 | print(f"File {archive_path} already exists. Skipping download.") 85 | 86 | count += 1 87 | 88 | except Exception as e: 89 | print(f"Error processing plugin: {e}") 90 | continue 91 | 92 | print(f"Processed {count} plugins.") 93 | print(f"Downloaded {download_count} archives.") 94 | 95 | if __name__ == "__main__": 96 | process_plugins() 97 | -------------------------------------------------------------------------------- /generate_publisher_report.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from collections import defaultdict 4 | 5 | PLUGIN_DIR = "excel_plugins" 6 | 7 | def parse_markdown(file_path): 8 | with open(file_path, 'r', encoding='utf-8') as f: 9 | content = f.read() 10 | 11 | # Extract Title 12 | title_match = re.search(r'^# (.+)$', content, re.MULTILINE) 13 | title = title_match.group(1) if title_match else "Unknown" 14 | 15 | # Extract Metadata 16 | votes_match = re.search(r'\*\*Rating:\*\* .*?\((\d+) votes\)', content) 17 | rating_match = re.search(r'\*\*Rating:\*\* ([\d\.]+)', content) 18 | publisher_match = re.search(r'\*\*Publisher:\*\* (.*)', content) 19 | 20 | votes = int(votes_match.group(1)) if votes_match else 0 21 | rating = float(rating_match.group(1)) if rating_match else 0.0 22 | publisher = publisher_match.group(1).strip() if publisher_match else "Unknown" 23 | 24 | # Extract Description 25 | desc_start = content.find("## Description") 26 | description = "" 27 | if desc_start != -1: 28 | raw_desc = content[desc_start + 14:].strip() 29 | lines = [line for line in raw_desc.split('\n') if not line.startswith("**Status:**") and not line.startswith("**Download Link:**")] 30 | description = "\n".join(lines).strip() 31 | 32 | return { 33 | "title": title, 34 | "publisher": publisher, 35 | "filename": os.path.basename(file_path), 36 | "votes": votes, 37 | "rating": rating, 38 | "description": description, 39 | "score": votes * rating 40 | } 41 | 42 | def main(): 43 | plugins = [] 44 | 45 | # Load all plugins 46 | files = [f for f in os.listdir(PLUGIN_DIR) if f.endswith('.md')] 47 | for file in files: 48 | path = os.path.join(PLUGIN_DIR, file) 49 | try: 50 | plugins.append(parse_markdown(path)) 51 | except Exception: 52 | continue 53 | 54 | # Group by Publisher 55 | publisher_stats = defaultdict(lambda: {"total_score": 0, "plugins": []}) 56 | 57 | for p in plugins: 58 | pub = p["publisher"] 59 | # Skip unknown or empty publishers if any 60 | if not pub or pub.lower() == "unknown": 61 | continue 62 | 63 | publisher_stats[pub]["total_score"] += p["score"] 64 | publisher_stats[pub]["plugins"].append(p) 65 | 66 | # Sort Publishers by Total Score 67 | sorted_publishers = sorted(publisher_stats.items(), key=lambda x: x[1]["total_score"], reverse=True) 68 | 69 | # Top 10 70 | top_10 = sorted_publishers[:10] 71 | 72 | report_lines = [] 73 | report_lines.append("# Top 10 Excel Plugin Publishers Report\n") 74 | report_lines.append("This report identifies the top 10 publishers based on the cumulative 'Impact Score' (Rating × Votes) of all their plugins.\n") 75 | 76 | for rank, (pub_name, data) in enumerate(top_10, 1): 77 | report_lines.append(f"## {rank}. {pub_name}") 78 | report_lines.append(f"**Total Impact Score:** {data['total_score']:.1f}") 79 | report_lines.append(f"**Total Plugins:** {len(data['plugins'])}\n") 80 | 81 | report_lines.append("| Plugin Name | Score | Rating | Votes | Summary |") 82 | report_lines.append("|---|---|---|---|---|") 83 | 84 | # Sort their plugins by score 85 | my_plugins = sorted(data['plugins'], key=lambda x: x["score"], reverse=True) 86 | 87 | # List top 5 plugins for brevity, or all if few 88 | for p in my_plugins[:10]: 89 | summary = p["description"][:100].replace('\n', ' ') + "..." if len(p["description"]) > 100 else p["description"] 90 | report_lines.append(f"| {p['title']} | {p['score']:.1f} | {p['rating']} | {p['votes']} | {summary} |") 91 | 92 | if len(my_plugins) > 10: 93 | report_lines.append(f"| *...and {len(my_plugins) - 10} more* | | | | |") 94 | 95 | report_lines.append("\n---\n") 96 | 97 | with open("TOP_PUBLISHERS_REPORT.md", "w", encoding='utf-8') as f: 98 | f.write("\n".join(report_lines)) 99 | 100 | print("Report generated: TOP_PUBLISHERS_REPORT.md") 101 | 102 | if __name__ == "__main__": 103 | main() 104 | -------------------------------------------------------------------------------- /examples/cpp-test-files/basic_classes.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | // Simple class 7 | class Rectangle { 8 | private: 9 | double width; 10 | double height; 11 | 12 | public: 13 | Rectangle(double w, double h) : width(w), height(h) {} 14 | 15 | virtual ~Rectangle() = default; 16 | 17 | double area() const { 18 | return width * height; 19 | } 20 | 21 | virtual double perimeter() const { 22 | return 2 * (width + height); 23 | } 24 | 25 | // Static member function 26 | static Rectangle createSquare(double side) { 27 | return Rectangle(side, side); 28 | } 29 | 30 | // Getters and setters 31 | double getWidth() const { return width; } 32 | double getHeight() const { return height; } 33 | 34 | void setWidth(double w) { width = w; } 35 | void setHeight(double h) { height = h; } 36 | }; 37 | 38 | // Inheritance example 39 | class Square : public Rectangle { 40 | public: 41 | Square(double side) : Rectangle(side, side) {} 42 | 43 | double perimeter() const override { 44 | return 4 * getWidth(); 45 | } 46 | 47 | void setSide(double side) { 48 | setWidth(side); 49 | setHeight(side); 50 | } 51 | }; 52 | 53 | // Abstract base class 54 | class Shape { 55 | public: 56 | virtual ~Shape() = default; 57 | virtual double area() const = 0; 58 | virtual double perimeter() const = 0; 59 | virtual void draw() const = 0; 60 | }; 61 | 62 | // Multiple inheritance 63 | class Drawable { 64 | public: 65 | virtual ~Drawable() = default; 66 | virtual void setColor(const std::string& color) = 0; 67 | virtual std::string getColor() const = 0; 68 | }; 69 | 70 | class ColoredRectangle : public Rectangle, public Drawable { 71 | private: 72 | std::string color; 73 | 74 | public: 75 | ColoredRectangle(double w, double h, const std::string& c) 76 | : Rectangle(w, h), color(c) {} 77 | 78 | void setColor(const std::string& c) override { 79 | color = c; 80 | } 81 | 82 | std::string getColor() const override { 83 | return color; 84 | } 85 | }; 86 | 87 | // Class with operator overloading 88 | class Vector2D { 89 | private: 90 | double x, y; 91 | 92 | public: 93 | Vector2D(double x = 0, double y = 0) : x(x), y(y) {} 94 | 95 | // Operator overloading 96 | Vector2D operator+(const Vector2D& other) const { 97 | return Vector2D(x + other.x, y + other.y); 98 | } 99 | 100 | Vector2D& operator+=(const Vector2D& other) { 101 | x += other.x; 102 | y += other.y; 103 | return *this; 104 | } 105 | 106 | bool operator==(const Vector2D& other) const { 107 | return x == other.x && y == other.y; 108 | } 109 | 110 | friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) { 111 | os << "(" << v.x << ", " << v.y << ")"; 112 | return os; 113 | } 114 | 115 | double magnitude() const { 116 | return std::sqrt(x * x + y * y); 117 | } 118 | }; 119 | 120 | // RAII example 121 | class FileHandler { 122 | private: 123 | std::FILE* file; 124 | std::string filename; 125 | 126 | public: 127 | explicit FileHandler(const std::string& fname) 128 | : filename(fname), file(std::fopen(fname.c_str(), "r")) { 129 | if (!file) { 130 | throw std::runtime_error("Could not open file: " + filename); 131 | } 132 | } 133 | 134 | ~FileHandler() { 135 | if (file) { 136 | std::fclose(file); 137 | } 138 | } 139 | 140 | // Delete copy constructor and assignment operator 141 | FileHandler(const FileHandler&) = delete; 142 | FileHandler& operator=(const FileHandler&) = delete; 143 | 144 | // Move constructor and assignment operator 145 | FileHandler(FileHandler&& other) noexcept 146 | : file(other.file), filename(std::move(other.filename)) { 147 | other.file = nullptr; 148 | } 149 | 150 | FileHandler& operator=(FileHandler&& other) noexcept { 151 | if (this != &other) { 152 | if (file) { 153 | std::fclose(file); 154 | } 155 | file = other.file; 156 | filename = std::move(other.filename); 157 | other.file = nullptr; 158 | } 159 | return *this; 160 | } 161 | 162 | bool isOpen() const { return file != nullptr; } 163 | std::FILE* get() const { return file; } 164 | }; -------------------------------------------------------------------------------- /tests/test-mcp-direct.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Direct test of MCP server tools 5 | * Uses the MCP SDK to communicate with the server 6 | */ 7 | 8 | import { spawn } from "node:child_process"; 9 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 10 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 11 | 12 | const TEST_DIR = 13 | process.env.TEST_DIR || 14 | process.env.TARGET_DIR || 15 | (process.env.PROJECT_DIR ? `${process.env.PROJECT_DIR}/../target-repo` : "/tmp/target-repo"); 16 | 17 | async function testMCPServer() { 18 | console.log("🧪 Starting MCP Protocol Tests\n"); 19 | 20 | // Start the MCP server as a subprocess 21 | const DIST_JS = process.env.DIST_JS || `${process.cwd()}/dist/index.js`; 22 | const serverProcess = spawn("node", [DIST_JS, TEST_DIR]); 23 | 24 | // Create MCP client transport 25 | const transport = new StdioClientTransport({ 26 | command: "node", 27 | args: [DIST_JS, TEST_DIR], 28 | }); 29 | 30 | // Create MCP client 31 | const client = new Client( 32 | { 33 | name: "test-client", 34 | version: "1.0.0", 35 | }, 36 | { 37 | capabilities: {}, 38 | }, 39 | ); 40 | 41 | try { 42 | // Connect to server 43 | console.log("📡 Connecting to MCP server..."); 44 | await client.connect(transport); 45 | console.log("✅ Connected!\n"); 46 | 47 | // List available tools 48 | console.log("📋 Listing available tools..."); 49 | const tools = await client.request({ method: "tools/list" }, { meta: {} }); 50 | console.log(`Found ${tools.tools.length} tools:\n`); 51 | 52 | for (const tool of tools.tools) { 53 | console.log(` • ${tool.name}: ${tool.description}`); 54 | } 55 | console.log(); 56 | 57 | // Test each tool category 58 | const tests = [ 59 | { 60 | name: "graph:analyze", 61 | args: { directory: TEST_DIR }, 62 | }, 63 | { 64 | name: "graph:index", 65 | args: { 66 | directory: TEST_DIR, 67 | incremental: true, 68 | batchMode: true, 69 | excludePatterns: ["node_modules/**", "*.test.js"], 70 | }, 71 | }, 72 | { 73 | name: "query:search", 74 | args: { query: "class", limit: 3 }, 75 | }, 76 | { 77 | name: "query:findEntity", 78 | args: { name: "BaserowApplication", type: "class" }, 79 | }, 80 | { 81 | name: "query:getRelationships", 82 | args: { entityId: "test-id", relationshipType: "imports" }, 83 | }, 84 | { 85 | name: "semantic:search", 86 | args: { query: "database models", limit: 3 }, 87 | }, 88 | { 89 | name: "semantic:explain", 90 | args: { 91 | code: "def calculate_total(items):\\n return sum(item.price for item in items)", 92 | language: "python", 93 | }, 94 | }, 95 | { 96 | name: "semantic:suggest", 97 | args: { 98 | code: "def add(a,b):\\n return a+b", 99 | language: "python", 100 | }, 101 | }, 102 | ]; 103 | 104 | // Run tests 105 | for (const test of tests) { 106 | console.log(`\n🔧 Testing: ${test.name}`); 107 | console.log(` Args: ${JSON.stringify(test.args, null, 2)}`); 108 | 109 | try { 110 | const result = await client.request( 111 | { 112 | method: "tools/call", 113 | params: { 114 | name: test.name, 115 | arguments: test.args, 116 | }, 117 | }, 118 | { meta: {} }, 119 | ); 120 | 121 | console.log(" ✅ Success!"); 122 | if (result.content && result.content.length > 0) { 123 | const content = result.content[0]; 124 | if (typeof content === "string") { 125 | console.log(` Result: ${content.substring(0, 200)}...`); 126 | } else if (content.text) { 127 | console.log(` Result: ${content.text.substring(0, 200)}...`); 128 | } 129 | } 130 | } catch (error) { 131 | console.log(` ❌ Error: ${error.message}`); 132 | } 133 | } 134 | 135 | console.log("\n✅ All tests completed!"); 136 | } catch (error) { 137 | console.error("❌ Test failed:", error); 138 | } finally { 139 | // Cleanup 140 | await client.close(); 141 | serverProcess.kill(); 142 | } 143 | } 144 | 145 | // Run tests 146 | testMCPServer().catch(console.error); 147 | -------------------------------------------------------------------------------- /scrape_marketplace.py: -------------------------------------------------------------------------------- 1 | 2 | import requests 3 | import json 4 | import re 5 | import time 6 | import sys 7 | 8 | # URLs to scrape 9 | 10 | all_plugins = [] 11 | urls = [] 12 | for i in range(1, 26): # Scrape pages 1 to 25 13 | urls.append(f"https://marketplace.microsoft.com/en-us/marketplace/apps?product=office%3Bexcel&page={i}") 14 | 15 | 16 | 17 | def fetch_and_parse(url): 18 | print(f"Fetching {url}...") 19 | try: 20 | response = requests.get(url) 21 | response.raise_for_status() 22 | content = response.text 23 | except Exception as e: 24 | print(f"Error fetching {url}: {e}") 25 | return 26 | 27 | # Extract JSON from window.__INITIAL_STATE__ 28 | match = re.search(r'window\.__INITIAL_STATE__\s*=', content) 29 | if match: 30 | start_search_index = match.end() 31 | start_index = content.find('{', start_search_index) 32 | 33 | if start_index != -1: 34 | # We need to find the end of the JSON object. 35 | # Simple brace counting is safer than regex for nested objects. 36 | brace_count = 0 37 | json_end_index = -1 38 | in_string = False 39 | escape = False 40 | 41 | for i in range(start_index, len(content)): 42 | char = content[i] 43 | 44 | if escape: 45 | escape = False 46 | continue 47 | 48 | if char == '\\': 49 | escape = True 50 | continue 51 | 52 | if char == '"': 53 | in_string = not in_string 54 | continue 55 | 56 | if not in_string: 57 | if char == '{': 58 | brace_count += 1 59 | elif char == '}': 60 | brace_count -= 1 61 | if brace_count == 0: 62 | json_end_index = i + 1 63 | break 64 | 65 | if json_end_index != -1: 66 | json_str = content[start_index:json_end_index] 67 | try: 68 | data = json.loads(json_str) 69 | if 'apps' in data and 'dataList' in data['apps']: 70 | apps_list = data['apps']['dataList'] 71 | print(f"Found {len(apps_list)} apps on page.") 72 | for app in apps_list: 73 | # Extract relevant fields 74 | plugin = { 75 | "name": app.get('title'), 76 | "description": app.get('shortDescription'), 77 | "rating": app.get('AverageRating'), 78 | "vote_count": app.get('NumberOfRatings'), 79 | "publisher": app.get('publisher'), 80 | "id": app.get('entityId'), 81 | "url": f"https://marketplace.microsoft.com/en-us/marketplace/apps/{app.get('entityId')}?tab=Overview", 82 | "categories": [c.get('longTitle') for c in app.get('categoriesDetails', [])] if app.get('categoriesDetails') else [], 83 | "industries": [i.get('longTitle') for i in app.get('industriesDetails', [])] if app.get('industriesDetails') else [], 84 | "icon_url": app.get('iconURL'), 85 | "download_link": app.get('downloadLink'), 86 | "price_model": app.get('pricingModel') # 1 might indicate Paid/Free 87 | } 88 | all_plugins.append(plugin) 89 | else: 90 | print("No apps data found in JSON.") 91 | except json.JSONDecodeError as e: 92 | print(f"Error decoding JSON on {url}: {e}") 93 | else: 94 | print("Could not find end of JSON object.") 95 | else: 96 | print("Could not find start of JSON object.") 97 | else: 98 | print("Could not find __INITIAL_STATE__ assignment.") 99 | 100 | for url in urls: 101 | fetch_and_parse(url) 102 | time.sleep(1) # Be polite 103 | 104 | # Save to file 105 | output_file = 'plugins.json' 106 | with open(output_file, 'w', encoding='utf-8') as f: 107 | json.dump(all_plugins, f, indent=2) 108 | 109 | print(f"Total plugins extracted: {len(all_plugins)}") 110 | print(f"Saved to {output_file}") 111 | -------------------------------------------------------------------------------- /src/tools/agent-metrics.ts: -------------------------------------------------------------------------------- 1 | import type { ConductorOrchestrator } from "../agents/conductor-orchestrator.js"; 2 | import type { KnowledgeBus } from "../core/knowledge-bus.js"; 3 | import type { ResourceManager } from "../core/resource-manager.js"; 4 | import type { Agent, AgentMetrics, AgentStatus, AgentType, ResourceConstraints } from "../types/agent.js"; 5 | 6 | interface AgentSummary { 7 | id: string; 8 | type: AgentType; 9 | status: AgentStatus; 10 | queueLength: number; 11 | memoryUsageMB: number; 12 | cpuUsagePercent: number; 13 | capabilities: { 14 | maxConcurrency: number; 15 | memoryLimitMB: number; 16 | priority: number; 17 | }; 18 | metrics: AgentMetrics; 19 | currentTaskType?: string; 20 | lastActivity: number; 21 | } 22 | 23 | interface ResourceSummary { 24 | throttled: boolean; 25 | constraints: ResourceConstraints; 26 | currentUsage?: ReturnType; 27 | } 28 | 29 | interface KnowledgeBusSummary { 30 | topicCount: number; 31 | entryCount: number; 32 | subscriptionCount: number; 33 | messageQueueSize: number; 34 | } 35 | 36 | export interface AgentMetricsSnapshot { 37 | timestamp: string; 38 | conductor: { 39 | registeredAgents: number; 40 | totalTasks: number; 41 | averageProcessingTime: number; 42 | overheadReduction: number; 43 | cacheHitRate: number; 44 | pendingTasks: number; 45 | approvalsPending: number; 46 | directImplementationAttempts: number; 47 | }; 48 | agents: AgentSummary[]; 49 | resources: ResourceSummary; 50 | knowledgeBus: KnowledgeBusSummary; 51 | } 52 | 53 | function normalizeAgent(agent: Agent): AgentSummary { 54 | const metrics = (agent as any).getMetrics 55 | ? (agent as any).getMetrics() 56 | : ({ 57 | agentId: agent.id, 58 | tasksProcessed: 0, 59 | tasksSucceeded: 0, 60 | tasksFailed: 0, 61 | averageProcessingTime: 0, 62 | currentMemoryMB: agent.getMemoryUsage(), 63 | currentCpuPercent: agent.getCpuUsage(), 64 | lastActivity: Date.now(), 65 | } as AgentMetrics); 66 | return { 67 | id: agent.id, 68 | type: agent.type, 69 | status: agent.status, 70 | queueLength: agent.getTaskQueue().length, 71 | memoryUsageMB: agent.getMemoryUsage(), 72 | cpuUsagePercent: agent.getCpuUsage(), 73 | capabilities: { 74 | maxConcurrency: agent.capabilities.maxConcurrency, 75 | memoryLimitMB: agent.capabilities.memoryLimit, 76 | priority: agent.capabilities.priority, 77 | }, 78 | metrics, 79 | currentTaskType: (agent as any).currentTask?.type, 80 | lastActivity: metrics.lastActivity, 81 | }; 82 | } 83 | 84 | export async function collectAgentMetrics(options: { 85 | conductor: ConductorOrchestrator; 86 | resourceManager: ResourceManager; 87 | knowledgeBus: KnowledgeBus; 88 | }): Promise { 89 | const { conductor, resourceManager, knowledgeBus } = options; 90 | 91 | const agentCollection = 92 | (conductor as any).agents instanceof Map ? (conductor as any).agents.values() : ([] as Agent[]); 93 | const agentMap: Agent[] = Array.from(agentCollection); 94 | const conductorMetrics = 95 | typeof (conductor as any).getPerformanceMetrics === "function" 96 | ? (conductor as any).getPerformanceMetrics() 97 | : { 98 | totalTasks: 0, 99 | avgProcessingTime: 0, 100 | overheadReduction: 0, 101 | cacheHitRate: 0, 102 | }; 103 | 104 | const resources: ResourceSummary = { 105 | throttled: resourceManager.isSystemThrottled(), 106 | constraints: resourceManager.getConstraints(), 107 | }; 108 | 109 | const usage = resourceManager.getCurrentUsage(); 110 | if (usage) { 111 | resources.currentUsage = usage; 112 | } 113 | 114 | const knowledgeStats = knowledgeBus.getStats(); 115 | 116 | return { 117 | timestamp: new Date().toISOString(), 118 | conductor: { 119 | registeredAgents: agentMap.length, 120 | totalTasks: conductorMetrics.totalTasks ?? 0, 121 | averageProcessingTime: conductorMetrics.avgProcessingTime ?? 0, 122 | overheadReduction: conductorMetrics.overheadReduction ?? 0, 123 | cacheHitRate: conductorMetrics.cacheHitRate ?? 0, 124 | pendingTasks: (conductor as any).pendingTasks?.size ?? 0, 125 | approvalsPending: (conductor as any).approvalRequired?.size ?? 0, 126 | directImplementationAttempts: (conductor as any).directImplementationAttempts ?? 0, 127 | }, 128 | agents: agentMap.map(normalizeAgent), 129 | resources, 130 | knowledgeBus: { 131 | topicCount: knowledgeStats.topicCount, 132 | entryCount: knowledgeStats.entryCount, 133 | subscriptionCount: knowledgeStats.subscriptionCount, 134 | messageQueueSize: knowledgeStats.messageQueueSize, 135 | }, 136 | }; 137 | } 138 | -------------------------------------------------------------------------------- /examples/cpp-test-files/math_utils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MATH_UTILS_HPP 2 | #define MATH_UTILS_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace MathUtils { 9 | // Constants 10 | constexpr double PI = 3.14159265358979323846; 11 | constexpr double E = 2.71828182845904523536; 12 | 13 | // Function declarations 14 | double deg_to_rad(double degrees); 15 | double rad_to_deg(double radians); 16 | 17 | // Template function declarations 18 | template 19 | T abs(const T& value); 20 | 21 | template 22 | T clamp(const T& value, const T& min, const T& max); 23 | 24 | // Class declarations 25 | template 26 | class Vector3D { 27 | private: 28 | T x, y, z; 29 | 30 | public: 31 | Vector3D(T x = T{}, T y = T{}, T z = T{}); 32 | 33 | // Getters 34 | T getX() const { return x; } 35 | T getY() const { return y; } 36 | T getZ() const { return z; } 37 | 38 | // Setters 39 | void setX(T x) { this->x = x; } 40 | void setY(T y) { this->y = y; } 41 | void setZ(T z) { this->z = z; } 42 | 43 | // Vector operations 44 | Vector3D operator+(const Vector3D& other) const; 45 | Vector3D operator-(const Vector3D& other) const; 46 | Vector3D operator*(T scalar) const; 47 | T dot(const Vector3D& other) const; 48 | Vector3D cross(const Vector3D& other) const; 49 | T magnitude() const; 50 | Vector3D normalized() const; 51 | }; 52 | 53 | // Specialized class for complex numbers 54 | class ComplexMath { 55 | public: 56 | using Complex = std::complex; 57 | 58 | static Complex add(const Complex& a, const Complex& b); 59 | static Complex multiply(const Complex& a, const Complex& b); 60 | static double magnitude(const Complex& c); 61 | static double phase(const Complex& c); 62 | static Complex conjugate(const Complex& c); 63 | }; 64 | 65 | // Statistical functions 66 | class Statistics { 67 | public: 68 | template 69 | static auto mean(Iterator begin, Iterator end) -> typename std::iterator_traits::value_type; 70 | 71 | template 72 | static auto variance(Iterator begin, Iterator end) -> typename std::iterator_traits::value_type; 73 | 74 | template 75 | static auto median(Container& data) -> typename Container::value_type; 76 | 77 | static double standard_deviation(const std::vector& data); 78 | static double correlation(const std::vector& x, const std::vector& y); 79 | }; 80 | 81 | // Matrix class template 82 | template 83 | class Matrix { 84 | private: 85 | T data[Rows][Cols]; 86 | 87 | public: 88 | Matrix(); 89 | Matrix(const T& value); 90 | Matrix(const Matrix& other); 91 | 92 | Matrix& operator=(const Matrix& other); 93 | 94 | T& operator()(size_t row, size_t col); 95 | const T& operator()(size_t row, size_t col) const; 96 | 97 | Matrix operator+(const Matrix& other) const; 98 | Matrix operator-(const Matrix& other) const; 99 | 100 | template 101 | Matrix operator*(const Matrix& other) const; 102 | 103 | Matrix operator*(const T& scalar) const; 104 | 105 | Matrix transpose() const; 106 | T determinant() const; // Only for square matrices 107 | Matrix inverse() const; // Only for square matrices 108 | 109 | constexpr size_t rows() const { return Rows; } 110 | constexpr size_t cols() const { return Cols; } 111 | }; 112 | 113 | // Type aliases for common matrix types 114 | using Matrix2d = Matrix; 115 | using Matrix3d = Matrix; 116 | using Matrix4d = Matrix; 117 | 118 | using Vector2d = Vector3D; 119 | using Vector3f = Vector3D; 120 | 121 | // Function templates implementation 122 | template 123 | inline T abs(const T& value) { 124 | return (value >= T{}) ? value : -value; 125 | } 126 | 127 | template 128 | inline T clamp(const T& value, const T& min, const T& max) { 129 | return (value < min) ? min : (value > max) ? max : value; 130 | } 131 | 132 | // Macro definitions 133 | #define SQUARE(x) ((x) * (x)) 134 | #define MAX(a, b) ((a) > (b) ? (a) : (b)) 135 | #define MIN(a, b) ((a) < (b) ? (a) : (b)) 136 | 137 | } // namespace MathUtils 138 | 139 | #endif // MATH_UTILS_HPP -------------------------------------------------------------------------------- /classify_plugins.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import math 4 | from collections import defaultdict 5 | 6 | # Configuration: Keywords for Classification 7 | CATEGORIES = { 8 | "AI & LLM": ["gpt", "openai", "ai ", "artificial intelligence", " machine learning", "copilot", "chatgpt", "generative", "nlp", "bot"], 9 | "Database & SQL": ["sql", "database", "query", "postgres", "mysql", "oracle", "mongodb", "azure data", "bigquery", "snowflake", "data connector"], 10 | "Data Visualization": ["chart", "graph", "diagram", "plot", "dashboard", "visualize", "sankey", "gantt", "map", "infographic"], 11 | "Finance & Accounting": ["finance", "budget", "tax", "accounting", "ledger", "invoice", "payroll", "currency", "stock", "portfolio"], 12 | "Sales & CRM": ["crm", "salesforce", "hubspot", "marketing", "lead", "customer", "dynamics 365"], 13 | "Productivity & Utilities": ["template", "format", "merge", "utility", "automation", "converter", "pdf", "qr code", "barcode"] 14 | } 15 | 16 | PLUGIN_DIR = "excel_plugins" 17 | 18 | def parse_markdown(file_path): 19 | with open(file_path, 'r', encoding='utf-8') as f: 20 | content = f.read() 21 | 22 | # Extract Title 23 | title_match = re.search(r'^# (.+)$', content, re.MULTILINE) 24 | title = title_match.group(1) if title_match else "Unknown" 25 | 26 | # Extract Metadata 27 | votes_match = re.search(r'\*\*Rating:\*\* .*?\((\d+) votes\)', content) 28 | rating_match = re.search(r'\*\*Rating:\*\* ([\d\.]+)', content) 29 | 30 | votes = int(votes_match.group(1)) if votes_match else 0 31 | rating = float(rating_match.group(1)) if rating_match else 0.0 32 | 33 | # Extract Description for keyword searching 34 | desc_start = content.find("## Description") 35 | description = content[desc_start:].lower() if desc_start != -1 else "" 36 | 37 | # Also search in the title 38 | search_text = (title + " " + description).lower() 39 | 40 | return { 41 | "title": title, 42 | "filename": os.path.basename(file_path), 43 | "votes": votes, 44 | "rating": rating, 45 | "search_text": search_text, 46 | "score": votes * rating # Simple impact score 47 | } 48 | 49 | def classify_plugin(plugin): 50 | tags = [] 51 | for category, keywords in CATEGORIES.items(): 52 | for keyword in keywords: 53 | if keyword in plugin["search_text"]: 54 | tags.append(category) 55 | break # Found one keyword for this category, move to next category 56 | return tags 57 | 58 | def main(): 59 | plugins = [] 60 | 61 | files = [f for f in os.listdir(PLUGIN_DIR) if f.endswith('.md')] 62 | print(f"Analyzing {len(files)} plugins...") 63 | 64 | for file in files: 65 | path = os.path.join(PLUGIN_DIR, file) 66 | try: 67 | data = parse_markdown(path) 68 | data["tags"] = classify_plugin(data) 69 | plugins.append(data) 70 | except Exception as e: 71 | print(f"Error parsing {file}: {e}") 72 | 73 | # Sort by Score (Votes * Rating) 74 | plugins.sort(key=lambda x: x["score"], reverse=True) 75 | 76 | # Generate Report 77 | report_lines = [] 78 | report_lines.append("# Excel Plugin Analysis Report\n") 79 | report_lines.append("This report classifies plugins by functionality and ranks them by 'Impact Score' (Rating × Votes).\n") 80 | 81 | # 1. Top Rated Overall 82 | report_lines.append("## 🏆 Top 20 Most Useful Plugins (Overall)\n") 83 | report_lines.append("| Rank | Name | Score | Rating | Votes | Categories |") 84 | report_lines.append("|---|---|---|---|---|---|") 85 | for i, p in enumerate(plugins[:20]): 86 | tags_str = ", ".join(p["tags"]) if p["tags"] else "General" 87 | report_lines.append(f"| {i+1} | {p['title']} | {p['score']:.1f} | {p['rating']} | {p['votes']} | {tags_str} |") 88 | 89 | report_lines.append("\n---\n") 90 | 91 | # 2. Top by Category 92 | for category in CATEGORIES.keys(): 93 | category_plugins = [p for p in plugins if category in p["tags"]] 94 | category_plugins.sort(key=lambda x: x["score"], reverse=True) 95 | 96 | report_lines.append(f"## 📂 {category} (Top 10)\n") 97 | if not category_plugins: 98 | report_lines.append("_No plugins found matching this category._\n") 99 | else: 100 | report_lines.append("| Rank | Name | Score | Description Snippet |") 101 | report_lines.append("|---|---|---|---|") 102 | for i, p in enumerate(category_plugins[:10]): 103 | report_lines.append(f"| {i+1} | {p['title']} | {p['score']:.1f} | {p['filename']} |") 104 | report_lines.append("\n") 105 | 106 | # Write Report 107 | with open("PLUGIN_ANALYSIS_REPORT.md", "w", encoding='utf-8') as f: 108 | f.write("\n".join(report_lines)) 109 | 110 | print(f"Analysis complete. Report saved to 'PLUGIN_ANALYSIS_REPORT.md'.") 111 | 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /src/vendor/jscpd/core/statistic.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import type { DetectorEvents, IEventPayload, IHandler, IStatistic, IStatisticRow, ISubscriber } from "."; 3 | 4 | export class Statistic implements ISubscriber { 5 | private static getDefaultStatistic(): IStatisticRow { 6 | return { 7 | lines: 0, 8 | tokens: 0, 9 | sources: 0, 10 | clones: 0, 11 | duplicatedLines: 0, 12 | duplicatedTokens: 0, 13 | percentage: 0, 14 | percentageTokens: 0, 15 | newDuplicatedLines: 0, 16 | newClones: 0, 17 | }; 18 | } 19 | 20 | private statistic: IStatistic = { 21 | detectionDate: new Date().toISOString(), 22 | formats: {}, 23 | total: Statistic.getDefaultStatistic(), 24 | }; 25 | 26 | public subscribe(): Partial> { 27 | return { 28 | CLONE_FOUND: this.cloneFound.bind(this), 29 | START_DETECTION: this.matchSource.bind(this), 30 | }; 31 | } 32 | 33 | public getStatistic(): IStatistic { 34 | return this.statistic; 35 | } 36 | 37 | private cloneFound(payload: IEventPayload): void { 38 | const { clone } = payload; 39 | const id: string = clone.duplicationA.sourceId; 40 | const id2: string = clone.duplicationB.sourceId; 41 | const linesCount: number = clone.duplicationA.end.line - clone.duplicationA.start.line; 42 | const duplicatedTokens: number = clone.duplicationA.end.position - clone.duplicationA.start.position; 43 | 44 | this.statistic.total.clones++; 45 | this.statistic.total.duplicatedLines += linesCount; 46 | this.statistic.total.duplicatedTokens += duplicatedTokens; 47 | this.statistic.formats[clone.format].total.clones++; 48 | this.statistic.formats[clone.format].total.duplicatedLines += linesCount; 49 | this.statistic.formats[clone.format].total.duplicatedTokens += duplicatedTokens; 50 | 51 | this.statistic.formats[clone.format].sources[id].clones++; 52 | this.statistic.formats[clone.format].sources[id].duplicatedLines += linesCount; 53 | this.statistic.formats[clone.format].sources[id].duplicatedTokens += duplicatedTokens; 54 | 55 | this.statistic.formats[clone.format].sources[id2].clones++; 56 | this.statistic.formats[clone.format].sources[id2].duplicatedLines += linesCount; 57 | this.statistic.formats[clone.format].sources[id2].duplicatedTokens += duplicatedTokens; 58 | 59 | this.updatePercentage(clone.format); 60 | } 61 | 62 | private matchSource(payload: IEventPayload): void { 63 | const { source } = payload; 64 | const format = source.getFormat(); 65 | if (!(format in this.statistic.formats)) { 66 | this.statistic.formats[format] = { 67 | sources: {}, 68 | total: Statistic.getDefaultStatistic(), 69 | }; 70 | } 71 | this.statistic.total.sources++; 72 | this.statistic.total.lines += source.getLinesCount(); 73 | this.statistic.total.tokens += source.getTokensCount(); 74 | this.statistic.formats[format].total.sources++; 75 | this.statistic.formats[format].total.lines += source.getLinesCount(); 76 | this.statistic.formats[format].total.tokens += source.getTokensCount(); 77 | 78 | this.statistic.formats[format].sources[source.getId()] = 79 | this.statistic.formats[format].sources[source.getId()] || Statistic.getDefaultStatistic(); 80 | 81 | this.statistic.formats[format].sources[source.getId()].sources = 1; 82 | this.statistic.formats[format].sources[source.getId()].lines += source.getLinesCount(); 83 | this.statistic.formats[format].sources[source.getId()].tokens += source.getTokensCount(); 84 | this.updatePercentage(format); 85 | } 86 | 87 | private updatePercentage(format: string): void { 88 | this.statistic.total.percentage = Statistic.calculatePercentage( 89 | this.statistic.total.lines, 90 | this.statistic.total.duplicatedLines, 91 | ); 92 | this.statistic.total.percentageTokens = Statistic.calculatePercentage( 93 | this.statistic.total.tokens, 94 | this.statistic.total.duplicatedTokens, 95 | ); 96 | 97 | this.statistic.formats[format].total.percentage = Statistic.calculatePercentage( 98 | this.statistic.formats[format].total.lines, 99 | this.statistic.formats[format].total.duplicatedLines, 100 | ); 101 | this.statistic.formats[format].total.percentageTokens = Statistic.calculatePercentage( 102 | this.statistic.formats[format].total.tokens, 103 | this.statistic.formats[format].total.duplicatedTokens, 104 | ); 105 | 106 | Object.entries(this.statistic.formats[format].sources).forEach(([id, stat]) => { 107 | this.statistic.formats[format].sources[id].percentage = Statistic.calculatePercentage( 108 | stat.lines, 109 | stat.duplicatedLines, 110 | ); 111 | this.statistic.formats[format].sources[id].percentageTokens = Statistic.calculatePercentage( 112 | stat.tokens, 113 | stat.duplicatedTokens, 114 | ); 115 | }); 116 | } 117 | 118 | private static calculatePercentage(total: number, cloned: number): number { 119 | return total ? Math.round((10000 * cloned) / total) / 100 : 0.0; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /config/cloud_prod.yaml: -------------------------------------------------------------------------------- 1 | mcp: 2 | embedding: 3 | enabled: true 4 | provider: cloudru 5 | model: "BAAI/bge-m3" 6 | fallbackToMemory: true 7 | cloudru: 8 | baseUrl: "https://foundation-models.api.cloud.ru" 9 | timeout: 15000 10 | concurrency: 4 11 | maxBatchSize: 128 12 | 13 | server: 14 | host: "localhost" 15 | port: 3000 16 | 17 | agents: 18 | maxConcurrent: 10 # Higher concurrency for production 19 | defaultTimeout: 900000 # 900 seconds for complex operations 20 | useParser: true # Enable ParserAgent for AST parsing (MCP_USE_PARSER) 21 | devIndexBatch: 100 # Batch size for file processing (MCP_DEV_INDEX_BATCH) 22 | 23 | # Database Configuration (Production Optimized) 24 | database: 25 | path: "/tmp/data/vectors.db" # Persistent volume path 26 | mode: "WAL" # WAL mode for best performance 27 | cacheSize: 50000 # Larger cache for production 28 | mmapSize: 1073741824 # 1GB memory mapping for large datasets 29 | synchronous: "NORMAL" # Balance between performance and safety 30 | tempStore: "MEMORY" # Keep temp data in memory 31 | 32 | # Logging Configuration (Production) 33 | logging: 34 | level: "warn" # Only warnings and errors in production 35 | format: "json" # Structured JSON logging for log aggregation 36 | enableConsole: false # Disable console output in production 37 | outputFile: "/tmp/logs_mcp/mcp-server.log" # Persistent log file 38 | maxFileSize: "100MB" # Larger log files for production 39 | maxFiles: 10 # Keep more log files for debugging 40 | 41 | # Parser Configuration (Production Optimized) 42 | parser: 43 | treeSitter: 44 | enabled: true 45 | languageConfigs: 46 | - "typescript" 47 | - "javascript" 48 | - "python" 49 | - "c" 50 | - "cpp" 51 | - "csharp" 52 | - "java" 53 | - "kotlin" 54 | - "rust" 55 | - "go" 56 | - "vba" 57 | maxFileSize: 5242880 # 5MB maximum file size for production 58 | timeout: 180000 # 180 seconds timeout for large files 59 | bufferSize: 1048576 # 1MB buffer size for tree-sitter parser 60 | 61 | incremental: 62 | enabled: true 63 | cacheSize: 5000 # Larger cache for production workloads 64 | cacheTTL: 1800000 # 30 minutes cache TTL 65 | 66 | agent: 67 | maxConcurrency: 4 # Parallel file processing 68 | memoryLimit: 2024 # MB - memory limit for parser agent 69 | priority: 8 # High priority for parsing 70 | batchSize: 10 # Files per batch 71 | cacheSize: 104857600 # 100MB cache (in bytes) 72 | workerPoolSize: 2 # Number of worker threads 73 | 74 | indexer: 75 | maxConcurrency: 5 # Single database operation for development 76 | memoryLimit: 2024 # MB 77 | priority: 7 78 | batchSize: 500 79 | cacheSize: 25165824 # 25MB cache (in bytes) 80 | cacheTTL: 180000 # 3 minutes (in ms) 81 | 82 | devAgent: 83 | maxConcurrency: 5 84 | memoryLimit: 2024 85 | priority: 7 86 | 87 | doraAgent: 88 | maxConcurrency: 3 89 | memoryLimit: 2024 90 | priority: 6 91 | 92 | queryAgent: 93 | maxConcurrency: 16 94 | memoryLimit: 2024 95 | priority: 9 96 | simpleQueryTimeout: 100 97 | complexQueryTimeout: 1500 98 | cacheWarmupSize: 200 99 | 100 | semanticAgent: 101 | maxConcurrency: 8 102 | memoryLimit: 512 103 | priority: 8 104 | batchSize: 16 105 | modelPath: "./models" 106 | 107 | coordinator: 108 | maxConcurrency: 160 109 | memoryLimit: 2024 110 | priority: 10 111 | taskQueueLimit: 160 112 | loadBalancingStrategy: "least-loaded" 113 | resourceConstraints: 114 | maxMemoryMB: 8096 115 | maxCpuPercent: 80 116 | maxConcurrentAgents: 20 117 | maxTaskQueueSize: 160 118 | 119 | conductor: 120 | maxConcurrency: 160 121 | memoryLimit: 2024 122 | priority: 10 123 | taskQueueLimit: 160 124 | loadBalancingStrategy: "least-loaded" 125 | resourceConstraints: 126 | maxMemoryMB: 8096 127 | maxCpuPercent: 80 128 | maxConcurrentAgents: 20 129 | maxTaskQueueSize: 160 130 | complexityThreshold: 8 131 | mandatoryDelegation: true 132 | 133 | # Performance Configuration (Production) 134 | performance: 135 | monitoring: 136 | enabled: true 137 | collectMetrics: true 138 | metricsInterval: 300000 # 5 minutes metrics collection 139 | 140 | optimization: 141 | enableVectorSearch: true # Enable vector search in production 142 | enableParallelParsing: true 143 | maxWorkerThreads: 8 # More threads for production servers 144 | 145 | # Production Security 146 | security: 147 | enableCORS: false # Disable CORS for production API 148 | rateLimiting: 149 | enabled: true 150 | requestsPerMinute: 1000 # Rate limit for API endpoints 151 | 152 | # Resource Limits 153 | resources: 154 | maxMemoryUsage: "8GB" # Memory limit for the application 155 | maxCpuUsage: "80%" # CPU usage threshold 156 | 157 | # Health Checks 158 | health: 159 | enableHealthCheck: true 160 | healthCheckInterval: 30000 # 30 seconds health check 161 | gracefulShutdownTimeout: 10000 # 10 seconds graceful shutdown 162 | -------------------------------------------------------------------------------- /config/ollama.yaml: -------------------------------------------------------------------------------- 1 | 2 | debug: false 3 | 4 | mcp: 5 | embedding: 6 | enabled: true 7 | provider: ollama 8 | model: "nomic-embed-text" 9 | fallbackToMemory: true 10 | ollama: 11 | baseUrl: "http://127.0.0.1:11434" 12 | timeout: 60000 13 | concurrency: 1 14 | maxBatchSize: 16 15 | 16 | server: 17 | host: "localhost" 18 | port: 3000 19 | 20 | agents: 21 | maxConcurrent: 10 # Higher concurrency for production 22 | defaultTimeout: 900000 # 900 seconds for complex operations 23 | useParser: true # Enable ParserAgent for AST parsing (MCP_USE_PARSER) 24 | devIndexBatch: 100 # Batch size for file processing (MCP_DEV_INDEX_BATCH) 25 | 26 | # Database Configuration (Production Optimized) 27 | database: 28 | path: "/tmp/data/vectors.db" # Persistent volume path 29 | mode: "WAL" # WAL mode for best performance 30 | cacheSize: 50000 # Larger cache for production 31 | mmapSize: 1073741824 # 1GB memory mapping for large datasets 32 | synchronous: "NORMAL" # Balance between performance and safety 33 | tempStore: "MEMORY" # Keep temp data in memory 34 | 35 | # Logging Configuration (Production) 36 | logging: 37 | level: "warn" # Only warnings and errors in production 38 | format: "json" # Structured JSON logging for log aggregation 39 | enableConsole: false # Disable console output in production 40 | outputFile: "/tmp/logs_mcp/mcp-server.log" # Persistent log file 41 | maxFileSize: "100MB" # Larger log files for production 42 | maxFiles: 10 # Keep more log files for debugging 43 | 44 | # Parser Configuration (Production Optimized) 45 | parser: 46 | treeSitter: 47 | enabled: true 48 | languageConfigs: 49 | - "typescript" 50 | - "javascript" 51 | - "python" 52 | - "c" 53 | - "cpp" 54 | - "csharp" 55 | - "java" 56 | - "kotlin" 57 | - "rust" 58 | - "go" 59 | - "vba" 60 | maxFileSize: 5242880 # 5MB maximum file size for production 61 | timeout: 180000 # 180 seconds timeout for large files 62 | bufferSize: 1048576 # 1MB buffer size for tree-sitter parser 63 | 64 | incremental: 65 | enabled: true 66 | cacheSize: 5000 # Larger cache for production workloads 67 | cacheTTL: 1800000 # 30 minutes cache TTL 68 | 69 | agent: 70 | maxConcurrency: 4 # Parallel file processing 71 | memoryLimit: 2024 # MB - memory limit for parser agent 72 | priority: 8 # High priority for parsing 73 | batchSize: 10 # Files per batch 74 | cacheSize: 104857600 # 100MB cache (in bytes) 75 | workerPoolSize: 2 # Number of worker threads 76 | 77 | indexer: 78 | maxConcurrency: 5 # Single database operation for development 79 | memoryLimit: 2024 # MB 80 | priority: 7 81 | batchSize: 500 82 | cacheSize: 25165824 # 25MB cache (in bytes) 83 | cacheTTL: 180000 # 3 minutes (in ms) 84 | 85 | devAgent: 86 | maxConcurrency: 5 87 | memoryLimit: 2024 88 | priority: 7 89 | 90 | doraAgent: 91 | maxConcurrency: 3 92 | memoryLimit: 2024 93 | priority: 6 94 | 95 | queryAgent: 96 | maxConcurrency: 16 97 | memoryLimit: 2024 98 | priority: 9 99 | simpleQueryTimeout: 100 100 | complexQueryTimeout: 1500 101 | cacheWarmupSize: 200 102 | 103 | semanticAgent: 104 | maxConcurrency: 8 105 | memoryLimit: 2024 106 | priority: 8 107 | batchSize: 16 108 | modelPath: "./models" 109 | 110 | coordinator: 111 | maxConcurrency: 160 112 | memoryLimit: 2024 113 | priority: 10 114 | taskQueueLimit: 160 115 | loadBalancingStrategy: "least-loaded" 116 | resourceConstraints: 117 | maxMemoryMB: 8096 118 | maxCpuPercent: 80 119 | maxConcurrentAgents: 20 120 | maxTaskQueueSize: 160 121 | 122 | conductor: 123 | maxConcurrency: 160 124 | memoryLimit: 2024 125 | priority: 10 126 | taskQueueLimit: 160 127 | loadBalancingStrategy: "least-loaded" 128 | resourceConstraints: 129 | maxMemoryMB: 8096 130 | maxCpuPercent: 80 131 | maxConcurrentAgents: 20 132 | maxTaskQueueSize: 160 133 | complexityThreshold: 8 134 | mandatoryDelegation: true 135 | 136 | # Performance Configuration (Production) 137 | performance: 138 | monitoring: 139 | enabled: true 140 | collectMetrics: true 141 | metricsInterval: 300000 # 5 minutes metrics collection 142 | 143 | optimization: 144 | enableVectorSearch: true # Enable vector search in production 145 | enableParallelParsing: true 146 | maxWorkerThreads: 8 # More threads for production servers 147 | 148 | # Production Security 149 | security: 150 | enableCORS: false # Disable CORS for production API 151 | rateLimiting: 152 | enabled: true 153 | requestsPerMinute: 1000 # Rate limit for API endpoints 154 | 155 | # Resource Limits 156 | resources: 157 | maxMemoryUsage: "8GB" # Memory limit for the application 158 | maxCpuUsage: "80%" # CPU usage threshold 159 | 160 | # Health Checks 161 | health: 162 | enableHealthCheck: true 163 | healthCheckInterval: 30000 # 30 seconds health check 164 | gracefulShutdownTimeout: 10000 # 10 seconds graceful shutdown 165 | --------------------------------------------------------------------------------