mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 00:52:18 +03:00
Validated on the resolved merge against the current tip (527da656 + the post-#11281 rebaseline): the single conflict was a comment-only collision in providers/[id]/models/route.ts (kept the tip's #10828-ordering note). Focused suites 125/125 across all 13 touched test files (build-sqlite-stub, cc-compatible, copilot-claude-messages, copilot-gemini-route, executor-github, ghe-copilot, github-copilot-discovery-token, github-copilot-model-discovery, noauth-sibling-7620, provider-header-profiles, provider-models-config, request-log-payloads, upstream-error-passthrough), typecheck:core clean, file-size/changelog-integrity OK. Merged --admin over the inherited 2026-08-23 base-red cluster (#9985) — the reds are proven tip failures (CLI catalog cluster + @testing-library allowlist, being drained by #11280), not from this diff. Note: the rebase means several items the body listed (relay x-relay-path SSRF, /v1/search blocked-providers, #10736 rotation fence, #10903, #10865, #10899, #10916) already landed upstream and are NOT in this delta — the delta is: better-sqlite3 build guard + build heap/worker caps + telemetry-off (#10060 re-derived), credential-echo passthrough refusal + OCR/moderation redaction + call-log key redaction, Copilot CLI 1.0.81-6 wire identity + Claude→/v1/messages name-matched routing + discovery token fix, CC model_not_found 400, compat overrides for no-auth aliases (#7620-pinned). The Copilot wire-identity change is the one to watch in production. Thank you @arminanton — and the ported-author credits in the commit history (@rqzbeh, yidecode, the #10899/#10916 authors) are preserved. Your config-posture finding (REQUIRE_API_KEY default vs 0.0.0.0) is noted for a maintainer decision, as you scoped it.
246 lines
7.3 KiB
TypeScript
246 lines
7.3 KiB
TypeScript
/**
|
|
* Copilot CodeGraph Knowledge Module
|
|
*
|
|
* Provides the Copilot with read-only access to the project's CodeGraph index.
|
|
* Queries the `.codegraph/codegraph.db` SQLite database to find symbols,
|
|
* explore relationships, list files, and search documentation.
|
|
*
|
|
* Falls back gracefully if the CodeGraph DB does not exist (e.g., production installs).
|
|
*/
|
|
|
|
import { existsSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { isNextBuildPhase } from "../buildPhase";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface CodeGraphNode {
|
|
id: string;
|
|
kind: string;
|
|
name: string;
|
|
qualifiedName: string;
|
|
filePath: string;
|
|
language: string;
|
|
startLine: number;
|
|
endLine: number;
|
|
signature?: string;
|
|
docstring?: string;
|
|
isExported: boolean;
|
|
visibility?: string;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Database access (lazy loaded)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let _db: unknown = null;
|
|
let dbPathOverride: string | null | undefined;
|
|
|
|
/** Override the index path for deterministic tests and embedded callers. */
|
|
export function setCodeGraphPathForTest(path: string | null | undefined): void {
|
|
dbPathOverride = path;
|
|
_db = null;
|
|
}
|
|
|
|
function getDbPath(): string | null {
|
|
if (dbPathOverride !== undefined) return dbPathOverride;
|
|
|
|
// Try project root first (dev), then cwd, then DATA_DIR
|
|
const candidates = [
|
|
join(process.cwd(), ".codegraph", "codegraph.db"),
|
|
join(process.cwd(), "..", ".codegraph", "codegraph.db"),
|
|
];
|
|
|
|
// Try to resolve from the project root
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Walk up to find .codegraph/
|
|
let dir = __dirname;
|
|
for (let i = 0; i < 10; i++) {
|
|
const candidate = join(dir, ".codegraph", "codegraph.db");
|
|
if (existsSync(candidate)) return candidate;
|
|
const parent = join(dir, "..");
|
|
if (parent === dir) break;
|
|
dir = parent;
|
|
}
|
|
|
|
for (const c of candidates) {
|
|
if (existsSync(c)) return c;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export interface CodeGraphQueryResult {
|
|
success: boolean;
|
|
data: unknown;
|
|
error?: string;
|
|
engine: "sqlite" | "cli" | "none";
|
|
}
|
|
|
|
function queryDb(query: string, params: unknown[] = []): CodeGraphQueryResult {
|
|
try {
|
|
if (!_db) {
|
|
const dbPath = getDbPath();
|
|
if (!dbPath) {
|
|
return { success: false, data: null, error: "CodeGraph DB not found", engine: "none" };
|
|
}
|
|
// Dynamic import to avoid hard dependency on better-sqlite3
|
|
_db = null;
|
|
|
|
// Use better-sqlite3 if available
|
|
try {
|
|
// Never load the native better-sqlite3 addon during the Next.js build:
|
|
// its Statement destructor aborts with SIGABRT at build-worker teardown
|
|
// (node::RemoveEnvironmentCleanupHook). This path is not exercised during
|
|
// build, so failing closed to "not available" is safe. (#10060)
|
|
if (isNextBuildPhase()) throw new Error("Skip better-sqlite3 during build");
|
|
|
|
const Database = require("better-sqlite3");
|
|
_db = new Database(dbPath, { readonly: true });
|
|
} catch {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: "better-sqlite3 not available",
|
|
engine: "none",
|
|
};
|
|
}
|
|
}
|
|
|
|
const stmt = (
|
|
_db as { prepare: (sql: string) => { all: (params: unknown[]) => unknown[] } }
|
|
).prepare(query);
|
|
const rows = stmt.all(params);
|
|
return { success: true, data: rows, engine: "sqlite" };
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
data: null,
|
|
error: err instanceof Error ? err.message : "Unknown error",
|
|
engine: "none",
|
|
};
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Search operations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Search symbols by name (exact or partial match via FTS).
|
|
*/
|
|
export function searchSymbols(query: string, limit = 20): CodeGraphQueryResult {
|
|
const sql = `
|
|
SELECT n.*
|
|
FROM nodes n
|
|
JOIN nodes_fts fts ON n.id = fts.id
|
|
WHERE nodes_fts MATCH ?
|
|
ORDER BY rank
|
|
LIMIT ?
|
|
`;
|
|
|
|
// Escape FTS special chars and create prefix query
|
|
const sanitized = query.replace(/[^a-zA-Z0-9_]/g, " ").trim();
|
|
if (!sanitized) {
|
|
// Fallback to LIKE if query is empty after sanitization
|
|
return queryDb(`SELECT * FROM nodes WHERE lower(name) LIKE ? ORDER BY kind, name LIMIT ?`, [
|
|
`%${query.toLowerCase()}%`,
|
|
limit,
|
|
]);
|
|
}
|
|
|
|
const ftsQuery = sanitized
|
|
.split(/\s+/)
|
|
.map((w) => `"${w}"*`)
|
|
.join(" AND ");
|
|
|
|
return queryDb(sql, [ftsQuery, limit]);
|
|
}
|
|
|
|
/**
|
|
* Find callers of a symbol (edges where target matches).
|
|
*/
|
|
export function findCallers(symbolName: string, limit = 20): CodeGraphQueryResult {
|
|
return queryDb(
|
|
`SELECT e.id as edgeId, e.kind as edgeKind, e.line, e.col,
|
|
s.id as sourceId, s.name as sourceName, s.kind as sourceKind,
|
|
s.file_path as sourceFile, s.start_line as sourceLine,
|
|
t.name as targetName, t.file_path as targetFile
|
|
FROM edges e
|
|
JOIN nodes s ON e.source = s.id
|
|
JOIN nodes t ON e.target = t.id
|
|
WHERE t.name = ?
|
|
ORDER BY e.kind
|
|
LIMIT ?`,
|
|
[symbolName, limit]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Find callees of a symbol (edges where source matches).
|
|
*/
|
|
export function findCallees(symbolName: string, limit = 20): CodeGraphQueryResult {
|
|
return queryDb(
|
|
`SELECT e.id as edgeId, e.kind as edgeKind, e.line, e.col,
|
|
s.name as sourceName,
|
|
t.id as targetId, t.name as targetName, t.kind as targetKind,
|
|
t.file_path as targetFile, t.start_line as targetLine
|
|
FROM edges e
|
|
JOIN nodes s ON e.source = s.id
|
|
JOIN nodes t ON e.target = t.id
|
|
WHERE s.name = ?
|
|
ORDER BY e.kind
|
|
LIMIT ?`,
|
|
[symbolName, limit]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get context for a file: all symbols defined in it.
|
|
*/
|
|
export function getFileContext(filePath: string): CodeGraphQueryResult {
|
|
// Try matching on suffix of file_path (many nodes store paths relative to root)
|
|
return queryDb(
|
|
`SELECT * FROM nodes
|
|
WHERE file_path LIKE ? OR file_path = ?
|
|
ORDER BY start_line
|
|
LIMIT 100`,
|
|
[`%${filePath}`, filePath]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* List all indexed files, optionally filtered by language.
|
|
*/
|
|
export function listFiles(language?: string, limit = 50): CodeGraphQueryResult {
|
|
if (language) {
|
|
return queryDb(`SELECT * FROM files WHERE language = ? ORDER BY path LIMIT ?`, [
|
|
language,
|
|
limit,
|
|
]);
|
|
}
|
|
return queryDb(`SELECT * FROM files ORDER BY path LIMIT ?`, [limit]);
|
|
}
|
|
|
|
/**
|
|
* Check if CodeGraph DB is available.
|
|
*/
|
|
export function isCodeGraphAvailable(): boolean {
|
|
return getDbPath() !== null;
|
|
}
|
|
|
|
/**
|
|
* Get summary stats from the index.
|
|
*/
|
|
export function getCodeGraphStats(): CodeGraphQueryResult {
|
|
return queryDb(`SELECT 'total_nodes' as key, COUNT(*) as value FROM nodes UNION ALL
|
|
SELECT 'total_edges', COUNT(*) FROM edges UNION ALL
|
|
SELECT 'total_files', COUNT(*) FROM files UNION ALL
|
|
SELECT 'languages', GROUP_CONCAT(DISTINCT language) FROM files UNION ALL
|
|
SELECT 'node_kinds', GROUP_CONCAT(DISTINCT kind) FROM nodes`);
|
|
}
|