Repository navigation
feat(copilot): AI assistant with CodeGraph + CLI + knowledge base #2669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { processCopilotChat } from "@/lib/copilot/engine"; | ||
| import type { CopilotRequest } from "@/lib/copilot/engine"; | ||
|
|
||
| /** | ||
| * POST /api/copilot/chat | ||
| * | ||
| * OmniRoute Copilot chat endpoint. | ||
| * Accepts user messages about OmniRoute configuration and returns | ||
| * tool-based responses + AI guidance. | ||
| * | ||
| * Body: { messages: [{ role: "user"|"assistant", content: string }] } | ||
| */ | ||
| export async function POST(request: Request) { | ||
| try { | ||
| const body: CopilotRequest = await request.json(); | ||
|
|
||
| if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) { | ||
| return NextResponse.json( | ||
| { error: "messages array is required" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
Comment on lines
+16
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The request body is being manually validated instead of using a Zod schema. According to the repository style guide (Rule 2.8), all inputs must be validated with Zod schemas from References
|
||
|
|
||
| const response = await processCopilotChat(body); | ||
|
|
||
| return NextResponse.json(response, { status: 200 }); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : "Unknown error"; | ||
| return NextResponse.json( | ||
| { error: `Copilot error: ${message}` }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| /** | ||
| * 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 { join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // 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; | ||
| } | ||
|
|
||
| export interface CodeGraphEdge { | ||
| id: number; | ||
| source: string; | ||
| target: string; | ||
| kind: string; | ||
| line?: number; | ||
| metadata?: Record<string, unknown>; | ||
| } | ||
|
|
||
| export interface CodeGraphFile { | ||
| path: string; | ||
| language: string; | ||
| nodeCount: number; | ||
| modifiedAt: number; | ||
| } | ||
|
|
||
| export interface CodeGraphSearchResult { | ||
| nodes: CodeGraphNode[]; | ||
| total: number; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Database access (lazy loaded) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| let _db: unknown = null; | ||
|
|
||
| function getDbPath(): string | null { | ||
| // 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 = fileURLToPath(new URL(".", 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 { | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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]); | ||
| } | ||
|
|
||
| /** | ||
| * Get impact analysis: find symbols that depend on a given symbol (transitively). | ||
| */ | ||
| export function getImpactAnalysis(symbolName: string, depth = 1): CodeGraphQueryResult { | ||
| if (depth <= 0) return { success: false, data: null, error: "Depth must be >= 1", engine: "none" }; | ||
|
|
||
| // Direct callers (depth 1) | ||
| const directCallers = findCallers(symbolName); | ||
| if (depth === 1) return directCallers; | ||
|
|
||
| // For depth > 1, we'd need recursive CTE or multiple queries. | ||
| // For now, just return direct callers with a note. | ||
| const result = directCallers; | ||
| return { | ||
| ...result, | ||
| data: (result.data as Record<string, unknown>[])?.map((r) => ({ | ||
| ...r, | ||
| _depth: 1, | ||
| _note: `Depth > 1 requires multiple queries. Use searchSymbols() + findCallers() iteratively for deeper analysis.`, | ||
| })), | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * 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`); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This endpoint allows sensitive operations such as creating API keys, revoking them, and executing CLI commands. However, it currently lacks any authentication or authorization checks. This is a critical security vulnerability that allows any unauthenticated user to potentially compromise the system.