@leanlabsinnov/codegraph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ciril Cyriac
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.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @cyrilc/codegraph
2
+
3
+ Live, queryable knowledge graph for your codebase. Indexes JS/TS into an embedded graph
4
+ DB with vector embeddings, then exposes a local MCP server that Claude Code, Cursor, and
5
+ Windsurf can call to answer questions like "what calls `useAuth`?", "what's the blast
6
+ radius of changing `formatPrice`?", or "find symbols semantically similar to a JWT auth
7
+ helper".
8
+
9
+ Zero infrastructure. The graph lives in a folder under `~/.codegraph/`. No Docker, no
10
+ external services.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm i -g @cyrilc/codegraph
16
+ ```
17
+
18
+ Requires Node 20+.
19
+
20
+ ## Quickstart
21
+
22
+ ```bash
23
+ codegraph config llm set byo-openai
24
+ export OPENAI_API_KEY=sk-...
25
+ codegraph index ~/my-project
26
+ codegraph serve
27
+ ```
28
+
29
+ Then point Claude Code / Cursor / Windsurf at `http://127.0.0.1:3748/mcp` with the bearer
30
+ token from `~/.codegraph/config.json`.
31
+
32
+ See the full [README on GitHub](https://github.com/cyrilc/codegraph) for client setup, all
33
+ 10 MCP tools, and troubleshooting.
34
+
35
+ ## Commands
36
+
37
+ | Command | What it does |
38
+ |---|---|
39
+ | `codegraph index <path>` | Walk the repo, parse JS/TS, embed every symbol, write to the graph |
40
+ | `codegraph index <path> --no-embed` | Skip embeddings (faster, semantic search disabled) |
41
+ | `codegraph status <path>` | Node + edge counts and embedding coverage |
42
+ | `codegraph serve` | Boot the MCP server on `:3748` |
43
+ | `codegraph doctor` | Verify Node version, config, LLM credentials, Kuzu writeable |
44
+ | `codegraph config show` | Print the resolved config |
45
+ | `codegraph config llm set [preset]` | Switch LLM preset (interactive when omitted) |
46
+ | `codegraph config llm test` | Round-trip the configured provider |
47
+
48
+ ## License
49
+
50
+ MIT
package/dist/bin.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildProgram,
4
+ renderError
5
+ } from "./chunk-O4ZO6CP5.js";
6
+ import "./chunk-B2TIVKUB.js";
7
+ import "./chunk-F5QKPRNW.js";
8
+ import "./chunk-XGPZDCQ4.js";
9
+
10
+ // src/bin.ts
11
+ import { CommanderError } from "commander";
12
+ var program = buildProgram();
13
+ program.parseAsync(process.argv).catch((err) => {
14
+ if (err instanceof CommanderError) {
15
+ process.exit(err.exitCode);
16
+ }
17
+ if (err instanceof Error && err.name === "ExitPromptError") {
18
+ process.stderr.write("\nCancelled.\n");
19
+ process.exit(130);
20
+ }
21
+ const verbose = process.env.CODEGRAPH_VERBOSE === "1";
22
+ process.stderr.write(`${renderError(err, { verbose })}
23
+ `);
24
+ process.exit(1);
25
+ });
26
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { CommanderError } from \"commander\";\nimport { buildProgram } from \"./program.js\";\nimport { renderError } from \"./ui.js\";\n\nconst program = buildProgram();\n\nprogram.parseAsync(process.argv).catch((err) => {\n // Commander's own usage errors print themselves; don't double-render them in a box.\n if (err instanceof CommanderError) {\n process.exit(err.exitCode);\n }\n // Inquirer raises this when the user hits Ctrl-C inside a prompt. Treat as a clean exit.\n if (err instanceof Error && err.name === \"ExitPromptError\") {\n process.stderr.write(\"\\nCancelled.\\n\");\n process.exit(130);\n }\n const verbose = process.env.CODEGRAPH_VERBOSE === \"1\";\n process.stderr.write(`${renderError(err, { verbose })}\\n`);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;AACA,SAAS,sBAAsB;AAI/B,IAAM,UAAU,aAAa;AAE7B,QAAQ,WAAW,QAAQ,IAAI,EAAE,MAAM,CAAC,QAAQ;AAE9C,MAAI,eAAe,gBAAgB;AACjC,YAAQ,KAAK,IAAI,QAAQ;AAAA,EAC3B;AAEA,MAAI,eAAe,SAAS,IAAI,SAAS,mBAAmB;AAC1D,YAAQ,OAAO,MAAM,gBAAgB;AACrC,YAAQ,KAAK,GAAG;AAAA,EAClB;AACA,QAAM,UAAU,QAAQ,IAAI,sBAAsB;AAClD,UAAQ,OAAO,MAAM,GAAG,YAAY,KAAK,EAAE,QAAQ,CAAC,CAAC;AAAA,CAAI;AACzD,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
@@ -0,0 +1,139 @@
1
+ import {
2
+ namespaceToString
3
+ } from "./chunk-XGPZDCQ4.js";
4
+
5
+ // ../llm-router/src/router.ts
6
+ import { embedMany, generateText } from "ai";
7
+ async function createLlmRouter(opts) {
8
+ const env = opts.env ?? process.env;
9
+ const resolved = {
10
+ ...opts.config,
11
+ embeddingNamespace: {
12
+ provider: opts.config.embeddings.provider,
13
+ model: opts.config.embeddings.model,
14
+ dimension: opts.config.embeddings.dimension
15
+ }
16
+ };
17
+ const generationModel = await buildGenerationModel(opts.config, env);
18
+ const embeddingModel = await buildEmbeddingModel(opts.config.embeddings, env);
19
+ async function doEmbed(texts) {
20
+ if (texts.length === 0) return [];
21
+ const { embeddings } = await embedMany({ model: embeddingModel, values: texts });
22
+ return embeddings;
23
+ }
24
+ async function doGenerate(options) {
25
+ const { text } = await generateText({
26
+ model: generationModel,
27
+ ...options.system !== void 0 ? { system: options.system } : {},
28
+ messages: options.messages,
29
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {},
30
+ ...options.maxTokens !== void 0 ? { maxTokens: options.maxTokens } : {}
31
+ });
32
+ return text;
33
+ }
34
+ const router = {
35
+ config: resolved,
36
+ embed: doEmbed,
37
+ generate: doGenerate,
38
+ async selfTest() {
39
+ const embedStart = Date.now();
40
+ const embeds = await doEmbed(["codegraph self-test"]);
41
+ const embedLatencyMs = Date.now() - embedStart;
42
+ const genStart = Date.now();
43
+ const out = await doGenerate({
44
+ messages: [{ role: "user", content: "Reply with the single word ok." }],
45
+ maxTokens: 5,
46
+ temperature: 0
47
+ });
48
+ const generateLatencyMs = Date.now() - genStart;
49
+ return {
50
+ embedDims: embeds[0]?.length ?? 0,
51
+ generationOk: typeof out === "string" && out.length > 0,
52
+ embedLatencyMs,
53
+ generateLatencyMs
54
+ };
55
+ }
56
+ };
57
+ return router;
58
+ }
59
+ function namespaceLabel(config) {
60
+ return namespaceToString({
61
+ provider: config.embeddings.provider,
62
+ model: config.embeddings.model,
63
+ dimension: config.embeddings.dimension
64
+ });
65
+ }
66
+ async function buildGenerationModel(config, env) {
67
+ const provider = config.generation.provider;
68
+ if (provider === "openai") return openaiText(config.generation, env, config.baseUrl);
69
+ if (provider === "anthropic") return anthropicText(config.generation, env, config.baseUrl);
70
+ if (provider === "google") return googleText(config.generation, env, config.baseUrl);
71
+ if (provider === "ollama") return ollamaText(config.generation, env, config.baseUrl);
72
+ throw new Error(`Unsupported generation provider: ${provider}`);
73
+ }
74
+ async function buildEmbeddingModel(config, env) {
75
+ if (config.provider === "openai") return openaiEmbedding(config, env);
76
+ if (config.provider === "google") return googleEmbedding(config, env);
77
+ if (config.provider === "ollama") return ollamaEmbedding(config, env);
78
+ if (config.provider === "anthropic") {
79
+ return openaiEmbedding(
80
+ { provider: "openai", model: "text-embedding-3-small", dimension: 1536 },
81
+ env
82
+ );
83
+ }
84
+ throw new Error(`Unsupported embedding provider: ${config.provider}`);
85
+ }
86
+ async function openaiText(config, env, baseUrl) {
87
+ const { createOpenAI } = await import("@ai-sdk/openai");
88
+ const openai = createOpenAI({
89
+ ...env.OPENAI_API_KEY ? { apiKey: env.OPENAI_API_KEY } : {},
90
+ ...baseUrl ? { baseURL: baseUrl } : {}
91
+ });
92
+ return openai(config.model);
93
+ }
94
+ async function openaiEmbedding(config, env) {
95
+ const { createOpenAI } = await import("@ai-sdk/openai");
96
+ const openai = createOpenAI(env.OPENAI_API_KEY ? { apiKey: env.OPENAI_API_KEY } : {});
97
+ return openai.embedding(config.model);
98
+ }
99
+ async function anthropicText(config, env, baseUrl) {
100
+ const { createAnthropic } = await import("@ai-sdk/anthropic");
101
+ const anthropic = createAnthropic({
102
+ ...env.ANTHROPIC_API_KEY ? { apiKey: env.ANTHROPIC_API_KEY } : {},
103
+ ...baseUrl ? { baseURL: baseUrl } : {}
104
+ });
105
+ return anthropic(config.model);
106
+ }
107
+ async function googleText(config, env, baseUrl) {
108
+ const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
109
+ const google = createGoogleGenerativeAI({
110
+ ...env.GOOGLE_GENERATIVE_AI_API_KEY ? { apiKey: env.GOOGLE_GENERATIVE_AI_API_KEY } : {},
111
+ ...baseUrl ? { baseURL: baseUrl } : {}
112
+ });
113
+ return google(config.model);
114
+ }
115
+ async function googleEmbedding(config, env) {
116
+ const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
117
+ const google = createGoogleGenerativeAI(
118
+ env.GOOGLE_GENERATIVE_AI_API_KEY ? { apiKey: env.GOOGLE_GENERATIVE_AI_API_KEY } : {}
119
+ );
120
+ return google.textEmbeddingModel(config.model);
121
+ }
122
+ async function ollamaText(config, env, baseUrl) {
123
+ const { createOllama } = await import("ollama-ai-provider");
124
+ const base = baseUrl ?? env.OLLAMA_BASE_URL;
125
+ const ollama = createOllama(base ? { baseURL: `${base.replace(/\/$/, "")}/api` } : {});
126
+ return ollama(config.model);
127
+ }
128
+ async function ollamaEmbedding(config, env) {
129
+ const { createOllama } = await import("ollama-ai-provider");
130
+ const base = env.OLLAMA_BASE_URL;
131
+ const ollama = createOllama(base ? { baseURL: `${base.replace(/\/$/, "")}/api` } : {});
132
+ return ollama.embedding(config.model);
133
+ }
134
+
135
+ export {
136
+ createLlmRouter,
137
+ namespaceLabel
138
+ };
139
+ //# sourceMappingURL=chunk-B2TIVKUB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../llm-router/src/router.ts"],"sourcesContent":["import type { EmbeddingModel, LanguageModel } from \"ai\";\nimport { embedMany, generateText } from \"ai\";\nimport type { LlmConfig, LlmEmbeddingConfig, LlmProviderConfig } from \"@codegraph/shared\";\nimport { namespaceToString } from \"@codegraph/shared\";\nimport type { GenerateOptions, LlmRouter, ResolvedLlmConfig } from \"./types.js\";\n\nexport interface CreateRouterOptions {\n config: LlmConfig;\n env?: NodeJS.ProcessEnv;\n}\n\nexport async function createLlmRouter(opts: CreateRouterOptions): Promise<LlmRouter> {\n const env = opts.env ?? process.env;\n const resolved: ResolvedLlmConfig = {\n ...opts.config,\n embeddingNamespace: {\n provider: opts.config.embeddings.provider,\n model: opts.config.embeddings.model,\n dimension: opts.config.embeddings.dimension,\n },\n };\n\n const generationModel = await buildGenerationModel(opts.config, env);\n const embeddingModel = await buildEmbeddingModel(opts.config.embeddings, env);\n\n async function doEmbed(texts: string[]): Promise<number[][]> {\n if (texts.length === 0) return [];\n const { embeddings } = await embedMany({ model: embeddingModel, values: texts });\n return embeddings;\n }\n\n async function doGenerate(options: GenerateOptions): Promise<string> {\n const { text } = await generateText({\n model: generationModel,\n ...(options.system !== undefined ? { system: options.system } : {}),\n messages: options.messages,\n ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),\n ...(options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {}),\n });\n return text;\n }\n\n const router: LlmRouter = {\n config: resolved,\n embed: doEmbed,\n generate: doGenerate,\n async selfTest() {\n const embedStart = Date.now();\n const embeds = await doEmbed([\"codegraph self-test\"]);\n const embedLatencyMs = Date.now() - embedStart;\n const genStart = Date.now();\n const out = await doGenerate({\n messages: [{ role: \"user\", content: \"Reply with the single word ok.\" }],\n maxTokens: 5,\n temperature: 0,\n });\n const generateLatencyMs = Date.now() - genStart;\n return {\n embedDims: embeds[0]?.length ?? 0,\n generationOk: typeof out === \"string\" && out.length > 0,\n embedLatencyMs,\n generateLatencyMs,\n };\n },\n };\n return router;\n}\n\n/**\n * Used by the CLI's `config llm` printer; surfaces the resolved namespace\n * tag without forcing a provider import upstream.\n */\nexport function namespaceLabel(config: LlmConfig): string {\n return namespaceToString({\n provider: config.embeddings.provider,\n model: config.embeddings.model,\n dimension: config.embeddings.dimension,\n });\n}\n\nasync function buildGenerationModel(\n config: LlmConfig,\n env: NodeJS.ProcessEnv,\n): Promise<LanguageModel> {\n const provider = config.generation.provider;\n if (provider === \"openai\") return openaiText(config.generation, env, config.baseUrl);\n if (provider === \"anthropic\") return anthropicText(config.generation, env, config.baseUrl);\n if (provider === \"google\") return googleText(config.generation, env, config.baseUrl);\n if (provider === \"ollama\") return ollamaText(config.generation, env, config.baseUrl);\n throw new Error(`Unsupported generation provider: ${provider}`);\n}\n\nasync function buildEmbeddingModel(\n config: LlmEmbeddingConfig,\n env: NodeJS.ProcessEnv,\n): Promise<EmbeddingModel<string>> {\n if (config.provider === \"openai\") return openaiEmbedding(config, env);\n if (config.provider === \"google\") return googleEmbedding(config, env);\n if (config.provider === \"ollama\") return ollamaEmbedding(config, env);\n if (config.provider === \"anthropic\") {\n // Anthropic has no embedding endpoint; fall back to OpenAI so the\n // pipeline keeps working when generation is anthropic.\n return openaiEmbedding(\n { provider: \"openai\", model: \"text-embedding-3-small\", dimension: 1536 },\n env,\n );\n }\n throw new Error(`Unsupported embedding provider: ${config.provider}`);\n}\n\nasync function openaiText(\n config: LlmProviderConfig,\n env: NodeJS.ProcessEnv,\n baseUrl?: string,\n): Promise<LanguageModel> {\n const { createOpenAI } = await import(\"@ai-sdk/openai\");\n const openai = createOpenAI({\n ...(env.OPENAI_API_KEY ? { apiKey: env.OPENAI_API_KEY } : {}),\n ...(baseUrl ? { baseURL: baseUrl } : {}),\n });\n return openai(config.model);\n}\n\nasync function openaiEmbedding(\n config: LlmEmbeddingConfig,\n env: NodeJS.ProcessEnv,\n): Promise<EmbeddingModel<string>> {\n const { createOpenAI } = await import(\"@ai-sdk/openai\");\n const openai = createOpenAI(env.OPENAI_API_KEY ? { apiKey: env.OPENAI_API_KEY } : {});\n return openai.embedding(config.model);\n}\n\nasync function anthropicText(\n config: LlmProviderConfig,\n env: NodeJS.ProcessEnv,\n baseUrl?: string,\n): Promise<LanguageModel> {\n const { createAnthropic } = await import(\"@ai-sdk/anthropic\");\n const anthropic = createAnthropic({\n ...(env.ANTHROPIC_API_KEY ? { apiKey: env.ANTHROPIC_API_KEY } : {}),\n ...(baseUrl ? { baseURL: baseUrl } : {}),\n });\n return anthropic(config.model);\n}\n\nasync function googleText(\n config: LlmProviderConfig,\n env: NodeJS.ProcessEnv,\n baseUrl?: string,\n): Promise<LanguageModel> {\n const { createGoogleGenerativeAI } = await import(\"@ai-sdk/google\");\n const google = createGoogleGenerativeAI({\n ...(env.GOOGLE_GENERATIVE_AI_API_KEY\n ? { apiKey: env.GOOGLE_GENERATIVE_AI_API_KEY }\n : {}),\n ...(baseUrl ? { baseURL: baseUrl } : {}),\n });\n return google(config.model);\n}\n\nasync function googleEmbedding(\n config: LlmEmbeddingConfig,\n env: NodeJS.ProcessEnv,\n): Promise<EmbeddingModel<string>> {\n const { createGoogleGenerativeAI } = await import(\"@ai-sdk/google\");\n const google = createGoogleGenerativeAI(\n env.GOOGLE_GENERATIVE_AI_API_KEY\n ? { apiKey: env.GOOGLE_GENERATIVE_AI_API_KEY }\n : {},\n );\n return google.textEmbeddingModel(config.model);\n}\n\nasync function ollamaText(\n config: LlmProviderConfig,\n env: NodeJS.ProcessEnv,\n baseUrl?: string,\n): Promise<LanguageModel> {\n const { createOllama } = await import(\"ollama-ai-provider\");\n const base = baseUrl ?? env.OLLAMA_BASE_URL;\n const ollama = createOllama(base ? { baseURL: `${base.replace(/\\/$/, \"\")}/api` } : {});\n return ollama(config.model);\n}\n\nasync function ollamaEmbedding(\n config: LlmEmbeddingConfig,\n env: NodeJS.ProcessEnv,\n): Promise<EmbeddingModel<string>> {\n const { createOllama } = await import(\"ollama-ai-provider\");\n const base = env.OLLAMA_BASE_URL;\n const ollama = createOllama(base ? { baseURL: `${base.replace(/\\/$/, \"\")}/api` } : {});\n return ollama.embedding(config.model);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,359 @@
1
+ import {
2
+ EDGE_KINDS,
3
+ NODE_KINDS
4
+ } from "./chunk-XGPZDCQ4.js";
5
+
6
+ // ../graph-db/src/client.ts
7
+ import { mkdir } from "fs/promises";
8
+ import { homedir } from "os";
9
+ import { dirname, resolve } from "path";
10
+ import * as kuzu from "kuzu";
11
+
12
+ // ../graph-db/src/schema.ts
13
+ var SYMBOL_COLUMNS = [
14
+ "id",
15
+ "kind",
16
+ "repoId",
17
+ "name",
18
+ "path",
19
+ "lineStart",
20
+ "lineEnd",
21
+ "signature",
22
+ "leadingComment",
23
+ "isExported",
24
+ // File-specific
25
+ "language",
26
+ "sizeBytes",
27
+ "contentHash",
28
+ // Function-specific
29
+ "isAsync",
30
+ "isArrow",
31
+ // Route-specific
32
+ "method",
33
+ "routePath",
34
+ "framework",
35
+ // Embedding
36
+ "embeddingNamespace"
37
+ ];
38
+ var EDGE_COLUMNS = ["line"];
39
+ function buildSchemaStatements(opts) {
40
+ const columnDefs = [
41
+ "id STRING",
42
+ "kind STRING",
43
+ "repoId STRING",
44
+ "name STRING",
45
+ "path STRING",
46
+ "lineStart INT64",
47
+ "lineEnd INT64",
48
+ "signature STRING",
49
+ "leadingComment STRING",
50
+ "isExported BOOLEAN",
51
+ "language STRING",
52
+ "sizeBytes INT64",
53
+ "contentHash STRING",
54
+ "isAsync BOOLEAN",
55
+ "isArrow BOOLEAN",
56
+ "method STRING",
57
+ "routePath STRING",
58
+ "framework STRING",
59
+ "embeddingNamespace STRING",
60
+ `embedding FLOAT[${opts.embeddingDimension}]`,
61
+ "PRIMARY KEY (id)"
62
+ ];
63
+ const statements = [
64
+ `CREATE NODE TABLE IF NOT EXISTS Symbol(${columnDefs.join(", ")})`
65
+ ];
66
+ for (const kind of EDGE_KINDS) {
67
+ statements.push(
68
+ `CREATE REL TABLE IF NOT EXISTS ${kind}(FROM Symbol TO Symbol, line INT64)`
69
+ );
70
+ }
71
+ return statements;
72
+ }
73
+ function buildVectorIndexStatements() {
74
+ return [
75
+ "INSTALL VECTOR",
76
+ "LOAD EXTENSION VECTOR",
77
+ "CALL CREATE_VECTOR_INDEX('Symbol', 'embedding_idx', 'embedding', metric := 'cosine')"
78
+ ];
79
+ }
80
+ var DEFAULT_EMBEDDING_DIMENSION = 1536;
81
+
82
+ // ../graph-db/src/client.ts
83
+ function defaultDbPath() {
84
+ return resolve(homedir(), ".codegraph", "graph");
85
+ }
86
+ var GraphDb = class {
87
+ dbPath;
88
+ embeddingDimension;
89
+ db = null;
90
+ conn = null;
91
+ vectorIndexReady = false;
92
+ /**
93
+ * Cache of `conn.prepare()` results keyed by Cypher source. Kuzu's Node SDK requires a
94
+ * prepared statement for any parameterized query - reusing the prepared object keeps
95
+ * UNWIND-batched upserts fast.
96
+ */
97
+ preparedCache = /* @__PURE__ */ new Map();
98
+ constructor(opts = {}) {
99
+ this.dbPath = opts.dbPath ?? defaultDbPath();
100
+ this.embeddingDimension = opts.embeddingDimension ?? DEFAULT_EMBEDDING_DIMENSION;
101
+ }
102
+ async connect() {
103
+ if (this.conn) return;
104
+ await mkdir(dirname(this.dbPath), { recursive: true });
105
+ this.db = new kuzu.Database(this.dbPath);
106
+ this.conn = new kuzu.Connection(this.db);
107
+ }
108
+ async close() {
109
+ this.preparedCache.clear();
110
+ try {
111
+ this.conn?.close?.();
112
+ } catch {
113
+ }
114
+ try {
115
+ this.db?.close?.();
116
+ } catch {
117
+ }
118
+ this.conn = null;
119
+ this.db = null;
120
+ this.vectorIndexReady = false;
121
+ }
122
+ requireConn() {
123
+ if (!this.conn) {
124
+ throw new Error("GraphDb not connected. Call connect() first.");
125
+ }
126
+ return this.conn;
127
+ }
128
+ /**
129
+ * Idempotent migration. Runs all DDL through `IF NOT EXISTS` guards and tolerates the
130
+ * vector-index "already exists" error from re-runs.
131
+ */
132
+ async migrate() {
133
+ await this.connect();
134
+ const schemaStmts = buildSchemaStatements({ embeddingDimension: this.embeddingDimension });
135
+ for (const stmt of schemaStmts) {
136
+ await this.exec(stmt);
137
+ }
138
+ for (const stmt of buildVectorIndexStatements()) {
139
+ try {
140
+ await this.exec(stmt);
141
+ } catch (err) {
142
+ const message = err instanceof Error ? err.message : String(err);
143
+ if (isAlreadyExistsError(message)) continue;
144
+ if (/extension/i.test(message) && /(not found|missing|unsupported|disabled)/i.test(message)) {
145
+ console.warn(
146
+ `[codegraph] vector extension unavailable; semantic search disabled. Underlying: ${message}`
147
+ );
148
+ return;
149
+ }
150
+ throw new Error(`migrate failed on \`${stmt}\`: ${message}`);
151
+ }
152
+ }
153
+ this.vectorIndexReady = true;
154
+ }
155
+ /**
156
+ * Typed Cypher escape hatch.
157
+ *
158
+ * Kuzu returns BIGINT columns as native BigInt; we coerce to plain `number` when safe so
159
+ * downstream JSON serialization (MCP responses, snapshot tests) does not need bespoke
160
+ * handling.
161
+ */
162
+ async query(cypher, params = {}) {
163
+ const result = await this.runQuery(cypher, params);
164
+ const raw = await collectAll(result);
165
+ const data = raw.map((row) => normalizeRow(row));
166
+ const headers = raw.length > 0 ? Object.keys(raw[0] ?? {}) : [];
167
+ return { data, headers, metadata: [] };
168
+ }
169
+ /** Fire-and-forget DDL/exec. */
170
+ async exec(cypher, params = {}) {
171
+ await this.runQuery(cypher, params);
172
+ }
173
+ /**
174
+ * Bridge to Kuzu's two execution paths:
175
+ * - `conn.query(stmt)` for unparameterized statements (the second positional arg is a
176
+ * `progressCallback`, NOT params - mistaking that is the #1 way to confuse the API).
177
+ * - `conn.prepare(stmt) + conn.execute(prepared, params)` for anything with `$name`
178
+ * placeholders. We cache the prepared statement so UNWIND batches reuse it.
179
+ */
180
+ async runQuery(cypher, params) {
181
+ const conn = this.requireConn();
182
+ if (Object.keys(params).length === 0) {
183
+ return conn.query(cypher);
184
+ }
185
+ let prepared = this.preparedCache.get(cypher);
186
+ if (!prepared) {
187
+ prepared = await conn.prepare(cypher);
188
+ if (!prepared.isSuccess()) {
189
+ throw new Error(prepared.getErrorMessage());
190
+ }
191
+ this.preparedCache.set(cypher, prepared);
192
+ }
193
+ return conn.execute(prepared, params);
194
+ }
195
+ /**
196
+ * Upserts nodes via batched UNWIND + MERGE. Kuzu requires every column we SET to exist
197
+ * in the schema, so each row is normalized to include every `SYMBOL_COLUMNS` field (NULL
198
+ * for fields not present).
199
+ */
200
+ async upsertNodes(nodes) {
201
+ if (nodes.length === 0) return;
202
+ await this.connect();
203
+ const BATCH = 200;
204
+ const setClause = SYMBOL_COLUMNS.filter((c) => c !== "id").map((c) => `n.${c} = r.${c}`).join(", ");
205
+ const cypher = `UNWIND $batch AS r MERGE (n:Symbol {id: r.id}) SET ${setClause}`;
206
+ for (let i = 0; i < nodes.length; i += BATCH) {
207
+ const slice = nodes.slice(i, i + BATCH);
208
+ const payload = slice.map(buildSymbolRow);
209
+ await this.exec(cypher, { batch: payload });
210
+ }
211
+ }
212
+ /**
213
+ * Upserts edges. Both endpoints must already exist as `Symbol` nodes; rows where the
214
+ * MATCH fails are silently dropped, matching Cypher semantics.
215
+ *
216
+ * Uses CREATE because the orchestrator wipes the repo's slice before writing, so
217
+ * duplicates can't pre-exist within a single index pass.
218
+ */
219
+ async upsertEdges(edges) {
220
+ if (edges.length === 0) return;
221
+ await this.connect();
222
+ const byKind = /* @__PURE__ */ new Map();
223
+ for (const e of edges) {
224
+ const bucket = byKind.get(e.kind);
225
+ if (bucket) bucket.push(e);
226
+ else byKind.set(e.kind, [e]);
227
+ }
228
+ const BATCH = 500;
229
+ for (const [kind, batch] of byKind) {
230
+ const cypher = `UNWIND $batch AS r MATCH (a:Symbol {id: r.fromId}) MATCH (b:Symbol {id: r.toId}) CREATE (a)-[e:${kind} {line: r.line}]->(b)`;
231
+ for (let i = 0; i < batch.length; i += BATCH) {
232
+ const slice = batch.slice(i, i + BATCH);
233
+ const payload = slice.map((e) => ({
234
+ fromId: e.fromId,
235
+ toId: e.toId,
236
+ line: typeof e.line === "number" ? e.line : null
237
+ }));
238
+ await this.exec(cypher, { batch: payload });
239
+ }
240
+ }
241
+ }
242
+ /**
243
+ * Deletes all nodes (and incident edges via DETACH DELETE) for a repo. If `paths` is
244
+ * provided, restricts the delete to nodes whose `path` is in the list - used by
245
+ * incremental re-indexing.
246
+ */
247
+ async deleteByRepo(repoId, paths) {
248
+ await this.connect();
249
+ if (paths && paths.length > 0) {
250
+ await this.exec(
251
+ "MATCH (n:Symbol) WHERE n.repoId = $repoId AND n.path IN $paths DETACH DELETE n",
252
+ { repoId, paths }
253
+ );
254
+ return;
255
+ }
256
+ await this.exec("MATCH (n:Symbol) WHERE n.repoId = $repoId DETACH DELETE n", { repoId });
257
+ }
258
+ /**
259
+ * Returns counts of nodes (per kind) and edges (per kind) for a repo, plus the share of
260
+ * non-File nodes that carry an embedding.
261
+ */
262
+ async stats(repoId) {
263
+ await this.connect();
264
+ const nodes = {};
265
+ for (const kind of NODE_KINDS) {
266
+ const r = await this.query(
267
+ "MATCH (n:Symbol) WHERE n.repoId = $repoId AND n.kind = $kind RETURN count(n) AS count",
268
+ { repoId, kind }
269
+ );
270
+ nodes[kind] = Number(r.data[0]?.count ?? 0);
271
+ }
272
+ const edges = {};
273
+ for (const kind of EDGE_KINDS) {
274
+ const r = await this.query(
275
+ `MATCH (a:Symbol)-[r:${kind}]->(b:Symbol)
276
+ WHERE a.repoId = $repoId AND b.repoId = $repoId
277
+ RETURN count(r) AS count`,
278
+ { repoId }
279
+ );
280
+ edges[kind] = Number(r.data[0]?.count ?? 0);
281
+ }
282
+ const cov = await this.query(
283
+ `MATCH (n:Symbol)
284
+ WHERE n.repoId = $repoId AND n.kind <> 'File'
285
+ RETURN count(n) AS total,
286
+ count(n.embedding) AS embedded`,
287
+ { repoId }
288
+ );
289
+ const row = cov.data[0];
290
+ const total = Number(row?.total ?? 0);
291
+ const embedded = Number(row?.embedded ?? 0);
292
+ const coverage = total === 0 ? 0 : embedded / total;
293
+ return { nodes, edges, embeddingCoverage: coverage };
294
+ }
295
+ /** True once `migrate()` confirmed the vector extension is loaded. */
296
+ hasVectorIndex() {
297
+ return this.vectorIndexReady;
298
+ }
299
+ };
300
+ function buildSymbolRow(node) {
301
+ const src = node;
302
+ const row = {};
303
+ for (const col of SYMBOL_COLUMNS) {
304
+ const value = src[col];
305
+ row[col] = value === void 0 ? null : value;
306
+ }
307
+ row.id = node.id;
308
+ row.kind = node.kind;
309
+ return row;
310
+ }
311
+ function normalizeRow(row) {
312
+ if (row instanceof Map) {
313
+ const out = {};
314
+ for (const [k, v] of row) {
315
+ out[String(k)] = coerceValue(v);
316
+ }
317
+ return out;
318
+ }
319
+ if (row && typeof row === "object") {
320
+ const src = row;
321
+ const out = {};
322
+ for (const k of Object.keys(src)) {
323
+ out[k] = coerceValue(src[k]);
324
+ }
325
+ return out;
326
+ }
327
+ return { value: coerceValue(row) };
328
+ }
329
+ function coerceValue(value) {
330
+ if (typeof value === "bigint") {
331
+ if (value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(Number.MIN_SAFE_INTEGER)) {
332
+ return Number(value);
333
+ }
334
+ return value.toString();
335
+ }
336
+ if (Array.isArray(value)) return value.map(coerceValue);
337
+ return value;
338
+ }
339
+ async function collectAll(result) {
340
+ const target = Array.isArray(result) ? result[result.length - 1] : result;
341
+ if (!target) return [];
342
+ const getAll = target.getAll;
343
+ if (typeof getAll !== "function") return [];
344
+ return getAll.call(target);
345
+ }
346
+ function isAlreadyExistsError(message) {
347
+ return /already exists/i.test(message) || /already loaded/i.test(message) || /already installed/i.test(message) || /duplicate (table|index)/i.test(message);
348
+ }
349
+
350
+ export {
351
+ SYMBOL_COLUMNS,
352
+ EDGE_COLUMNS,
353
+ buildSchemaStatements,
354
+ buildVectorIndexStatements,
355
+ DEFAULT_EMBEDDING_DIMENSION,
356
+ defaultDbPath,
357
+ GraphDb
358
+ };
359
+ //# sourceMappingURL=chunk-F5QKPRNW.js.map