@openez-graph/cli 0.3.2 → 0.4.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/dist/cli.cjs CHANGED
@@ -11564,6 +11564,15 @@ var init_schema = __esm({
11564
11564
  });
11565
11565
 
11566
11566
  // ../../packages/db/src/sqlite/database-loader.ts
11567
+ function getRequireUrl() {
11568
+ try {
11569
+ if (typeof import_meta !== "undefined" && import_meta.url) {
11570
+ return import_meta.url;
11571
+ }
11572
+ } catch {
11573
+ }
11574
+ return `file://${__filename}`;
11575
+ }
11567
11576
  function tryResolveAddon() {
11568
11577
  if (resolvedAddon !== void 0) return resolvedAddon;
11569
11578
  try {
@@ -11600,9 +11609,7 @@ var init_database_loader = __esm({
11600
11609
  import_node_fs = __toESM(require("fs"), 1);
11601
11610
  import_node_module = __toESM(require("module"), 1);
11602
11611
  import_meta = {};
11603
- _require = typeof __non_webpack_require__ === "function" ? __non_webpack_require__ : import_node_module.default.createRequire(
11604
- typeof import_meta !== "undefined" && import_meta.url ? import_meta.url : `file://${__filename}`
11605
- );
11612
+ _require = typeof __non_webpack_require__ === "function" ? __non_webpack_require__ : import_node_module.default.createRequire(getRequireUrl());
11606
11613
  }
11607
11614
  });
11608
11615
 
@@ -11722,6 +11729,68 @@ function initializeWorkspaceSchema(sqlite) {
11722
11729
  CREATE INDEX IF NOT EXISTS idx_graph_edges_type ON graph_edges(type);
11723
11730
  CREATE INDEX IF NOT EXISTS idx_embeddings_chunk_id ON embeddings(chunk_id);
11724
11731
  `);
11732
+ sqlite.exec(`
11733
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
11734
+ chunk_id UNINDEXED,
11735
+ path,
11736
+ heading,
11737
+ language,
11738
+ search_text,
11739
+ content,
11740
+ tokenize = 'porter unicode61'
11741
+ );
11742
+ `);
11743
+ const ftsColumns = sqlite.prepare("PRAGMA table_info(chunks_fts)").all();
11744
+ if (!ftsColumns.some((column) => column.name === "search_text")) {
11745
+ sqlite.exec(`
11746
+ DROP TRIGGER IF EXISTS chunks_fts_insert;
11747
+ DROP TRIGGER IF EXISTS chunks_fts_delete;
11748
+ DROP TRIGGER IF EXISTS chunks_fts_update;
11749
+ DROP TABLE chunks_fts;
11750
+ CREATE VIRTUAL TABLE chunks_fts USING fts5(
11751
+ chunk_id UNINDEXED,
11752
+ path,
11753
+ heading,
11754
+ language,
11755
+ search_text,
11756
+ content,
11757
+ tokenize = 'porter unicode61'
11758
+ );
11759
+ `);
11760
+ }
11761
+ sqlite.exec(`
11762
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_insert AFTER INSERT ON chunks
11763
+ BEGIN
11764
+ INSERT INTO chunks_fts (chunk_id, path, heading, language, search_text, content)
11765
+ SELECT new.id, documents.path, coalesce(new.heading, ''),
11766
+ coalesce(documents.language, ''), coalesce(json_extract(new.metadata, '$.searchText'), ''), new.content
11767
+ FROM documents WHERE documents.id = new.document_id;
11768
+ END;
11769
+
11770
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_delete AFTER DELETE ON chunks
11771
+ BEGIN
11772
+ DELETE FROM chunks_fts WHERE chunk_id = old.id;
11773
+ END;
11774
+
11775
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_update AFTER UPDATE ON chunks
11776
+ BEGIN
11777
+ DELETE FROM chunks_fts WHERE chunk_id = old.id;
11778
+ INSERT INTO chunks_fts (chunk_id, path, heading, language, search_text, content)
11779
+ SELECT new.id, documents.path, coalesce(new.heading, ''),
11780
+ coalesce(documents.language, ''), coalesce(json_extract(new.metadata, '$.searchText'), ''), new.content
11781
+ FROM documents WHERE documents.id = new.document_id;
11782
+ END;
11783
+ `);
11784
+ sqlite.exec(`
11785
+ INSERT INTO chunks_fts (chunk_id, path, heading, language, search_text, content)
11786
+ SELECT chunks.id, documents.path, coalesce(chunks.heading, ''),
11787
+ coalesce(documents.language, ''), coalesce(json_extract(chunks.metadata, '$.searchText'), ''), chunks.content
11788
+ FROM chunks
11789
+ INNER JOIN documents ON documents.id = chunks.document_id
11790
+ WHERE NOT EXISTS (
11791
+ SELECT 1 FROM chunks_fts WHERE chunks_fts.chunk_id = chunks.id
11792
+ );
11793
+ `);
11725
11794
  }
11726
11795
  function getWorkspaceTableDefinitions() {
11727
11796
  return [
@@ -12081,8 +12150,9 @@ function createWorkspaceRepository(rootPath) {
12081
12150
  const existing = native.prepare("SELECT * FROM graph_nodes WHERE type = ? AND label = ?").get(input.type, input.label);
12082
12151
  if (existing) {
12083
12152
  const nextMetadata = input.metadata ?? String(existing.metadata ?? "{}");
12084
- const nextRefId = input.refId ?? existing.refId ?? null;
12085
- if (nextRefId !== existing.refId || nextMetadata !== existing.metadata) {
12153
+ const existingRefId = existing.ref_id ?? null;
12154
+ const nextRefId = input.refId ?? existingRefId;
12155
+ if (nextRefId !== existingRefId || nextMetadata !== existing.metadata) {
12086
12156
  native.prepare("UPDATE graph_nodes SET ref_id = ?, metadata = ?, updated_at = ? WHERE id = ?").run(nextRefId, nextMetadata, (/* @__PURE__ */ new Date()).toISOString(), existing.id);
12087
12157
  }
12088
12158
  return String(existing.id);
@@ -12133,36 +12203,59 @@ function createWorkspaceRepository(rootPath) {
12133
12203
  },
12134
12204
  // ── Full-Text Search ──
12135
12205
  async fullTextSearch(query, limit2) {
12136
- const likePattern = `%${query}%`;
12206
+ const ftsQuery = sanitizeFtsQuery(query);
12207
+ if (!ftsQuery) return [];
12137
12208
  const rows = native.prepare(
12138
- `SELECT chunks.id, chunks.content, chunks.heading, chunks.metadata, documents.path
12139
- FROM chunks
12209
+ `SELECT
12210
+ chunks.id, chunks.content, chunks.heading, chunks.metadata,
12211
+ documents.path,
12212
+ bm25(chunks_fts, 0, 4, 3, 1.5, 2, 1)
12213
+ * CASE
12214
+ WHEN documents.path LIKE 'tests/%' OR documents.path LIKE '%/__tests__/%' OR documents.path GLOB '*.test.*' THEN 0.8
12215
+ WHEN documents.kind = 'code' THEN 1.35
12216
+ ELSE 1
12217
+ END AS bm25_score
12218
+ FROM chunks_fts
12219
+ INNER JOIN chunks ON chunks.id = chunks_fts.chunk_id
12140
12220
  INNER JOIN documents ON documents.id = chunks.document_id
12141
- WHERE chunks.content LIKE ?
12221
+ WHERE chunks_fts MATCH ?
12222
+ ORDER BY bm25_score ASC
12142
12223
  LIMIT ?`
12143
- ).all(likePattern, limit2);
12144
- return rows.map((row) => ({
12145
- id: String(row.id),
12146
- path: String(row.path),
12147
- content: String(row.content),
12148
- score: 0.1,
12149
- heading: row.heading ? String(row.heading) : null,
12150
- metadata: safeParseJson(String(row.metadata ?? ""), {})
12151
- }));
12224
+ ).all(ftsQuery, limit2 * 5);
12225
+ const seenPaths = /* @__PURE__ */ new Set();
12226
+ return rows.map((row) => {
12227
+ const bm25 = Number(row.bm25_score ?? 0);
12228
+ const score = -bm25;
12229
+ return {
12230
+ id: String(row.id),
12231
+ path: String(row.path),
12232
+ content: String(row.content),
12233
+ score,
12234
+ heading: row.heading ? String(row.heading) : null,
12235
+ metadata: safeParseJson(String(row.metadata ?? ""), {})
12236
+ };
12237
+ }).filter((row) => {
12238
+ if (seenPaths.has(row.path)) return false;
12239
+ seenPaths.add(row.path);
12240
+ return true;
12241
+ }).slice(0, limit2);
12152
12242
  },
12153
12243
  // ── Graph Traversal ──
12154
- async graphNeighbors(label, depth) {
12155
- const seedNodes = native.prepare("SELECT * FROM graph_nodes WHERE label = ? LIMIT 1").all(label);
12244
+ async graphNeighbors(labelOrId, depth) {
12245
+ const seedNodes = native.prepare("SELECT * FROM graph_nodes WHERE id = ? OR label = ? ORDER BY id = ? DESC LIMIT 1").all(labelOrId, labelOrId, labelOrId);
12156
12246
  if (seedNodes.length === 0) {
12157
12247
  return { nodes: [], edges: [] };
12158
12248
  }
12159
12249
  const seedId = String(seedNodes[0].id);
12160
12250
  const visited = /* @__PURE__ */ new Set();
12161
- const resultNodes = [];
12251
+ const resultNodes = [
12252
+ { ...seedNodes[0], metadata: safeParseJson(String(seedNodes[0].metadata ?? ""), {}) }
12253
+ ];
12162
12254
  const resultEdges = [];
12255
+ const resultEdgeIds = /* @__PURE__ */ new Set();
12163
12256
  let currentBatch = [seedId];
12164
12257
  visited.add(seedId);
12165
- for (let hop = 0; hop <= depth; hop++) {
12258
+ for (let hop = 0; hop < Math.max(0, depth); hop++) {
12166
12259
  if (currentBatch.length === 0) break;
12167
12260
  const placeholders = currentBatch.map(() => "?").join(",");
12168
12261
  const edges = native.prepare(`SELECT * FROM graph_edges WHERE (from_node_id IN (${placeholders}) OR to_node_id IN (${placeholders}))`).all(...currentBatch, ...currentBatch);
@@ -12170,15 +12263,19 @@ function createWorkspaceRepository(rootPath) {
12170
12263
  for (const edge of edges) {
12171
12264
  const fromId = String(edge.from_node_id);
12172
12265
  const toId = String(edge.to_node_id);
12173
- if (!visited.has(fromId)) {
12266
+ if (!visited.has(fromId) && visited.size < 200) {
12174
12267
  nextBatch.push(fromId);
12175
12268
  visited.add(fromId);
12176
12269
  }
12177
- if (!visited.has(toId)) {
12270
+ if (!visited.has(toId) && visited.size < 200) {
12178
12271
  nextBatch.push(toId);
12179
12272
  visited.add(toId);
12180
12273
  }
12181
- resultEdges.push(edge);
12274
+ const edgeId = String(edge.id);
12275
+ if (!resultEdgeIds.has(edgeId)) {
12276
+ resultEdgeIds.add(edgeId);
12277
+ resultEdges.push(edge);
12278
+ }
12182
12279
  }
12183
12280
  for (const nodeId of nextBatch) {
12184
12281
  const node = native.prepare("SELECT * FROM graph_nodes WHERE id = ?").get(nodeId);
@@ -12188,9 +12285,6 @@ function createWorkspaceRepository(rootPath) {
12188
12285
  }
12189
12286
  currentBatch = nextBatch;
12190
12287
  }
12191
- if (seedNodes[0]) {
12192
- resultNodes.push({ ...seedNodes[0], metadata: safeParseJson(String(seedNodes[0].metadata ?? ""), {}) });
12193
- }
12194
12288
  return { nodes: resultNodes, edges: resultEdges };
12195
12289
  },
12196
12290
  // ── Memory Operations ──
@@ -12317,6 +12411,38 @@ function safeParseJson(value, fallback) {
12317
12411
  return fallback;
12318
12412
  }
12319
12413
  }
12414
+ function sanitizeFtsQuery(query) {
12415
+ const stopwords = /* @__PURE__ */ new Set([
12416
+ "a",
12417
+ "an",
12418
+ "are",
12419
+ "does",
12420
+ "extracted",
12421
+ "how",
12422
+ "implement",
12423
+ "implementation",
12424
+ "implemented",
12425
+ "in",
12426
+ "is",
12427
+ "of",
12428
+ "the",
12429
+ "to",
12430
+ "what",
12431
+ "where",
12432
+ "work"
12433
+ ]);
12434
+ const codeVerbs = {
12435
+ created: "create",
12436
+ generated: "generate",
12437
+ indexing: "index",
12438
+ selected: "select",
12439
+ stored: "store",
12440
+ written: "write"
12441
+ };
12442
+ const terms = (query.match(/[\p{L}\p{N}$]+/gu) ?? []).filter((t) => t.length > 1 && !stopwords.has(t.toLowerCase()));
12443
+ if (terms.length === 0) return "";
12444
+ return [...new Set(terms.map((term) => codeVerbs[term.toLowerCase()] ?? term))].map((term) => `"${term}"*`).join(" OR ");
12445
+ }
12320
12446
  var import_node_crypto;
12321
12447
  var init_repository = __esm({
12322
12448
  "../../packages/db/src/sqlite/repository.ts"() {
@@ -27434,6 +27560,27 @@ function truncateToTokenLimit(value, maxTokens) {
27434
27560
  return value.slice(0, approximateMaxChars);
27435
27561
  }
27436
27562
  }
27563
+ function splitToTokenLimit(value, maxTokens, overlapTokens = 0) {
27564
+ if (!value || maxTokens <= 0) return [];
27565
+ const overlap = Math.min(Math.max(0, overlapTokens), maxTokens - 1);
27566
+ try {
27567
+ const tokens = encode2(value);
27568
+ if (tokens.length <= maxTokens) return [value];
27569
+ const chunks2 = [];
27570
+ for (let start = 0; start < tokens.length; start += maxTokens - overlap) {
27571
+ chunks2.push(decode2(tokens.slice(start, start + maxTokens)));
27572
+ }
27573
+ return chunks2;
27574
+ } catch {
27575
+ const maxChars = maxTokens * 4;
27576
+ const overlapChars = overlap * 4;
27577
+ const chunks2 = [];
27578
+ for (let start = 0; start < value.length; start += maxChars - overlapChars) {
27579
+ chunks2.push(value.slice(start, start + maxChars));
27580
+ }
27581
+ return chunks2;
27582
+ }
27583
+ }
27437
27584
  var init_tokenizer = __esm({
27438
27585
  "../../packages/core/src/tokenizer.ts"() {
27439
27586
  "use strict";
@@ -27442,6 +27589,14 @@ var init_tokenizer = __esm({
27442
27589
  });
27443
27590
 
27444
27591
  // ../../packages/core/src/embeddings.ts
27592
+ function embeddingStorageModel(provider) {
27593
+ return `${provider.model}:openez-code-v1`;
27594
+ }
27595
+ function formatEmbeddingInput(provider, input, task) {
27596
+ const text2 = task === "query" ? input.content : [`path: ${input.path ?? ""}`, input.heading ? `heading: ${input.heading}` : "", input.content].filter(Boolean).join("\n");
27597
+ const nomicPrefix = provider.provider === "ollama" && provider.model.includes("nomic-embed-text") ? task === "query" ? "search_query: " : "search_document: " : "";
27598
+ return `${nomicPrefix}${text2}`;
27599
+ }
27445
27600
  function getEmbeddingProvider() {
27446
27601
  const env = loadEnv();
27447
27602
  if (env.EMBEDDING_PROVIDER === "none" || !env.EMBEDDING_PROVIDER) {
@@ -27548,13 +27703,14 @@ async function codeContext(input) {
27548
27703
  const edges = neighbors.edges.filter((edge) => edge.type === "calls");
27549
27704
  const relatedChunks = neighbors.nodes.filter((node) => node.type === "chunk");
27550
27705
  const symbol = neighbors.nodes.find(
27551
- (node) => node.type === "symbol" || node.label === input.symbolOrPath
27706
+ (node) => node.type === "symbol" && (node.label === input.symbolOrPath || node.id === input.symbolOrPath)
27552
27707
  );
27708
+ const symbolId = symbol ? String(symbol.id) : "";
27553
27709
  return {
27554
27710
  symbol,
27555
27711
  files,
27556
- callers: edges,
27557
- callees: edges,
27712
+ callers: edges.filter((edge) => String(edge.to_node_id) === symbolId),
27713
+ callees: edges.filter((edge) => String(edge.from_node_id) === symbolId),
27558
27714
  relatedChunks
27559
27715
  };
27560
27716
  }
@@ -27590,12 +27746,12 @@ var init_memory = __esm({
27590
27746
  });
27591
27747
 
27592
27748
  // ../../packages/core/src/rrf.ts
27593
- function reciprocalRankFusion(resultSets, k = 60) {
27749
+ function reciprocalRankFusion(resultSets, k = 60, weights = []) {
27594
27750
  const map = /* @__PURE__ */ new Map();
27595
- resultSets.forEach((resultSet) => {
27751
+ resultSets.forEach((resultSet, resultSetIndex) => {
27596
27752
  resultSet.forEach((entry, index2) => {
27597
27753
  const existing = map.get(entry.item.id);
27598
- const score = 1 / (k + index2 + 1);
27754
+ const score = (weights[resultSetIndex] ?? 1) / (k + index2 + 1);
27599
27755
  if (existing) {
27600
27756
  existing.score += score;
27601
27757
  } else {
@@ -27634,77 +27790,116 @@ function formatContextBlock(chunk) {
27634
27790
  return `[source: ${chunk.path}:${startLine}-${endLine} | score: ${chunk.score.toFixed(3)}]
27635
27791
  ${chunk.content}`;
27636
27792
  }
27637
- async function vectorSearch(rootPath, query, limit2) {
27638
- const provider = getEmbeddingProvider();
27639
- if (!provider) return [];
27640
- const [queryEmbedding] = await provider.embed([query]);
27641
- const queryDimensions = queryEmbedding.length;
27642
- const embeddingJson = JSON.stringify(queryEmbedding);
27793
+ function cosineSimilarity(left, right) {
27794
+ if (left.length === 0 || left.length !== right.length) return 0;
27795
+ let dot = 0;
27796
+ let leftNorm = 0;
27797
+ let rightNorm = 0;
27798
+ for (let index2 = 0; index2 < left.length; index2 += 1) {
27799
+ dot += left[index2] * right[index2];
27800
+ leftNorm += left[index2] * left[index2];
27801
+ rightNorm += right[index2] * right[index2];
27802
+ }
27803
+ return leftNorm === 0 || rightNorm === 0 ? 0 : dot / Math.sqrt(leftNorm * rightNorm);
27804
+ }
27805
+ function parseEmbedding(value) {
27806
+ try {
27807
+ const parsed = JSON.parse(String(value));
27808
+ return Array.isArray(parsed) && parsed.every((item) => typeof item === "number") ? parsed : [];
27809
+ } catch {
27810
+ return [];
27811
+ }
27812
+ }
27813
+ async function rankStoredEmbeddings(rootPath, provider, queryEmbedding, limit2) {
27814
+ if (queryEmbedding.length === 0) return [];
27643
27815
  const repo = createWorkspaceRepository(rootPath);
27644
27816
  const results = await repo.queryRaw(
27645
27817
  `SELECT
27646
27818
  chunks.id, chunks.content, chunks.heading, chunks.metadata,
27647
- documents.path
27819
+ documents.path, embeddings.embedding
27648
27820
  FROM embeddings
27649
27821
  INNER JOIN chunks ON chunks.id = embeddings.chunk_id
27650
27822
  INNER JOIN documents ON documents.id = chunks.document_id
27651
- WHERE embeddings.model = ?
27652
- AND embeddings.dimensions = ?
27653
- ORDER BY abs(length(embeddings.embedding) - ?) ASC
27654
- LIMIT ?`,
27655
- [provider.model, queryDimensions, embeddingJson.length, limit2]
27823
+ WHERE embeddings.provider = ?
27824
+ AND embeddings.model = ?
27825
+ AND embeddings.dimensions = ?`,
27826
+ [provider.provider, embeddingStorageModel(provider), queryEmbedding.length]
27656
27827
  );
27657
27828
  return results.map((row) => ({
27658
27829
  id: String(row.id),
27659
27830
  path: String(row.path),
27660
27831
  content: String(row.content),
27661
- score: 0.5,
27832
+ score: cosineSimilarity(queryEmbedding, parseEmbedding(row.embedding)),
27662
27833
  heading: row.heading ? String(row.heading) : null,
27663
27834
  metadata: safeParseJson2(String(row.metadata ?? "{}"), {})
27664
- }));
27835
+ })).sort((left, right) => right.score - left.score).slice(0, limit2);
27836
+ }
27837
+ async function vectorSearch(rootPath, query, limit2) {
27838
+ const provider = getEmbeddingProvider();
27839
+ if (!provider) return [];
27840
+ const [queryEmbedding] = await provider.embed([
27841
+ formatEmbeddingInput(provider, { content: query }, "query")
27842
+ ]);
27843
+ return rankStoredEmbeddings(rootPath, provider, queryEmbedding ?? [], limit2);
27665
27844
  }
27666
- async function graphExpand(rootPath, seedIds, limit2) {
27845
+ async function graphExpand(rootPath, seedIds, depth, limit2) {
27667
27846
  if (seedIds.length === 0) return [];
27668
27847
  const repo = createWorkspaceRepository(rootPath);
27669
27848
  const placeholders = seedIds.map(() => "?").join(",");
27670
27849
  const results = await repo.queryRaw(
27671
- `WITH seed_nodes AS (
27672
- SELECT id, ref_id
27850
+ `WITH RECURSIVE walk(node_id, depth) AS (
27851
+ SELECT id, 0
27673
27852
  FROM graph_nodes
27674
27853
  WHERE type = 'chunk'
27675
27854
  AND ref_id IN (${placeholders})
27676
- ),
27677
- neighbor_nodes AS (
27678
- SELECT DISTINCT
27855
+ UNION
27856
+ SELECT
27679
27857
  CASE
27680
- WHEN graph_edges.from_node_id = seed_nodes.id THEN graph_edges.to_node_id
27858
+ WHEN graph_edges.from_node_id = walk.node_id THEN graph_edges.to_node_id
27681
27859
  ELSE graph_edges.from_node_id
27682
- END AS node_id
27683
- FROM graph_edges
27684
- INNER JOIN seed_nodes
27685
- ON graph_edges.from_node_id = seed_nodes.id
27686
- OR graph_edges.to_node_id = seed_nodes.id
27860
+ END,
27861
+ walk.depth + 1
27862
+ FROM walk
27863
+ INNER JOIN graph_edges
27864
+ ON graph_edges.from_node_id = walk.node_id
27865
+ OR graph_edges.to_node_id = walk.node_id
27866
+ WHERE walk.depth < ?
27867
+ ),
27868
+ candidate_chunks AS (
27869
+ SELECT chunks.id, MIN(walk.depth) AS distance
27870
+ FROM walk
27871
+ INNER JOIN graph_nodes ON graph_nodes.id = walk.node_id
27872
+ INNER JOIN chunks ON chunks.id = graph_nodes.ref_id
27873
+ WHERE graph_nodes.type = 'chunk'
27874
+ AND walk.depth > 0
27875
+ AND chunks.id NOT IN (${placeholders})
27876
+ GROUP BY chunks.id
27877
+ ORDER BY distance
27687
27878
  LIMIT ?
27688
27879
  )
27689
27880
  SELECT
27690
27881
  chunks.id, chunks.content, chunks.heading, chunks.metadata,
27691
27882
  documents.path,
27692
- 0.15 AS score
27693
- FROM graph_nodes
27694
- INNER JOIN neighbor_nodes ON neighbor_nodes.node_id = graph_nodes.id
27695
- INNER JOIN chunks ON chunks.id = graph_nodes.ref_id
27883
+ 1.0 / (candidate_chunks.distance + 1) AS score
27884
+ FROM candidate_chunks
27885
+ INNER JOIN chunks ON chunks.id = candidate_chunks.id
27696
27886
  INNER JOIN documents ON documents.id = chunks.document_id
27697
- WHERE graph_nodes.type = 'chunk'`,
27698
- [...seedIds, limit2]
27887
+ ORDER BY candidate_chunks.distance, documents.path`,
27888
+ [...seedIds, depth, ...seedIds, limit2 * 5]
27699
27889
  );
27890
+ const seenPaths = /* @__PURE__ */ new Set();
27700
27891
  return results.map((row) => ({
27701
27892
  id: String(row.id),
27702
27893
  path: String(row.path),
27703
27894
  content: String(row.content),
27704
- score: Number(row.score ?? 0.15),
27895
+ score: Number(row.score ?? 0),
27705
27896
  heading: row.heading ? String(row.heading) : null,
27706
27897
  metadata: safeParseJson2(String(row.metadata ?? "{}"), {})
27707
- }));
27898
+ })).filter((row) => {
27899
+ if (seenPaths.has(row.path)) return false;
27900
+ seenPaths.add(row.path);
27901
+ return true;
27902
+ }).slice(0, limit2);
27708
27903
  }
27709
27904
  async function memoryQuery(input) {
27710
27905
  const registry2 = createRegistryRepository();
@@ -27721,29 +27916,34 @@ async function memoryQuery(input) {
27721
27916
  repo.fullTextSearch(input.query, retrieval.textLimit),
27722
27917
  vectorSearch(workspace.rootPath, input.query, retrieval.vectorLimit)
27723
27918
  ]);
27919
+ const primaryResults = ftsResults.length > 0 ? ftsResults : vectorResults;
27724
27920
  let fused = reciprocalRankFusion([
27725
- ftsResults.map((item) => ({ item, score: item.score })),
27726
- vectorResults.map((item) => ({ item, score: item.score }))
27921
+ primaryResults.map((item) => ({ item, score: item.score }))
27727
27922
  ]);
27728
27923
  if (!input.skipGraphExpand) {
27729
27924
  const graphResults = await graphExpand(
27730
27925
  workspace.rootPath,
27731
- fused.slice(0, finalLimit).map((entry) => entry.item.id),
27926
+ fused.slice(0, Math.min(finalLimit, 5)).map((entry) => entry.item.id),
27927
+ retrieval.graphHops,
27732
27928
  retrieval.maxGraphNeighbors
27733
27929
  );
27930
+ const fusedItemsByPath = new Map(fused.map((entry) => [entry.item.path, entry.item]));
27734
27931
  fused = reciprocalRankFusion([
27735
27932
  fused,
27736
- graphResults.map((item) => ({ item, score: item.score }))
27737
- ]);
27933
+ graphResults.map((item) => ({ item: fusedItemsByPath.get(item.path) ?? item, score: item.score }))
27934
+ ], 60, [1, 0.25]);
27738
27935
  }
27739
27936
  const selected = [];
27740
27937
  let usedTokens = 0;
27938
+ const chunksPerPath = /* @__PURE__ */ new Map();
27741
27939
  for (const entry of fused) {
27742
27940
  if (selected.length >= finalLimit) break;
27743
27941
  const tokenCount = countTokens2(entry.item.content);
27744
27942
  if (usedTokens + tokenCount > maxTokens) continue;
27745
- selected.push(entry.item);
27943
+ if (chunksPerPath.has(entry.item.path)) continue;
27944
+ selected.push({ ...entry.item, score: entry.score });
27746
27945
  usedTokens += tokenCount;
27946
+ chunksPerPath.set(entry.item.path, (chunksPerPath.get(entry.item.path) ?? 0) + 1);
27747
27947
  }
27748
27948
  const sources = selected.map((chunk) => sourceFromChunk(chunk, "retrieved-context"));
27749
27949
  await repo.insertQueryLog({
@@ -302258,6 +302458,10 @@ function getLineRange(node) {
302258
302458
  endLine: node.getEndLineNumber()
302259
302459
  };
302260
302460
  }
302461
+ function codeSearchText(text2) {
302462
+ const identifiers = text2.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
302463
+ return [...new Set(identifiers.flatMap((identifier) => identifier.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(" ").filter((term) => term.length > 1)))].slice(0, 256).join(" ");
302464
+ }
302261
302465
  function indexCode(content, filePath) {
302262
302466
  const project = new import_ts_morph.Project({
302263
302467
  useInMemoryFileSystem: true,
@@ -302269,6 +302473,7 @@ function indexCode(content, filePath) {
302269
302473
  const chunks2 = [];
302270
302474
  const definedSymbols = [];
302271
302475
  const calledIdentifiers = /* @__PURE__ */ new Set();
302476
+ const callExpressions = [];
302272
302477
  sourceFile.getImportDeclarations().forEach((declaration) => {
302273
302478
  declaration.getDescendantsOfKind(import_ts_morph.SyntaxKind.Identifier).forEach((identifier) => {
302274
302479
  if (identifier.getText()) {
@@ -302308,6 +302513,7 @@ function indexCode(content, filePath) {
302308
302513
  symbolType: symbolType2,
302309
302514
  metadata: {
302310
302515
  kind: "code",
302516
+ searchText: codeSearchText(text2),
302311
302517
  symbolName: name,
302312
302518
  symbolType: symbolType2,
302313
302519
  exported,
@@ -302319,6 +302525,7 @@ function indexCode(content, filePath) {
302319
302525
  const calledName = expression.getText();
302320
302526
  if (calledName) {
302321
302527
  calledIdentifiers.add(calledName);
302528
+ callExpressions.push({ callerName: name, calleeName: calledName.split(".").pop() ?? calledName });
302322
302529
  }
302323
302530
  });
302324
302531
  });
@@ -302339,6 +302546,7 @@ function indexCode(content, filePath) {
302339
302546
  symbolType: "variable",
302340
302547
  metadata: {
302341
302548
  kind: "code",
302549
+ searchText: codeSearchText(text2),
302342
302550
  symbolName: name,
302343
302551
  symbolType: "variable",
302344
302552
  exported,
@@ -302359,6 +302567,7 @@ function indexCode(content, filePath) {
302359
302567
  contentHash: hashContent(slice),
302360
302568
  metadata: {
302361
302569
  kind: "code",
302570
+ searchText: codeSearchText(slice),
302362
302571
  fallback: true,
302363
302572
  startLine: index2 + 1,
302364
302573
  endLine: Math.min(index2 + 80, lines.length)
@@ -302370,7 +302579,8 @@ function indexCode(content, filePath) {
302370
302579
  chunks: chunks2,
302371
302580
  importPaths: sourceFile.getImportDeclarations().map((declaration) => declaration.getModuleSpecifierValue()),
302372
302581
  definedSymbols,
302373
- calledIdentifiers: [...calledIdentifiers]
302582
+ calledIdentifiers: [...calledIdentifiers],
302583
+ callExpressions
302374
302584
  };
302375
302585
  }
302376
302586
  var import_ts_morph;
@@ -302572,13 +302782,37 @@ function inferDocumentKind(filePath) {
302572
302782
  }
302573
302783
  return { kind: "text", language: extension.slice(1) || null, extension };
302574
302784
  }
302785
+ function stripPythonAlias(value) {
302786
+ return value.trim().replace(/^\(+|\)+$/g, "").split(/\s+as\s+/i)[0]?.trim() ?? "";
302787
+ }
302788
+ function parsePythonImportLine(line) {
302789
+ const trimmed = line.trim().replace(/\s+#.*$/, "");
302790
+ const fromMatch = /^from\s+([.\w]+)\s+import\s+(.+)$/.exec(trimmed);
302791
+ if (fromMatch) {
302792
+ const modulePath = fromMatch[1];
302793
+ const importedNames = fromMatch[2].split(",").map(stripPythonAlias).filter((name) => name && name !== "*");
302794
+ return [
302795
+ modulePath,
302796
+ ...importedNames.map((name) => modulePath.endsWith(".") ? `${modulePath}${name}` : `${modulePath}.${name}`)
302797
+ ];
302798
+ }
302799
+ const importMatch = /^import\s+(.+)$/.exec(trimmed);
302800
+ if (!importMatch) return [];
302801
+ return importMatch[1].split(",").map(stripPythonAlias).filter(Boolean);
302802
+ }
302803
+ function normalizePythonCallName(value) {
302804
+ const parts = value.split(".").filter(Boolean);
302805
+ return parts[parts.length - 1] ?? value;
302806
+ }
302575
302807
  function parsePython(content) {
302576
302808
  const lines = content.split("\n");
302577
302809
  const definedSymbols = [];
302578
302810
  const importPaths = [];
302811
+ const calledIdentifiers = /* @__PURE__ */ new Set();
302812
+ const callExpressions = [];
302579
302813
  const symbolRegex = /^(?:async\s+)?(?:def|class)\s+(\w+)/;
302580
- const importRegex = /^(?:from\s+(\S+)\s+)?import\s+(\S+)/;
302581
302814
  const moduleDocstring = /^"""/;
302815
+ const callRegex = /(\w+(?:\.\w+)*)\s*\(/g;
302582
302816
  let inMultilineString = false;
302583
302817
  for (let i = 0; i < lines.length; i++) {
302584
302818
  const line = lines[i];
@@ -302594,29 +302828,37 @@ function parsePython(content) {
302594
302828
  }
302595
302829
  continue;
302596
302830
  }
302597
- const symbolMatch = symbolRegex.exec(line);
302598
- if (symbolMatch && line.trim().startsWith("def") || symbolMatch && line.trim().startsWith("class") || symbolMatch && line.trim().startsWith("async def") || symbolMatch && line.trim().startsWith("async class")) {
302831
+ const trimmed = line.trim();
302832
+ const symbolMatch = symbolRegex.exec(trimmed);
302833
+ if (symbolMatch) {
302599
302834
  const name = symbolMatch[1];
302600
- const isAsync2 = line.trim().startsWith("async");
302601
- const stripped = isAsync2 ? line.trim().slice(6) : line.trim();
302835
+ const isAsync2 = trimmed.startsWith("async");
302836
+ const stripped = isAsync2 ? trimmed.slice(6) : trimmed;
302602
302837
  const symbolType2 = stripped.startsWith("def") ? "function" : "class";
302603
302838
  const exported = !name.startsWith("_");
302604
302839
  const startLine = i + 1;
302605
302840
  const endLine = findBlockEnd(lines, i);
302606
302841
  const content2 = lines.slice(i, endLine).join("\n");
302607
302842
  definedSymbols.push({ name, symbolType: symbolType2, type: symbolType2, exported, startLine, endLine });
302843
+ const bodyContent = lines.slice(i, endLine).join("\n");
302844
+ let callMatch;
302845
+ const localCallRegex = new RegExp(callRegex);
302846
+ while ((callMatch = localCallRegex.exec(bodyContent)) !== null) {
302847
+ const rawCalledName = callMatch[1];
302848
+ const calledName = normalizePythonCallName(rawCalledName);
302849
+ if (!PYTHON_CALL_IGNORES.has(rawCalledName) && !PYTHON_CALL_IGNORES.has(calledName) && calledName !== name) {
302850
+ calledIdentifiers.add(calledName);
302851
+ callExpressions.push({ callerName: name, calleeName: calledName });
302852
+ }
302853
+ }
302608
302854
  }
302609
- const importMatch = importRegex.exec(line);
302610
- if (importMatch) {
302611
- const modulePath = importMatch[1] || importMatch[2];
302612
- importPaths.push(modulePath);
302613
- }
302855
+ importPaths.push(...parsePythonImportLine(line));
302614
302856
  }
302615
302857
  const chunks2 = createSymbolChunks(definedSymbols, lines, "python");
302616
302858
  if (chunks2.length === 0) {
302617
- return makeFallbackChunks(content, lines);
302859
+ return { ...makeFallbackChunks(content, lines), callExpressions: [] };
302618
302860
  }
302619
- return { chunks: chunks2, importPaths, definedSymbols, calledIdentifiers: [] };
302861
+ return { chunks: chunks2, importPaths: [...new Set(importPaths)], definedSymbols, calledIdentifiers: [...calledIdentifiers], callExpressions };
302620
302862
  }
302621
302863
  function parseGo(content) {
302622
302864
  const lines = content.split("\n");
@@ -302683,7 +302925,7 @@ function parseGo(content) {
302683
302925
  if (chunks2.length === 0) {
302684
302926
  return makeFallbackChunks(content, lines);
302685
302927
  }
302686
- return { chunks: chunks2, importPaths, definedSymbols, calledIdentifiers: [] };
302928
+ return { chunks: chunks2, importPaths, definedSymbols, calledIdentifiers: [], callExpressions: [] };
302687
302929
  }
302688
302930
  function parseRust(content) {
302689
302931
  const lines = content.split("\n");
@@ -302781,7 +303023,7 @@ function parseRust(content) {
302781
303023
  if (chunks2.length === 0) {
302782
303024
  return makeFallbackChunks(content, lines);
302783
303025
  }
302784
- return { chunks: chunks2, importPaths, definedSymbols, calledIdentifiers: [] };
303026
+ return { chunks: chunks2, importPaths, definedSymbols, calledIdentifiers: [], callExpressions: [] };
302785
303027
  }
302786
303028
  function indexConfig(content, language) {
302787
303029
  switch (language) {
@@ -303003,9 +303245,9 @@ function makeFallbackChunks(content, lines) {
303003
303245
  }
303004
303246
  });
303005
303247
  }
303006
- return { chunks: chunks2, importPaths: [], definedSymbols: [], calledIdentifiers: [] };
303248
+ return { chunks: chunks2, importPaths: [], definedSymbols: [], calledIdentifiers: [], callExpressions: [] };
303007
303249
  }
303008
- var import_node_path9, codeExtensions, configExtensions, markdownExtensions;
303250
+ var import_node_path9, codeExtensions, configExtensions, markdownExtensions, PYTHON_CALL_IGNORES;
303009
303251
  var init_languages = __esm({
303010
303252
  "../../packages/indexer/src/languages.ts"() {
303011
303253
  "use strict";
@@ -303032,6 +303274,30 @@ var init_languages = __esm({
303032
303274
  [".toml", "toml"]
303033
303275
  ]);
303034
303276
  markdownExtensions = /* @__PURE__ */ new Set([".md", ".mdx"]);
303277
+ PYTHON_CALL_IGNORES = /* @__PURE__ */ new Set([
303278
+ "if",
303279
+ "for",
303280
+ "while",
303281
+ "with",
303282
+ "return",
303283
+ "yield",
303284
+ "print",
303285
+ "len",
303286
+ "range",
303287
+ "str",
303288
+ "int",
303289
+ "float",
303290
+ "list",
303291
+ "dict",
303292
+ "set",
303293
+ "tuple",
303294
+ "bool",
303295
+ "isinstance",
303296
+ "issubclass",
303297
+ "super",
303298
+ "self",
303299
+ "cls"
303300
+ ]);
303035
303301
  }
303036
303302
  });
303037
303303
 
@@ -303104,7 +303370,10 @@ var init_scanner = __esm({
303104
303370
  "**/build/**",
303105
303371
  "**/coverage/**",
303106
303372
  "**/.turbo/**",
303107
- "**/.openez/**"
303373
+ "**/.openez/**",
303374
+ "**/pnpm-lock.yaml",
303375
+ "**/package-lock.json",
303376
+ "**/yarn.lock"
303108
303377
  ];
303109
303378
  }
303110
303379
  });
@@ -303141,24 +303410,68 @@ function createWorkspaceFileResolver(workspaceRoot, files) {
303141
303410
  }
303142
303411
  return null;
303143
303412
  }
303413
+ function resolvePythonModulePath(modulePath) {
303414
+ const basePath = normalizeRelativePath(modulePath.replace(/\./g, "/"));
303415
+ const directPath = `${basePath}.py`;
303416
+ if (knownRelativePaths.has(directPath)) return directPath;
303417
+ const initPath = `${basePath}/__init__.py`;
303418
+ if (knownRelativePaths.has(initPath)) return initPath;
303419
+ return null;
303420
+ }
303421
+ function resolvePythonRelativeImport(importerRelativePath, importPath) {
303422
+ const dotMatch = /^(\.+)(.*)$/.exec(importPath);
303423
+ if (!dotMatch) return null;
303424
+ const level = dotMatch[1].length;
303425
+ const remainder = dotMatch[2].replace(/^\./, "");
303426
+ let baseDirectory = normalizeRelativePath(import_node_path11.default.dirname(importerRelativePath));
303427
+ for (let index2 = 1; index2 < level; index2++) {
303428
+ baseDirectory = normalizeRelativePath(import_node_path11.default.dirname(baseDirectory));
303429
+ }
303430
+ const modulePath = remainder ? normalizeRelativePath(import_node_path11.default.join(baseDirectory, remainder.replace(/\./g, "/"))) : baseDirectory;
303431
+ return resolvePythonModulePath(modulePath);
303432
+ }
303433
+ function resolvePythonImport(importerRelativePath, importPath) {
303434
+ if (importPath.startsWith(".")) {
303435
+ const resolved = resolvePythonRelativeImport(importerRelativePath, importPath);
303436
+ if (resolved) return resolved;
303437
+ }
303438
+ return resolvePythonModulePath(importPath);
303439
+ }
303144
303440
  return {
303145
- resolveImport(importerRelativePath, importPath) {
303146
- if (!importPath.startsWith(".")) return null;
303147
- return resolveRelativeImport(importerRelativePath, importPath);
303441
+ resolveImport(importerRelativePath, importPath, language) {
303442
+ if (language === "python") {
303443
+ const resolved = resolvePythonImport(importerRelativePath, importPath);
303444
+ if (resolved) return resolved;
303445
+ }
303446
+ if (importPath.startsWith(".")) {
303447
+ return resolveRelativeImport(importerRelativePath, importPath);
303448
+ }
303449
+ return null;
303148
303450
  }
303149
303451
  };
303150
303452
  }
303151
303453
  async function resetDocumentArtifacts(repo, documentId) {
303152
303454
  const chunks2 = await repo.getChunksByDocument(documentId);
303153
303455
  const chunkIds = chunks2.map((c) => c.id);
303154
- const allNodeIds = [documentId, ...chunkIds];
303155
303456
  if (chunkIds.length > 0) {
303156
303457
  await repo.deleteEmbeddingsByChunkIds(chunkIds);
303157
303458
  }
303158
- await repo.deleteEdgesByNodeIds(allNodeIds);
303159
303459
  await repo.deleteGraphNodesByRefId(documentId);
303160
303460
  await repo.deleteChunksByDocument(documentId);
303161
303461
  }
303462
+ function boundChunks(chunks2, targetTokens, overlapTokens) {
303463
+ return chunks2.flatMap((chunk) => {
303464
+ const parts = splitToTokenLimit(chunk.content, targetTokens, overlapTokens);
303465
+ if (parts.length <= 1) return chunk;
303466
+ return parts.map((content, splitIndex) => ({
303467
+ ...chunk,
303468
+ content,
303469
+ tokenCount: countTokens2(content),
303470
+ contentHash: hashContent(content),
303471
+ metadata: { ...chunk.metadata, splitIndex, splitCount: parts.length }
303472
+ }));
303473
+ });
303474
+ }
303162
303475
  async function chunkDocument(input) {
303163
303476
  const info = inferDocumentKind(input.relativePath);
303164
303477
  if (info.kind === "markdown") {
@@ -303174,7 +303487,8 @@ async function chunkDocument(input) {
303174
303487
  importPaths: [],
303175
303488
  wikilinks: result.wikilinks,
303176
303489
  definedSymbols: [],
303177
- calledIdentifiers: []
303490
+ calledIdentifiers: [],
303491
+ callExpressions: []
303178
303492
  };
303179
303493
  }
303180
303494
  if (info.kind === "config") {
@@ -303186,7 +303500,8 @@ async function chunkDocument(input) {
303186
303500
  importPaths: [],
303187
303501
  wikilinks: [],
303188
303502
  definedSymbols: [],
303189
- calledIdentifiers: []
303503
+ calledIdentifiers: [],
303504
+ callExpressions: []
303190
303505
  };
303191
303506
  }
303192
303507
  if (info.kind === "code") {
@@ -303199,7 +303514,8 @@ async function chunkDocument(input) {
303199
303514
  importPaths: result.importPaths,
303200
303515
  wikilinks: [],
303201
303516
  definedSymbols: result.definedSymbols,
303202
- calledIdentifiers: result.calledIdentifiers
303517
+ calledIdentifiers: result.calledIdentifiers,
303518
+ callExpressions: result.callExpressions
303203
303519
  };
303204
303520
  }
303205
303521
  if (info.language === "python") {
@@ -303211,7 +303527,8 @@ async function chunkDocument(input) {
303211
303527
  importPaths: result.importPaths,
303212
303528
  wikilinks: [],
303213
303529
  definedSymbols: result.definedSymbols,
303214
- calledIdentifiers: result.calledIdentifiers
303530
+ calledIdentifiers: result.calledIdentifiers,
303531
+ callExpressions: result.callExpressions
303215
303532
  };
303216
303533
  }
303217
303534
  if (info.language === "go") {
@@ -303223,7 +303540,8 @@ async function chunkDocument(input) {
303223
303540
  importPaths: result.importPaths,
303224
303541
  wikilinks: [],
303225
303542
  definedSymbols: result.definedSymbols,
303226
- calledIdentifiers: result.calledIdentifiers
303543
+ calledIdentifiers: result.calledIdentifiers,
303544
+ callExpressions: result.callExpressions
303227
303545
  };
303228
303546
  }
303229
303547
  if (info.language === "rust") {
@@ -303235,7 +303553,8 @@ async function chunkDocument(input) {
303235
303553
  importPaths: result.importPaths,
303236
303554
  wikilinks: [],
303237
303555
  definedSymbols: result.definedSymbols,
303238
- calledIdentifiers: result.calledIdentifiers
303556
+ calledIdentifiers: result.calledIdentifiers,
303557
+ callExpressions: result.callExpressions
303239
303558
  };
303240
303559
  }
303241
303560
  const fallbackChunk2 = {
@@ -303256,7 +303575,8 @@ async function chunkDocument(input) {
303256
303575
  importPaths: [],
303257
303576
  wikilinks: [],
303258
303577
  definedSymbols: [],
303259
- calledIdentifiers: []
303578
+ calledIdentifiers: [],
303579
+ callExpressions: []
303260
303580
  };
303261
303581
  }
303262
303582
  const fallbackChunk = {
@@ -303276,19 +303596,33 @@ async function chunkDocument(input) {
303276
303596
  importPaths: [],
303277
303597
  wikilinks: [],
303278
303598
  definedSymbols: [],
303279
- calledIdentifiers: []
303599
+ calledIdentifiers: [],
303600
+ callExpressions: []
303280
303601
  };
303281
303602
  }
303282
- async function writeEmbeddingsToRepo(repo, chunkRows) {
303283
- const provider = getEmbeddingProvider();
303603
+ async function writeEmbeddingsToRepo(repo, chunkRows, provider) {
303284
303604
  if (!provider || chunkRows.length === 0) {
303285
303605
  return 0;
303286
303606
  }
303607
+ const existing = await repo.queryRaw(
303608
+ `SELECT chunk_id FROM embeddings
303609
+ WHERE provider = ? AND model = ? AND chunk_id IN (${chunkRows.map(() => "?").join(",")})`,
303610
+ [provider.provider, embeddingStorageModel(provider), ...chunkRows.map((chunk) => chunk.id)]
303611
+ );
303612
+ const existingIds = new Set(existing.map((row) => String(row.chunk_id)));
303613
+ const missingRows = chunkRows.filter((chunk) => !existingIds.has(chunk.id));
303614
+ if (missingRows.length === 0) return 0;
303287
303615
  try {
303288
- const vectors = await provider.embed(chunkRows.map((chunk) => chunk.content));
303616
+ const vectors = await provider.embed(
303617
+ missingRows.map((chunk) => formatEmbeddingInput(provider, chunk, "document"))
303618
+ );
303619
+ if (vectors.length !== missingRows.length) {
303620
+ console.error(`Embedding provider returned ${vectors.length} vectors for ${missingRows.length} chunks`);
303621
+ return 0;
303622
+ }
303289
303623
  const invalidEmbeddingIndex = vectors.findIndex((embedding) => embedding.length === 0);
303290
303624
  if (invalidEmbeddingIndex !== -1) {
303291
- console.error(`Embedding provider returned empty vector for chunk ${chunkRows[invalidEmbeddingIndex].id}`);
303625
+ console.error(`Embedding provider returned empty vector for chunk ${missingRows[invalidEmbeddingIndex].id}`);
303292
303626
  return 0;
303293
303627
  }
303294
303628
  const dimensions = vectors[0]?.length ?? 0;
@@ -303298,9 +303632,9 @@ async function writeEmbeddingsToRepo(repo, chunkRows) {
303298
303632
  }
303299
303633
  await repo.insertEmbeddings(
303300
303634
  vectors.map((embedding, index2) => ({
303301
- chunkId: chunkRows[index2].id,
303635
+ chunkId: missingRows[index2].id,
303302
303636
  provider: provider.provider,
303303
- model: provider.model,
303637
+ model: embeddingStorageModel(provider),
303304
303638
  dimensions,
303305
303639
  embedding: JSON.stringify(embedding)
303306
303640
  }))
@@ -303328,20 +303662,36 @@ async function indexWorkspace(input) {
303328
303662
  await writeLocalWorkspaceConfig(workspace);
303329
303663
  const repo = createWorkspaceRepository(workspace.rootPath);
303330
303664
  const settings = await getBrainSettings();
303665
+ const config2 = await loadBrainConfig(workspace.rootPath);
303666
+ const configuredWorkspace = config2.workspaces?.find(
303667
+ (candidate) => candidate.id === workspace.id || import_node_path11.default.resolve(candidate.root) === import_node_path11.default.resolve(workspace.rootPath)
303668
+ );
303669
+ const includeGlobs = workspace.includeGlobs || configuredWorkspace?.include.join("\n") || "";
303670
+ const excludeGlobs = workspace.excludeGlobs || configuredWorkspace?.exclude.join("\n") || "";
303671
+ const embeddingProvider = getEmbeddingProvider();
303331
303672
  const runMode = input.mode ?? "incremental";
303332
303673
  const reportProgress = async (message, progress) => {
303333
303674
  await input.onProgress?.({ message, progress });
303334
303675
  };
303335
- const runId = await repo.createIndexRun({ mode: runMode });
303336
303676
  if (runMode === "full") {
303337
303677
  await repo.resetAll();
303338
303678
  }
303679
+ const runId = await repo.createIndexRun({ mode: runMode });
303339
303680
  await reportProgress("Scanning workspace files...", 5);
303340
303681
  const files = await scanWorkspaceFiles({
303341
303682
  rootPath: workspace.rootPath,
303342
- include: workspace.includeGlobs || "",
303343
- exclude: workspace.excludeGlobs || ""
303683
+ include: includeGlobs,
303684
+ exclude: excludeGlobs
303344
303685
  });
303686
+ if (runMode === "incremental") {
303687
+ const scannedPaths = new Set(files.map((file) => file.relativePath));
303688
+ for (const document of await repo.listDocuments()) {
303689
+ if (!scannedPaths.has(document.path)) {
303690
+ await resetDocumentArtifacts(repo, document.id);
303691
+ await repo.deleteDocument(document.id);
303692
+ }
303693
+ }
303694
+ }
303345
303695
  const workspaceFileResolver = createWorkspaceFileResolver(
303346
303696
  workspace.rootPath,
303347
303697
  files.map((file) => ({
@@ -303352,6 +303702,8 @@ async function indexWorkspace(input) {
303352
303702
  let filesUpdated = 0;
303353
303703
  let chunksWritten = 0;
303354
303704
  let embeddingsWritten = 0;
303705
+ const symbolNodeIdsByName = /* @__PURE__ */ new Map();
303706
+ const pendingCallEdges = [];
303355
303707
  try {
303356
303708
  await reportProgress(
303357
303709
  files.length === 0 ? "No files matched the workspace filters" : `Queued ${files.length} file(s) for indexing`,
@@ -303363,7 +303715,19 @@ async function indexWorkspace(input) {
303363
303715
  const content = await import_promises7.default.readFile(file.absolutePath, "utf8");
303364
303716
  const contentHash = hashContent(content);
303365
303717
  const existingDocument = await repo.getDocumentByPath(file.relativePath);
303366
- if (runMode === "incremental" && existingDocument && existingDocument.contentHash === contentHash && existingDocument.mtimeMs === file.mtimeMs) {
303718
+ const existingChunks = existingDocument ? await repo.getChunksByDocument(existingDocument.id) : [];
303719
+ const unchanged = runMode === "incremental" && existingDocument && existingDocument.contentHash === contentHash && existingDocument.mtimeMs === file.mtimeMs;
303720
+ if (unchanged) {
303721
+ embeddingsWritten += await writeEmbeddingsToRepo(
303722
+ repo,
303723
+ existingChunks.map((chunk) => ({
303724
+ id: chunk.id,
303725
+ content: chunk.content,
303726
+ path: existingDocument.path,
303727
+ heading: chunk.heading
303728
+ })),
303729
+ embeddingProvider
303730
+ );
303367
303731
  continue;
303368
303732
  }
303369
303733
  const indexed = await chunkDocument({
@@ -303373,6 +303737,7 @@ async function indexWorkspace(input) {
303373
303737
  targetTokens: settings.chunking.targetTokens,
303374
303738
  overlapTokens: settings.chunking.overlapTokens
303375
303739
  });
303740
+ indexed.chunks = boundChunks(indexed.chunks, settings.chunking.targetTokens, settings.chunking.overlapTokens);
303376
303741
  let documentId;
303377
303742
  if (existingDocument) {
303378
303743
  await resetDocumentArtifacts(repo, existingDocument.id);
@@ -303450,10 +303815,11 @@ async function indexWorkspace(input) {
303450
303815
  toNodeId: chunkNodeId,
303451
303816
  type: "represented_by"
303452
303817
  });
303818
+ symbolNodeIdsByName.set(symbolName, symbolNodeId);
303453
303819
  }
303454
303820
  }
303455
303821
  for (const importPath of indexed.importPaths) {
303456
- const resolvedImportPath = workspaceFileResolver.resolveImport(file.relativePath, importPath);
303822
+ const resolvedImportPath = workspaceFileResolver?.resolveImport(file.relativePath, importPath, indexed.language ?? void 0);
303457
303823
  if (!resolvedImportPath) continue;
303458
303824
  const targetNodeId = await repo.upsertGraphNode({
303459
303825
  type: "file",
@@ -303479,43 +303845,33 @@ async function indexWorkspace(input) {
303479
303845
  type: "mentions"
303480
303846
  });
303481
303847
  }
303482
- if (indexed.definedSymbols.length > 0 && indexed.calledIdentifiers.length > 0) {
303483
- const symbolNodeIds = await Promise.all(
303484
- indexed.definedSymbols.map(
303485
- (symbol) => repo.upsertGraphNode({
303486
- type: "symbol",
303487
- label: symbol.name,
303488
- metadata: JSON.stringify({
303489
- symbolType: symbol.type,
303490
- exported: symbol.exported,
303491
- filePath: file.relativePath
303492
- })
303493
- })
303494
- )
303495
- );
303496
- for (const caller of indexed.calledIdentifiers) {
303497
- const callerIndex = indexed.definedSymbols.findIndex((s) => s.name === caller);
303498
- if (callerIndex === -1) continue;
303499
- const calleeNode = await repo.findGraphNode("symbol", caller);
303500
- if (calleeNode && symbolNodeIds[callerIndex] !== calleeNode.id) {
303501
- await repo.insertEdge({
303502
- fromNodeId: symbolNodeIds[callerIndex],
303503
- toNodeId: calleeNode.id,
303504
- type: "calls",
303505
- weight: 0.35,
303506
- metadata: JSON.stringify({ heuristic: true })
303507
- });
303508
- }
303509
- }
303510
- }
303848
+ pendingCallEdges.push(...indexed.callExpressions);
303511
303849
  const chunkRows = chunkIds.map((id, i) => ({
303512
303850
  id,
303513
- content: indexed.chunks[i].content
303851
+ content: indexed.chunks[i].content,
303852
+ path: file.relativePath,
303853
+ heading: indexed.chunks[i].heading
303514
303854
  }));
303515
- embeddingsWritten += await writeEmbeddingsToRepo(repo, chunkRows);
303855
+ embeddingsWritten += await writeEmbeddingsToRepo(repo, chunkRows, embeddingProvider);
303516
303856
  chunksWritten += chunkIds.length;
303517
303857
  filesUpdated += 1;
303518
303858
  }
303859
+ const insertedCallEdges = /* @__PURE__ */ new Set();
303860
+ for (const callExpression of pendingCallEdges) {
303861
+ const callerNodeId = symbolNodeIdsByName.get(callExpression.callerName) ?? (await repo.findGraphNode("symbol", callExpression.callerName))?.id;
303862
+ const calleeNodeId = symbolNodeIdsByName.get(callExpression.calleeName) ?? (await repo.findGraphNode("symbol", callExpression.calleeName))?.id;
303863
+ if (!callerNodeId || !calleeNodeId || callerNodeId === calleeNodeId) continue;
303864
+ const edgeKey = `${callerNodeId}:${calleeNodeId}:calls`;
303865
+ if (insertedCallEdges.has(edgeKey)) continue;
303866
+ insertedCallEdges.add(edgeKey);
303867
+ await repo.insertEdge({
303868
+ fromNodeId: callerNodeId,
303869
+ toNodeId: calleeNodeId,
303870
+ type: "calls",
303871
+ weight: 0.35,
303872
+ metadata: JSON.stringify({ heuristic: true, callee: callExpression.calleeName })
303873
+ });
303874
+ }
303519
303875
  await reportProgress("Finalizing index run...", 98);
303520
303876
  await repo.completeIndexRun(runId, {
303521
303877
  status: "completed",
@@ -303588,7 +303944,8 @@ var init_index_workspace = __esm({
303588
303944
  ".mts",
303589
303945
  ".cts",
303590
303946
  ".md",
303591
- ".mdx"
303947
+ ".mdx",
303948
+ ".py"
303592
303949
  ];
303593
303950
  }
303594
303951
  });
@@ -318760,6 +319117,15 @@ var init_mcp_bridge = __esm({
318760
319117
  });
318761
319118
 
318762
319119
  // ../web/src/server/sqlite.ts
319120
+ function getRequireUrl2() {
319121
+ try {
319122
+ if (typeof import_meta3 !== "undefined" && import_meta3.url) {
319123
+ return import_meta3.url;
319124
+ }
319125
+ } catch {
319126
+ }
319127
+ return `file://${__filename}`;
319128
+ }
318763
319129
  function safeParseJson3(value, fallback) {
318764
319130
  if (!value) return fallback;
318765
319131
  try {
@@ -319165,9 +319531,7 @@ var init_sqlite2 = __esm({
319165
319531
  import_node_path13 = __toESM(require("path"), 1);
319166
319532
  import_node_module2 = require("module");
319167
319533
  import_meta3 = {};
319168
- require2 = (0, import_node_module2.createRequire)(
319169
- typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : `file://${__filename}`
319170
- );
319534
+ require2 = (0, import_node_module2.createRequire)(getRequireUrl2());
319171
319535
  Database = require2("better-sqlite3");
319172
319536
  registryDb2 = null;
319173
319537
  workspaceDbs = /* @__PURE__ */ new Map();
@@ -319188,6 +319552,15 @@ var init_sqlite2 = __esm({
319188
319552
  });
319189
319553
 
319190
319554
  // ../web/src/server/index.ts
319555
+ function getDirname() {
319556
+ try {
319557
+ if (typeof import_meta4 !== "undefined" && import_meta4.url) {
319558
+ return import_node_path14.default.dirname(new URL(import_meta4.url).pathname);
319559
+ }
319560
+ } catch {
319561
+ }
319562
+ return typeof __dirname2 !== "undefined" ? __dirname2 : process.cwd();
319563
+ }
319191
319564
  function mapWorkspace2(ws) {
319192
319565
  return {
319193
319566
  id: ws.id,
@@ -319210,9 +319583,9 @@ function mapWorkspace2(ws) {
319210
319583
  };
319211
319584
  }
319212
319585
  function resolveWebDist() {
319213
- const sourceDist = import_node_path14.default.resolve(__dirname, "..", "dist");
319586
+ const sourceDist = import_node_path14.default.resolve(__dirname2, "..", "dist");
319214
319587
  if ((0, import_node_fs10.existsSync)(import_node_path14.default.join(sourceDist, "index.html"))) return sourceDist;
319215
- const cliDist = import_node_path14.default.resolve(__dirname, "web");
319588
+ const cliDist = import_node_path14.default.resolve(__dirname2, "web");
319216
319589
  if ((0, import_node_fs10.existsSync)(import_node_path14.default.join(cliDist, "index.html"))) return cliDist;
319217
319590
  return null;
319218
319591
  }
@@ -319228,7 +319601,7 @@ function createWebServer() {
319228
319601
  }
319229
319602
  return app;
319230
319603
  }
319231
- var import_serve_static, import_hono, import_cors, import_node_crypto3, import_node_fs10, import_node_path14, app, DEFAULT_INCLUDE_GLOBS, DEFAULT_EXCLUDE_GLOBS;
319604
+ var import_serve_static, import_hono, import_cors, import_node_crypto3, import_node_fs10, import_node_path14, import_meta4, __dirname2, app, DEFAULT_INCLUDE_GLOBS, DEFAULT_EXCLUDE_GLOBS;
319232
319605
  var init_server3 = __esm({
319233
319606
  "../web/src/server/index.ts"() {
319234
319607
  "use strict";
@@ -319241,11 +319614,21 @@ var init_server3 = __esm({
319241
319614
  init_sqlite2();
319242
319615
  init_src3();
319243
319616
  init_src();
319617
+ import_meta4 = {};
319618
+ __dirname2 = getDirname();
319244
319619
  app = new import_hono.Hono();
319245
- app.use("/*", (0, import_cors.cors)({
319246
- origin: ["http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:11368", "http://127.0.0.1:11368"],
319247
- credentials: true
319248
- }));
319620
+ app.use(
319621
+ "/*",
319622
+ (0, import_cors.cors)({
319623
+ origin: [
319624
+ "http://localhost:5173",
319625
+ "http://127.0.0.1:5173",
319626
+ "http://localhost:11368",
319627
+ "http://127.0.0.1:11368"
319628
+ ],
319629
+ credentials: true
319630
+ })
319631
+ );
319249
319632
  DEFAULT_INCLUDE_GLOBS = [
319250
319633
  "src/**/*.{ts,tsx,js,jsx}",
319251
319634
  "app/**/*.{ts,tsx}",
@@ -319276,7 +319659,13 @@ var init_server3 = __esm({
319276
319659
  if (!target) {
319277
319660
  return c.json({
319278
319661
  workspace: { id: "", name: "No workspace", root: "" },
319279
- stats: { documents: 0, chunks: 0, graphNodes: 0, graphEdges: 0, memories: 0 },
319662
+ stats: {
319663
+ documents: 0,
319664
+ chunks: 0,
319665
+ graphNodes: 0,
319666
+ graphEdges: 0,
319667
+ memories: 0
319668
+ },
319280
319669
  recentRuns: [],
319281
319670
  recentDocuments: [],
319282
319671
  recentMemories: [],
@@ -319303,7 +319692,13 @@ var init_server3 = __esm({
319303
319692
  console.error("Dashboard error:", err);
319304
319693
  return c.json({
319305
319694
  workspace: { id: "", name: "No workspace", root: "" },
319306
- stats: { documents: 0, chunks: 0, graphNodes: 0, graphEdges: 0, memories: 0 },
319695
+ stats: {
319696
+ documents: 0,
319697
+ chunks: 0,
319698
+ graphNodes: 0,
319699
+ graphEdges: 0,
319700
+ memories: 0
319701
+ },
319307
319702
  recentRuns: [],
319308
319703
  recentDocuments: [],
319309
319704
  recentMemories: [],
@@ -319343,10 +319738,14 @@ var init_server3 = __esm({
319343
319738
  if (!rootPath) return c.json({ valid: false, error: "Path is required" });
319344
319739
  try {
319345
319740
  const stats = await import_node_fs10.promises.stat(rootPath);
319346
- if (!stats.isDirectory()) return c.json({ valid: false, error: "Path is not a directory" });
319741
+ if (!stats.isDirectory())
319742
+ return c.json({ valid: false, error: "Path is not a directory" });
319347
319743
  return c.json({ valid: true });
319348
319744
  } catch {
319349
- return c.json({ valid: false, error: "Directory does not exist or is not accessible" });
319745
+ return c.json({
319746
+ valid: false,
319747
+ error: "Directory does not exist or is not accessible"
319748
+ });
319350
319749
  }
319351
319750
  });
319352
319751
  app.get("/api/workspaces", (c) => {
@@ -319370,7 +319769,11 @@ var init_server3 = __esm({
319370
319769
  return c.json({ ok: true, data });
319371
319770
  } catch (err) {
319372
319771
  const message = err instanceof Error ? err.message : String(err);
319373
- return c.json({ ok: false, error: message, dbPath: resolveRegistryDbPath2() });
319772
+ return c.json({
319773
+ ok: false,
319774
+ error: message,
319775
+ dbPath: resolveRegistryDbPath2()
319776
+ });
319374
319777
  }
319375
319778
  });
319376
319779
  app.get("/api/workspaces/:id", (c) => {
@@ -319388,19 +319791,28 @@ var init_server3 = __esm({
319388
319791
  return c.json({ ok: true, data });
319389
319792
  } catch (err) {
319390
319793
  const message = err instanceof Error ? err.message : String(err);
319391
- return c.json({ ok: false, error: message, dbPath: resolveRegistryDbPath2() });
319794
+ return c.json({
319795
+ ok: false,
319796
+ error: message,
319797
+ dbPath: resolveRegistryDbPath2()
319798
+ });
319392
319799
  }
319393
319800
  });
319394
319801
  app.post("/api/workspaces", async (c) => {
319395
319802
  try {
319396
319803
  const body = await c.req.json();
319397
319804
  const rootPath = body.rootPath;
319398
- if (!rootPath) return c.json({ success: false, error: "rootPath is required" });
319805
+ if (!rootPath)
319806
+ return c.json({ success: false, error: "rootPath is required" });
319399
319807
  try {
319400
319808
  const stats = await import_node_fs10.promises.stat(rootPath);
319401
- if (!stats.isDirectory()) return c.json({ success: false, error: "Path is not a directory" });
319809
+ if (!stats.isDirectory())
319810
+ return c.json({ success: false, error: "Path is not a directory" });
319402
319811
  } catch {
319403
- return c.json({ success: false, error: "Directory does not exist or is not accessible" });
319812
+ return c.json({
319813
+ success: false,
319814
+ error: "Directory does not exist or is not accessible"
319815
+ });
319404
319816
  }
319405
319817
  const ws = ensureRegistryWorkspace({
319406
319818
  name: body.name?.trim() || import_node_path14.default.basename(rootPath),
@@ -319410,7 +319822,11 @@ var init_server3 = __esm({
319410
319822
  });
319411
319823
  return c.json({
319412
319824
  success: true,
319413
- workspace: { ...mapWorkspace2(ws), latestIndexRun: null, latestGraphRun: null }
319825
+ workspace: {
319826
+ ...mapWorkspace2(ws),
319827
+ latestIndexRun: null,
319828
+ latestGraphRun: null
319829
+ }
319414
319830
  });
319415
319831
  } catch (err) {
319416
319832
  console.error("Failed to create workspace:", err);
@@ -319436,7 +319852,11 @@ var init_server3 = __esm({
319436
319852
  app.post("/api/workspaces/:id/index", async (c) => {
319437
319853
  const id = c.req.param("id");
319438
319854
  const ws = getRegistryWorkspace(id);
319439
- if (!ws) return c.json({ jobId: null, status: "error", error: "Workspace not found" }, 404);
319855
+ if (!ws)
319856
+ return c.json(
319857
+ { jobId: null, status: "error", error: "Workspace not found" },
319858
+ 404
319859
+ );
319440
319860
  const body = await c.req.json().catch(() => ({ mode: "incremental" }));
319441
319861
  updateRegistryWorkspace(id, {
319442
319862
  indexingStatus: "running",
@@ -319610,8 +320030,11 @@ function getThisDir() {
319610
320030
  if (typeof __dirname !== "undefined") {
319611
320031
  return __dirname;
319612
320032
  }
319613
- if (typeof import_meta4 !== "undefined" && import_meta4.dirname) {
319614
- return import_meta4.dirname;
320033
+ try {
320034
+ if (typeof import_meta5 !== "undefined" && import_meta5.dirname) {
320035
+ return import_meta5.dirname;
320036
+ }
320037
+ } catch {
319615
320038
  }
319616
320039
  return process.cwd();
319617
320040
  }
@@ -319643,13 +320066,13 @@ function resolveCliInvocation() {
319643
320066
  repoRoot
319644
320067
  };
319645
320068
  }
319646
- var import_node_fs11, import_node_path15, import_meta4;
320069
+ var import_node_fs11, import_node_path15, import_meta5;
319647
320070
  var init_resolve_cli = __esm({
319648
320071
  "src/resolve-cli.ts"() {
319649
320072
  "use strict";
319650
320073
  import_node_fs11 = __toESM(require("fs"), 1);
319651
320074
  import_node_path15 = __toESM(require("path"), 1);
319652
- import_meta4 = {};
320075
+ import_meta5 = {};
319653
320076
  }
319654
320077
  });
319655
320078
 
@@ -320773,7 +321196,7 @@ var {
320773
321196
  init_src();
320774
321197
  init_src4();
320775
321198
  var program2 = new Command();
320776
- program2.name("openez").description("OpenEZ Graph - Local-first knowledge retrieval system").version("0.3.0");
321199
+ program2.name("openez").description("OpenEZ Graph - Local-first knowledge retrieval system").version("0.4.0");
320777
321200
  program2.command("init").description("Initialize a workspace at the given path and run initial index").argument("[path]", "path to the project directory", process.cwd()).option("--no-index", "skip initial indexing").action(async (targetPath, options) => {
320778
321201
  const resolvedPath = import_node_path19.default.resolve(targetPath);
320779
321202
  if (!import_node_fs15.default.existsSync(resolvedPath)) {