ctxloom-pro 1.5.5 → 1.7.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.
@@ -79,6 +79,27 @@ var init_logger = __esm({
79
79
  // ../../packages/core/src/indexer/embedder.ts
80
80
  import fs3 from "fs";
81
81
  import path3 from "path";
82
+ function resolveEmbeddingModel(env = process.env) {
83
+ const envModel = env.CTXLOOM_EMBEDDING_MODEL?.trim();
84
+ if (!envModel) return MODEL_REGISTRY.minilm;
85
+ const registered = MODEL_REGISTRY[envModel];
86
+ if (registered) return registered;
87
+ const envDim = env.CTXLOOM_EMBEDDING_DIM ? Number.parseInt(env.CTXLOOM_EMBEDDING_DIM, 10) : NaN;
88
+ if (!Number.isFinite(envDim) || envDim <= 0) {
89
+ throw new Error(
90
+ `CTXLOOM_EMBEDDING_MODEL=${envModel} is not a known alias. Either use one of [${Object.keys(MODEL_REGISTRY).join(", ")}] or set CTXLOOM_EMBEDDING_DIM=<vector-length> alongside a raw HF id.`
91
+ );
92
+ }
93
+ return {
94
+ hfId: envModel,
95
+ dim: envDim,
96
+ // Without a known artifact size we can't enforce the truncated-download
97
+ // guard. Use 1 MB as the minimum; the worst case is a redundant retry
98
+ // rather than a hung process.
99
+ minBytes: 1024 * 1024,
100
+ description: `User-supplied model: ${envModel} (${envDim}-dim)`
101
+ };
102
+ }
82
103
  function collectFiles(dir, results = []) {
83
104
  const IGNORED_DIRS = /* @__PURE__ */ new Set([
84
105
  // Build artifacts + dependency caches
@@ -144,12 +165,37 @@ function collectFiles(dir, results = []) {
144
165
  }
145
166
  return results;
146
167
  }
147
- var MIN_MODEL_BYTES;
168
+ var MODEL_REGISTRY, ACTIVE_MODEL, EMBEDDING_DIMENSION, MODEL_ID, MIN_MODEL_BYTES;
148
169
  var init_embedder = __esm({
149
170
  "../../packages/core/src/indexer/embedder.ts"() {
150
171
  "use strict";
151
172
  init_logger();
152
- MIN_MODEL_BYTES = 80 * 1024 * 1024;
173
+ MODEL_REGISTRY = {
174
+ // The historical default. General English, 384-dim, ~90 MB.
175
+ // Kept as the free-tier default so existing users see zero change.
176
+ minilm: {
177
+ hfId: "sentence-transformers/all-MiniLM-L6-v2",
178
+ dim: 384,
179
+ minBytes: 80 * 1024 * 1024,
180
+ description: "General-purpose English sentence embedder (2020). 384-dim. The legacy default."
181
+ },
182
+ // Code-specific embedding model (Jina AI). 768-dim, ~140 MB.
183
+ // Empirically 20-40% better recall on code-similarity queries than
184
+ // MiniLM — the upgrade path recommended in the v1.7.0 analysis.
185
+ // Runs through the same @huggingface/transformers pipeline so the
186
+ // privacy story (fully local, no network at inference time) is
187
+ // preserved.
188
+ "jina-code": {
189
+ hfId: "jinaai/jina-embeddings-v2-base-code",
190
+ dim: 768,
191
+ minBytes: 130 * 1024 * 1024,
192
+ description: "Code-specific embedder (Jina, 2024). 768-dim. Better recall on code-similarity tasks."
193
+ }
194
+ };
195
+ ACTIVE_MODEL = resolveEmbeddingModel();
196
+ EMBEDDING_DIMENSION = ACTIVE_MODEL.dim;
197
+ MODEL_ID = ACTIVE_MODEL.hfId;
198
+ MIN_MODEL_BYTES = ACTIVE_MODEL.minBytes;
153
199
  }
154
200
  });
155
201
 
@@ -160,18 +206,19 @@ var init_VectorStore = __esm({
160
206
  "../../packages/core/src/db/VectorStore.ts"() {
161
207
  "use strict";
162
208
  init_logger();
209
+ init_embedder();
163
210
  }
164
211
  });
165
212
 
166
213
  // server/index.ts
167
214
  import express from "express";
168
215
  import cors from "cors";
169
- import path46 from "path";
170
- import fs34 from "fs";
216
+ import path47 from "path";
217
+ import fs35 from "fs";
171
218
  import { fileURLToPath as fileURLToPath2 } from "url";
172
219
 
173
220
  // server/loader.ts
174
- import path40 from "path";
221
+ import path41 from "path";
175
222
 
176
223
  // ../../packages/core/src/graph/DependencyGraph.ts
177
224
  import fs7 from "fs";
@@ -657,6 +704,13 @@ var ASTParser = class {
657
704
  extractTSNodes(rootNode, _filePath, lines) {
658
705
  const nodes = [];
659
706
  const processedIds = /* @__PURE__ */ new Set();
707
+ const hasCallableRight = (n) => {
708
+ const right = n.childForFieldName?.("right") ?? n.children[n.children.length - 1];
709
+ if (!right) return false;
710
+ if (right.type === "function" || right.type === "function_expression" || right.type === "arrow_function" || right.type === "function_declaration") return true;
711
+ if (right.type === "assignment_expression") return hasCallableRight(right);
712
+ return false;
713
+ };
660
714
  const walk = (node) => {
661
715
  if (processedIds.has(node.id)) return;
662
716
  switch (node.type) {
@@ -828,6 +882,56 @@ var ASTParser = class {
828
882
  processedIds.add(node.id);
829
883
  return;
830
884
  }
885
+ // ─── Prototype / object method assignments ──────────────────────
886
+ // CommonJS libraries (and pre-class-syntax ES) attach their public
887
+ // API via assignment expressions:
888
+ //
889
+ // res.send = function send(body) { ... }
890
+ // res.json = function (obj) { ... }
891
+ // res.contentType = res.type = function (type) { ... } // chained
892
+ // exports.foo = function foo() { ... }
893
+ // MyClass.prototype.bar = function () { ... }
894
+ //
895
+ // Without this case, none of those names enter the symbol index,
896
+ // so `lookupSymbolsByFile()` returns empty for libraries like
897
+ // express, and any downstream tool that wants to attribute callers
898
+ // to a file (blast-radius symbolCallers, ctx_get_definition, etc.)
899
+ // falls flat. The call graph itself already records callers of
900
+ // `send`/`json`/etc. — this case bridges the gap so we can match
901
+ // them back to the file that defines them.
902
+ //
903
+ // Heuristic:
904
+ // - left = member_expression → use the FINAL property as symbol
905
+ // - right = function | arrow_function | function_expression
906
+ // - right = assignment_expression → recurse for chained pattern
907
+ // Anything else (constants, identifiers being aliased) is skipped
908
+ // intentionally — those aren't callable API surface.
909
+ case "assignment_expression": {
910
+ const left = node.childForFieldName?.("left") ?? node.children.find(
911
+ (c) => c?.type === "member_expression" || c?.type === "identifier"
912
+ );
913
+ const right = node.childForFieldName?.("right") ?? node.children[node.children.length - 1];
914
+ if (left?.type === "member_expression" && right) {
915
+ const prop = left.childForFieldName?.("property") ?? left.children[left.children.length - 1];
916
+ const propName = prop?.text;
917
+ if (right.type === "assignment_expression") {
918
+ walk(right);
919
+ }
920
+ const rightIsCallable = right.type === "function" || right.type === "function_expression" || right.type === "arrow_function" || right.type === "function_declaration" || right.type === "assignment_expression" && hasCallableRight(right);
921
+ if (propName && rightIsCallable && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propName)) {
922
+ const sig = lines[node.startPosition.row] ?? "";
923
+ nodes.push({
924
+ type: "method",
925
+ name: propName,
926
+ signature: sig.trim().slice(0, 200),
927
+ startLine: node.startPosition.row + 1,
928
+ endLine: node.endPosition.row + 1
929
+ });
930
+ }
931
+ }
932
+ processedIds.add(node.id);
933
+ return;
934
+ }
831
935
  // ─── Lexical declarations (const fn = () => {}) ────────────────
832
936
  case "lexical_declaration": {
833
937
  for (const child of node.children) {
@@ -934,10 +1038,34 @@ var ASTParser = class {
934
1038
  }
935
1039
  case "import_from_statement": {
936
1040
  const moduleNode = node.children.find((c) => c?.type === "dotted_name" || c?.type === "relative_import");
1041
+ const sourceText = moduleNode?.text ?? "";
1042
+ const importedNames = [];
1043
+ let pastImportKeyword = false;
1044
+ for (const child of node.children) {
1045
+ if (!child) continue;
1046
+ if (child.text === "import" && (child.type === "import" || child.children.length === 0)) {
1047
+ pastImportKeyword = true;
1048
+ continue;
1049
+ }
1050
+ if (!pastImportKeyword) continue;
1051
+ if (child.type === "wildcard_import") continue;
1052
+ if (child.type === "dotted_name") {
1053
+ importedNames.push(child.text);
1054
+ } else if (child.type === "aliased_import") {
1055
+ const alias = child.childForFieldName?.("alias");
1056
+ const aliasName = alias?.text;
1057
+ if (aliasName) importedNames.push(aliasName);
1058
+ else {
1059
+ const name = child.childForFieldName?.("name")?.text;
1060
+ if (name) importedNames.push(name);
1061
+ }
1062
+ }
1063
+ }
937
1064
  nodes.push({
938
1065
  type: "import",
939
- name: moduleNode?.text ?? "",
940
- source: moduleNode?.text ?? "",
1066
+ name: sourceText,
1067
+ source: sourceText,
1068
+ importedNames: importedNames.length > 0 ? importedNames : void 0,
941
1069
  startLine: node.startPosition.row + 1,
942
1070
  endLine: node.endPosition.row + 1
943
1071
  });
@@ -1807,6 +1935,78 @@ var ASTParser = class {
1807
1935
  tree.delete();
1808
1936
  }
1809
1937
  }
1938
+ /**
1939
+ * Extract all call edges in a Python (.py / .ipynb) file. Mirrors
1940
+ * `parseAllCallEdges` but uses tree-sitter-python node types.
1941
+ *
1942
+ * Tree-sitter-python relevant shapes:
1943
+ * - call.function = identifier "foo" → callee = "foo"
1944
+ * - call.function = attribute (obj.method) → callee = "method"
1945
+ * - call.function = call (chained) → recurse into inner
1946
+ *
1947
+ * Enclosing-context tracking follows the same pattern: track the
1948
+ * innermost `function_definition`. Methods inside a class are also
1949
+ * function_definitions, so the same handler covers them.
1950
+ */
1951
+ async parseAllPythonCallEdges(filePath) {
1952
+ if (!this.pyLang) await this.loadPython();
1953
+ if (!this.pyLang) return [];
1954
+ const parser = this.getParser(this.pyLang);
1955
+ let source;
1956
+ try {
1957
+ const raw = fs2.readFileSync(filePath, "utf-8");
1958
+ source = filePath.endsWith(".ipynb") ? extractNotebookPythonSource(raw) : raw;
1959
+ } catch {
1960
+ return [];
1961
+ }
1962
+ if (!source.trim()) return [];
1963
+ const tree = parser.parse(source);
1964
+ if (!tree) return [];
1965
+ const results = [];
1966
+ const extractCalleeName = (fn) => {
1967
+ if (fn.type === "identifier") return fn.text;
1968
+ if (fn.type === "attribute") {
1969
+ const right = fn.childForFieldName?.("attribute") ?? fn.children[fn.children.length - 1];
1970
+ return right?.text ?? "";
1971
+ }
1972
+ if (fn.type === "call") {
1973
+ const innerFn = fn.childForFieldName?.("function");
1974
+ return innerFn ? extractCalleeName(innerFn) : "";
1975
+ }
1976
+ return "";
1977
+ };
1978
+ const walk = (node, contextStack) => {
1979
+ let newStack = contextStack;
1980
+ if (node.type === "function_definition") {
1981
+ const nameNode = node.childForFieldName?.("name");
1982
+ if (nameNode?.text) {
1983
+ newStack = [...contextStack, nameNode.text];
1984
+ }
1985
+ }
1986
+ if (node.type === "call") {
1987
+ const fn = node.childForFieldName?.("function") ?? node.children.find((c) => c?.type === "identifier" || c?.type === "attribute" || c?.type === "call");
1988
+ if (fn) {
1989
+ const name = extractCalleeName(fn);
1990
+ if (name && name.length > 0) {
1991
+ results.push({
1992
+ callerSymbol: newStack[newStack.length - 1] ?? "",
1993
+ calleeSymbol: name,
1994
+ line: node.startPosition.row + 1
1995
+ });
1996
+ }
1997
+ }
1998
+ }
1999
+ for (const child of node.children) {
2000
+ if (child) walk(child, newStack);
2001
+ }
2002
+ };
2003
+ try {
2004
+ walk(tree.rootNode, []);
2005
+ return results;
2006
+ } finally {
2007
+ tree.delete();
2008
+ }
2009
+ }
1810
2010
  /**
1811
2011
  * Extract all call edges in a TypeScript/TSX file.
1812
2012
  * Tracks the enclosing function/method context for each call site.
@@ -1893,48 +2093,100 @@ var GoModuleResolver = class {
1893
2093
  }
1894
2094
  /**
1895
2095
  * Resolve a module-path import (e.g. `github.com/myorg/myapp/internal/auth`)
1896
- * to a relative project path (e.g. `internal/auth/auth.go`).
2096
+ * to the FIRST relative project path (e.g. `internal/auth/auth.go`).
1897
2097
  *
1898
2098
  * Returns null for:
1899
2099
  * - Third-party imports (different module prefix)
1900
2100
  * - Relative imports (use resolveRelative() instead)
1901
2101
  * - Imports where no .go files are found
2102
+ *
2103
+ * NOTE: A Go import imports a PACKAGE (a directory of .go files), not a
2104
+ * single file. Use `resolveAll()` to get every file in the package — that
2105
+ * matches Go's compile-unit semantics and is what the graph wants for
2106
+ * accurate reachability. `resolve()` is kept for back-compat callers
2107
+ * that only need a representative file.
1902
2108
  */
1903
2109
  resolve(importPath) {
1904
- if (!this.modulePath) return null;
1905
- if (importPath.startsWith(".")) return null;
1906
- if (!importPath.startsWith(this.modulePath)) return null;
2110
+ const all = this.resolveAll(importPath);
2111
+ return all[0] ?? null;
2112
+ }
2113
+ /**
2114
+ * Resolve a module-path import to ALL non-test .go files in the target
2115
+ * package directory. This matches Go's compile-unit semantics: a single
2116
+ * `import "github.com/foo/bar/pkg"` statement brings the entire `pkg/`
2117
+ * directory into the dependency graph — every exported symbol from
2118
+ * every .go file in that directory is accessible to the caller.
2119
+ *
2120
+ * Pre-fix the resolver returned only ONE file per import, which made
2121
+ * gin's `binding/` package (~20 files) appear to be a single file from
2122
+ * the graph's perspective. The bench's graphReachability on gin
2123
+ * collapsed to 0.32 because PRs that touched 4 files in `binding/`
2124
+ * had only ONE of them in the graph reach of the entry-point file.
2125
+ * Returning ALL package files fixes the structural model.
2126
+ *
2127
+ * Test files (_test.go) are intentionally excluded — they're not part
2128
+ * of a package's public API and aren't imported by callers. The
2129
+ * test↔source link is handled separately by per-directory sibling
2130
+ * edges (see DependencyGraph's Go intra-package pass).
2131
+ */
2132
+ resolveAll(importPath) {
2133
+ if (!this.modulePath) return [];
2134
+ if (importPath.startsWith(".")) return [];
2135
+ if (!importPath.startsWith(this.modulePath)) return [];
1907
2136
  const suffix = importPath.slice(this.modulePath.length);
1908
- if (!suffix) return null;
2137
+ if (!suffix) return [];
1909
2138
  const subPath = suffix.startsWith("/") ? suffix.slice(1) : suffix;
1910
2139
  const absDir = path4.join(this.rootDir, subPath);
1911
- return this.firstGoFileInDir(absDir, subPath);
2140
+ return this.allGoFilesInDir(
2141
+ absDir,
2142
+ subPath,
2143
+ /* includeTests */
2144
+ false
2145
+ );
1912
2146
  }
1913
2147
  /**
1914
2148
  * Resolve a relative import (`./config`, `../pkg`) from a given Go source file.
1915
2149
  * Returns the relative project path to the first .go file found, or null.
2150
+ * Kept for back-compat — most callers should prefer `resolveRelativeAll()`.
1916
2151
  */
1917
2152
  resolveRelative(fromFile, importSpec) {
2153
+ const all = this.resolveRelativeAll(fromFile, importSpec);
2154
+ return all[0] ?? null;
2155
+ }
2156
+ /**
2157
+ * Resolve a relative import to ALL non-test .go files in the target
2158
+ * package directory. See `resolveAll()` for rationale.
2159
+ */
2160
+ resolveRelativeAll(fromFile, importSpec) {
1918
2161
  const fromDir = path4.dirname(fromFile);
1919
2162
  const absTarget = path4.resolve(fromDir, importSpec);
1920
2163
  const subPath = path4.relative(this.rootDir, absTarget);
1921
- return this.firstGoFileInDir(absTarget, subPath);
2164
+ return this.allGoFilesInDir(
2165
+ absTarget,
2166
+ subPath,
2167
+ /* includeTests */
2168
+ false
2169
+ );
1922
2170
  }
1923
- firstGoFileInDir(absDir, relDir) {
1924
- if (!fs4.existsSync(absDir)) return null;
2171
+ /**
2172
+ * Enumerate every .go file in a directory, returning project-relative
2173
+ * paths sorted with non-test files first. Used by both the single-file
2174
+ * and all-files resolvers.
2175
+ */
2176
+ allGoFilesInDir(absDir, relDir, includeTests) {
2177
+ if (!fs4.existsSync(absDir)) return [];
1925
2178
  let entries;
1926
2179
  try {
1927
2180
  entries = fs4.readdirSync(absDir);
1928
2181
  } catch {
1929
- return null;
2182
+ return [];
1930
2183
  }
1931
- const goFiles = entries.filter((f) => f.endsWith(".go")).sort((a, b) => {
2184
+ const goFiles = entries.filter((f) => f.endsWith(".go")).filter((f) => includeTests || !f.endsWith("_test.go")).sort((a, b) => {
1932
2185
  const aTest = a.endsWith("_test.go") ? 1 : 0;
1933
2186
  const bTest = b.endsWith("_test.go") ? 1 : 0;
1934
2187
  return aTest - bTest || a.localeCompare(b);
1935
2188
  });
1936
- if (goFiles.length === 0) return null;
1937
- return path4.join(relDir, goFiles[0]).replace(/\\/g, "/");
2189
+ return goFiles.map((f) => path4.join(relDir, f).replace(/\\/g, "/"));
1938
2190
  }
1939
2191
  };
1940
2192
 
@@ -1976,6 +2228,24 @@ function extractImports(filePath, content) {
1976
2228
  return extractNotebookImports(filePath, content);
1977
2229
  case ".vue":
1978
2230
  return extractVueImports(content);
2231
+ case ".c":
2232
+ case ".cc":
2233
+ case ".cpp":
2234
+ case ".cxx":
2235
+ case ".h":
2236
+ case ".hh":
2237
+ case ".hpp":
2238
+ case ".hxx":
2239
+ return extractCppImports(content);
2240
+ case ".scala":
2241
+ return extractScalaImports(content);
2242
+ case ".lua":
2243
+ return extractLuaImports(content);
2244
+ case ".ex":
2245
+ case ".exs":
2246
+ return extractElixirImports(content);
2247
+ case ".zig":
2248
+ return extractZigImports(content);
1979
2249
  default:
1980
2250
  return [];
1981
2251
  }
@@ -1995,8 +2265,14 @@ function resolveImport(fromAbs, raw, rootDir) {
1995
2265
  if (ext === ".dart") return resolveDartImport(fromAbs, fromDir, raw, rootDir);
1996
2266
  if (ext === ".ipynb") return resolvePythonImport(fromAbs, fromDir, raw, rootDir);
1997
2267
  if (ext === ".vue") return resolveVueImport(fromAbs, fromDir, raw, rootDir);
2268
+ if (CPP_EXTENSIONS.has(ext)) return resolveCppImport(fromDir, raw, rootDir);
2269
+ if (ext === ".scala") return resolveScalaImport(fromDir, raw, rootDir);
2270
+ if (ext === ".lua") return resolveLuaImport(fromDir, raw, rootDir);
2271
+ if (ext === ".ex" || ext === ".exs") return resolveElixirImport(fromDir, raw, rootDir);
2272
+ if (ext === ".zig") return resolveZigImport(fromAbs, fromDir, raw, rootDir);
1998
2273
  return null;
1999
2274
  }
2275
+ var CPP_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"]);
2000
2276
  function extractPythonImports(content) {
2001
2277
  const results = [];
2002
2278
  let m;
@@ -2283,6 +2559,124 @@ function resolveVueImport(fromAbs, fromDir, raw, rootDir) {
2283
2559
  }
2284
2560
  return null;
2285
2561
  }
2562
+ function extractCppImports(content) {
2563
+ const results = [];
2564
+ const localInclude = /^\s*#\s*include\s+"([^"]+)"/gm;
2565
+ let m;
2566
+ while ((m = localInclude.exec(content)) !== null) {
2567
+ results.push({ specifier: m[1], isRelative: true });
2568
+ }
2569
+ return results;
2570
+ }
2571
+ function resolveCppImport(fromDir, raw, rootDir) {
2572
+ const rootResolved = path5.resolve(rootDir);
2573
+ const candidates = [
2574
+ path5.resolve(fromDir, raw.specifier),
2575
+ path5.resolve(rootDir, raw.specifier)
2576
+ ];
2577
+ for (const c of candidates) {
2578
+ if (!c.startsWith(rootResolved + path5.sep) && c !== rootResolved) continue;
2579
+ if (fs5.existsSync(c)) return path5.relative(rootDir, c);
2580
+ }
2581
+ return null;
2582
+ }
2583
+ function extractScalaImports(content) {
2584
+ const results = [];
2585
+ const importRe = /^\s*import\s+((?:\w+\.)*\w+)(?:\.\{[^}]+\})?/gm;
2586
+ let m;
2587
+ while ((m = importRe.exec(content)) !== null) {
2588
+ results.push({ specifier: m[1], isRelative: false });
2589
+ }
2590
+ return results;
2591
+ }
2592
+ function resolveScalaImport(fromDir, raw, rootDir) {
2593
+ const asPath = raw.specifier.replace(/\./g, path5.sep);
2594
+ const candidates = [
2595
+ path5.join(rootDir, "src", "main", "scala", asPath + ".scala"),
2596
+ path5.join(rootDir, asPath + ".scala")
2597
+ ];
2598
+ for (const c of candidates) {
2599
+ if (fs5.existsSync(c)) return path5.relative(rootDir, c);
2600
+ }
2601
+ const className = raw.specifier.split(".").pop() ?? raw.specifier;
2602
+ const local = path5.join(fromDir, className + ".scala");
2603
+ if (fs5.existsSync(local)) return path5.relative(rootDir, local);
2604
+ return null;
2605
+ }
2606
+ function extractLuaImports(content) {
2607
+ const results = [];
2608
+ const requireRe = /\brequire\s*\(?\s*['"]([^'"]+)['"]/g;
2609
+ let m;
2610
+ while ((m = requireRe.exec(content)) !== null) {
2611
+ results.push({ specifier: m[1], isRelative: false });
2612
+ }
2613
+ return results;
2614
+ }
2615
+ function resolveLuaImport(fromDir, raw, rootDir) {
2616
+ const asPath = raw.specifier.replace(/\./g, path5.sep);
2617
+ const candidates = [
2618
+ path5.join(fromDir, asPath + ".lua"),
2619
+ path5.join(rootDir, asPath + ".lua"),
2620
+ // Lua's package convention also supports init.lua as a directory
2621
+ // entry point — analogous to Python's __init__.py.
2622
+ path5.join(rootDir, asPath, "init.lua")
2623
+ ];
2624
+ for (const c of candidates) {
2625
+ if (fs5.existsSync(c)) return path5.relative(rootDir, c);
2626
+ }
2627
+ return null;
2628
+ }
2629
+ function extractElixirImports(content) {
2630
+ const results = [];
2631
+ const re = /^\s*(?:alias|import|use|require)\s+([A-Z][\w.]*)/gm;
2632
+ let m;
2633
+ while ((m = re.exec(content)) !== null) {
2634
+ results.push({ specifier: m[1], isRelative: false });
2635
+ }
2636
+ return results;
2637
+ }
2638
+ function resolveElixirImport(fromDir, raw, rootDir) {
2639
+ const segments = raw.specifier.split(".").map(
2640
+ (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()
2641
+ );
2642
+ const asPath = segments.join(path5.sep);
2643
+ const candidates = [
2644
+ path5.join(rootDir, "lib", asPath + ".ex"),
2645
+ path5.join(rootDir, "lib", asPath + ".exs"),
2646
+ path5.join(rootDir, asPath + ".ex"),
2647
+ path5.join(rootDir, asPath + ".exs")
2648
+ ];
2649
+ for (const c of candidates) {
2650
+ if (fs5.existsSync(c)) return path5.relative(rootDir, c);
2651
+ }
2652
+ const tail = segments[segments.length - 1];
2653
+ const local = path5.join(fromDir, tail + ".ex");
2654
+ if (fs5.existsSync(local)) return path5.relative(rootDir, local);
2655
+ return null;
2656
+ }
2657
+ function extractZigImports(content) {
2658
+ const results = [];
2659
+ const importRe = /@import\s*\(\s*"([^"]+)"\s*\)/g;
2660
+ let m;
2661
+ while ((m = importRe.exec(content)) !== null) {
2662
+ const spec = m[1];
2663
+ if (spec.endsWith(".zig")) {
2664
+ const isRelative = spec.startsWith(".") || !spec.includes("/");
2665
+ results.push({ specifier: spec, isRelative });
2666
+ }
2667
+ }
2668
+ return results;
2669
+ }
2670
+ function resolveZigImport(fromAbs, fromDir, raw, rootDir) {
2671
+ void fromAbs;
2672
+ const rootResolved = path5.resolve(rootDir);
2673
+ const candidate = path5.resolve(fromDir, raw.specifier);
2674
+ if (!candidate.startsWith(rootResolved + path5.sep) && candidate !== rootResolved) {
2675
+ return null;
2676
+ }
2677
+ if (fs5.existsSync(candidate)) return path5.relative(rootDir, candidate);
2678
+ return null;
2679
+ }
2286
2680
 
2287
2681
  // ../../packages/core/src/utils/TsConfigPathsResolver.ts
2288
2682
  import fs6 from "fs";
@@ -2513,6 +2907,7 @@ var CallGraphIndex = class _CallGraphIndex {
2513
2907
 
2514
2908
  // ../../packages/core/src/graph/DependencyGraph.ts
2515
2909
  var TS_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".vue"]);
2910
+ var PY_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".ipynb"]);
2516
2911
  var AST_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs", ".java", ".cs", ".rb", ".kt", ".kts", ".swift", ".ipynb", ".php", ".dart"]);
2517
2912
  var DependencyGraph = class {
2518
2913
  /** file → set of files it imports (forward edges) */
@@ -2522,6 +2917,31 @@ var DependencyGraph = class {
2522
2917
  /** Symbol index: symbolName → { filePath, type, signature, startLine?, endLine? } */
2523
2918
  symbolIndex = /* @__PURE__ */ new Map();
2524
2919
  callGraphIndex = new CallGraphIndex();
2920
+ /**
2921
+ * Re-export tracing (v1.6.x):
2922
+ * reExportMap[barrelFile][symbol] = sourceFile
2923
+ *
2924
+ * Built during graph construction from `from .submodule import Name`
2925
+ * statements (Python). When file C imports `from <barrel> import Name`,
2926
+ * the import resolver consults this map and emits a parallel edge
2927
+ * C → sourceFile so blast-radius queries against sourceFile can find
2928
+ * C as a transitive consumer through the barrel.
2929
+ *
2930
+ * Concrete fastapi case:
2931
+ * tests/test_routing.py has `from fastapi import APIRouter`
2932
+ * fastapi/__init__.py has `from .routing import APIRouter`
2933
+ * → reExportMap['fastapi/__init__.py']['APIRouter'] = 'fastapi/routing.py'
2934
+ * → emit edge tests/test_routing.py → fastapi/routing.py
2935
+ */
2936
+ reExportMap = /* @__PURE__ */ new Map();
2937
+ /**
2938
+ * Pending re-export queries. Populated during pass 1 (parse all
2939
+ * files, extract their imports); resolved in pass 2 once the
2940
+ * reExportMap is fully built. Required because file ordering would
2941
+ * otherwise miss re-exports whose source files are parsed AFTER the
2942
+ * consumer file.
2943
+ */
2944
+ pendingReExportQueries = [];
2525
2945
  parser = null;
2526
2946
  rootDir = "";
2527
2947
  snapshotDir = "";
@@ -2576,8 +2996,10 @@ var DependencyGraph = class {
2576
2996
  for (const imp of importNodes) {
2577
2997
  const spec = imp.source ?? imp.name;
2578
2998
  const isRelative = spec.startsWith(".");
2579
- const resolved = isRelative ? goResolver.resolveRelative(absPath, spec) : goResolver.resolve(spec);
2580
- if (resolved) this.addEdge(relPath, resolved);
2999
+ const resolvedAll = isRelative ? goResolver.resolveRelativeAll(absPath, spec) : goResolver.resolveAll(spec);
3000
+ for (const resolved of resolvedAll) {
3001
+ this.addEdge(relPath, resolved);
3002
+ }
2581
3003
  }
2582
3004
  } else {
2583
3005
  const importNodes = nodes.filter((n) => n.type === "import");
@@ -2586,7 +3008,26 @@ var DependencyGraph = class {
2586
3008
  const specifier = imp.source ?? imp.name;
2587
3009
  const isRelative = specifier.startsWith(".");
2588
3010
  const resolved = resolveImport(absPath, { specifier, isRelative }, rootDir);
2589
- if (resolved) this.addEdge(relPath, resolved);
3011
+ if (resolved) {
3012
+ this.addEdge(relPath, resolved);
3013
+ if (ext === ".py" && imp.importedNames && imp.importedNames.length > 0) {
3014
+ if (isRelative) {
3015
+ let map2 = this.reExportMap.get(relPath);
3016
+ if (!map2) {
3017
+ map2 = /* @__PURE__ */ new Map();
3018
+ this.reExportMap.set(relPath, map2);
3019
+ }
3020
+ for (const name of imp.importedNames) {
3021
+ map2.set(name, resolved);
3022
+ }
3023
+ }
3024
+ this.pendingReExportQueries.push({
3025
+ caller: relPath,
3026
+ barrel: resolved,
3027
+ symbols: imp.importedNames
3028
+ });
3029
+ }
3030
+ }
2590
3031
  }
2591
3032
  } else {
2592
3033
  const content = fs7.readFileSync(absPath, "utf-8");
@@ -2598,7 +3039,7 @@ var DependencyGraph = class {
2598
3039
  }
2599
3040
  }
2600
3041
  for (const node of nodes) {
2601
- if (node.type === "function" || node.type === "class" || node.type === "interface") {
3042
+ if (node.type === "function" || node.type === "class" || node.type === "interface" || node.type === "method") {
2602
3043
  const existing = this.symbolIndex.get(node.name) ?? [];
2603
3044
  existing.push({
2604
3045
  filePath: relPath,
@@ -2615,6 +3056,11 @@ var DependencyGraph = class {
2615
3056
  for (const edge of callEdges) {
2616
3057
  this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
2617
3058
  }
3059
+ } else if (PY_EXTENSIONS.has(ext)) {
3060
+ const callEdges = await this.parser.parseAllPythonCallEdges(absPath);
3061
+ for (const edge of callEdges) {
3062
+ this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
3063
+ }
2618
3064
  }
2619
3065
  } else {
2620
3066
  const content = fs7.readFileSync(absPath, "utf-8");
@@ -2628,6 +3074,49 @@ var DependencyGraph = class {
2628
3074
  logger.error("Failed to parse", { file: relPath, detail: err instanceof Error ? err.message : String(err) });
2629
3075
  }
2630
3076
  }
3077
+ let reExportEdgesAdded = 0;
3078
+ for (const { caller, barrel, symbols } of this.pendingReExportQueries) {
3079
+ const map2 = this.reExportMap.get(barrel);
3080
+ if (!map2) continue;
3081
+ for (const sym of symbols) {
3082
+ const source = map2.get(sym);
3083
+ if (source && source !== caller && source !== barrel) {
3084
+ this.addEdge(caller, source);
3085
+ reExportEdgesAdded += 1;
3086
+ }
3087
+ }
3088
+ }
3089
+ this.pendingReExportQueries = [];
3090
+ if (reExportEdgesAdded > 0) {
3091
+ logger.info("Re-export tracing added parallel edges", { count: reExportEdgesAdded });
3092
+ }
3093
+ let goTestEdgesAdded = 0;
3094
+ for (const relPath of this.forwardEdges.keys()) {
3095
+ if (!relPath.endsWith("_test.go")) continue;
3096
+ const dir = path7.dirname(relPath);
3097
+ const base = path7.basename(relPath, "_test.go");
3098
+ const namesake = path7.join(dir, base + ".go").replace(/\\/g, "/");
3099
+ if (this.forwardEdges.has(namesake)) {
3100
+ this.addEdge(relPath, namesake);
3101
+ this.addEdge(namesake, relPath);
3102
+ goTestEdgesAdded += 2;
3103
+ continue;
3104
+ }
3105
+ const absDir = path7.join(this.rootDir, dir);
3106
+ try {
3107
+ const siblings = fs7.readdirSync(absDir).filter((f) => f.endsWith(".go") && !f.endsWith("_test.go"));
3108
+ for (const sib of siblings) {
3109
+ const sibRel = path7.join(dir, sib).replace(/\\/g, "/");
3110
+ this.addEdge(relPath, sibRel);
3111
+ this.addEdge(sibRel, relPath);
3112
+ goTestEdgesAdded += 2;
3113
+ }
3114
+ } catch {
3115
+ }
3116
+ }
3117
+ if (goTestEdgesAdded > 0) {
3118
+ logger.info("Go test\u2194source linkage added edges", { count: goTestEdgesAdded });
3119
+ }
2631
3120
  await this.saveSnapshot();
2632
3121
  logger.info("Graph built", { files: files.length, edges: this.edgeCount() });
2633
3122
  if (options?.afterReady) {
@@ -2820,8 +3309,10 @@ var DependencyGraph = class {
2820
3309
  for (const imp of importNodes) {
2821
3310
  const spec = imp.source ?? imp.name;
2822
3311
  const isRelative = spec.startsWith(".");
2823
- const resolved = isRelative ? goResolver.resolveRelative(absPath, spec) : goResolver.resolve(spec);
2824
- if (resolved) this.addEdge(relPath, resolved);
3312
+ const resolvedAll = isRelative ? goResolver.resolveRelativeAll(absPath, spec) : goResolver.resolveAll(spec);
3313
+ for (const resolved of resolvedAll) {
3314
+ this.addEdge(relPath, resolved);
3315
+ }
2825
3316
  }
2826
3317
  } else {
2827
3318
  const importNodes = nodes.filter((n) => n.type === "import");
@@ -2842,7 +3333,7 @@ var DependencyGraph = class {
2842
3333
  }
2843
3334
  }
2844
3335
  for (const node of nodes) {
2845
- if (node.type === "function" || node.type === "class" || node.type === "interface") {
3336
+ if (node.type === "function" || node.type === "class" || node.type === "interface" || node.type === "method") {
2846
3337
  const existing = this.symbolIndex.get(node.name) ?? [];
2847
3338
  existing.push({
2848
3339
  filePath: relPath,
@@ -2859,6 +3350,11 @@ var DependencyGraph = class {
2859
3350
  for (const edge of callEdges) {
2860
3351
  this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
2861
3352
  }
3353
+ } else if (PY_EXTENSIONS.has(ext)) {
3354
+ const callEdges = await this.parser.parseAllPythonCallEdges(absPath);
3355
+ for (const edge of callEdges) {
3356
+ this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
3357
+ }
2862
3358
  }
2863
3359
  } else {
2864
3360
  const content = fs7.readFileSync(absPath, "utf-8");
@@ -2915,12 +3411,12 @@ var DependencyGraph = class {
2915
3411
  symbolIndex: Object.fromEntries(this.symbolIndex.entries())
2916
3412
  };
2917
3413
  const snapshotPath = this.getSnapshotPath();
2918
- const tmpPath = snapshotPath + ".tmp";
3414
+ const tmpPath = `${snapshotPath}.${process.pid}.tmp`;
2919
3415
  fs7.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
2920
3416
  fs7.renameSync(tmpPath, snapshotPath);
2921
3417
  const callData = this.callGraphIndex.toJSON();
2922
3418
  const callPath = path7.join(this.snapshotDir, "call-graph-snapshot.json");
2923
- const callTmp = callPath + ".tmp";
3419
+ const callTmp = `${callPath}.${process.pid}.tmp`;
2924
3420
  fs7.writeFileSync(callTmp, JSON.stringify(callData));
2925
3421
  fs7.renameSync(callTmp, callPath);
2926
3422
  }
@@ -3267,8 +3763,8 @@ var CoChangeIndex = class _CoChangeIndex {
3267
3763
  if (event.isBulk || event.isMerge) return;
3268
3764
  const paths = event.files.map((f) => f.path);
3269
3765
  if (paths.length === 0) return;
3270
- for (const path47 of paths) {
3271
- this.nodeCounts.set(path47, (this.nodeCounts.get(path47) ?? 0) + 1);
3766
+ for (const path48 of paths) {
3767
+ this.nodeCounts.set(path48, (this.nodeCounts.get(path48) ?? 0) + 1);
3272
3768
  }
3273
3769
  for (let i = 0; i < paths.length; i++) {
3274
3770
  for (let j = i + 1; j < paths.length; j++) {
@@ -3415,8 +3911,8 @@ var ChurnIndex = class _ChurnIndex {
3415
3911
  */
3416
3912
  snapshot() {
3417
3913
  const nodes = {};
3418
- for (const [path47, raw] of this.nodes) {
3419
- nodes[path47] = {
3914
+ for (const [path48, raw] of this.nodes) {
3915
+ nodes[path48] = {
3420
3916
  commits: raw.commits,
3421
3917
  churnLines: raw.churnLines,
3422
3918
  bugCommits: raw.bugCommits,
@@ -3431,8 +3927,8 @@ var ChurnIndex = class _ChurnIndex {
3431
3927
  */
3432
3928
  static load(s) {
3433
3929
  const idx = new _ChurnIndex();
3434
- for (const [path47, raw] of Object.entries(s.nodes)) {
3435
- idx.nodes.set(path47, {
3930
+ for (const [path48, raw] of Object.entries(s.nodes)) {
3931
+ idx.nodes.set(path48, {
3436
3932
  commits: raw.commits,
3437
3933
  churnLines: raw.churnLines,
3438
3934
  bugCommits: raw.bugCommits,
@@ -3445,8 +3941,8 @@ var ChurnIndex = class _ChurnIndex {
3445
3941
  // -------------------------------------------------------------------------
3446
3942
  // Private helpers
3447
3943
  // -------------------------------------------------------------------------
3448
- getOrCreate(path47) {
3449
- const existing = this.nodes.get(path47);
3944
+ getOrCreate(path48) {
3945
+ const existing = this.nodes.get(path48);
3450
3946
  if (existing !== void 0) return existing;
3451
3947
  const fresh = {
3452
3948
  commits: 0,
@@ -3455,7 +3951,7 @@ var ChurnIndex = class _ChurnIndex {
3455
3951
  authorCounts: {},
3456
3952
  lastTouch: 0
3457
3953
  };
3458
- this.nodes.set(path47, fresh);
3954
+ this.nodes.set(path48, fresh);
3459
3955
  return fresh;
3460
3956
  }
3461
3957
  };
@@ -3536,12 +4032,12 @@ var OwnershipIndex = class _OwnershipIndex {
3536
4032
  */
3537
4033
  snapshot() {
3538
4034
  const nodes = {};
3539
- for (const [path47, raw] of this.nodes) {
4035
+ for (const [path48, raw] of this.nodes) {
3540
4036
  const authorWeights = {};
3541
4037
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3542
4038
  authorWeights[email] = { ...entry };
3543
4039
  }
3544
- nodes[path47] = { authorWeights, lastTouch: raw.lastTouch };
4040
+ nodes[path48] = { authorWeights, lastTouch: raw.lastTouch };
3545
4041
  }
3546
4042
  return { version: 1, nodes };
3547
4043
  }
@@ -3550,23 +4046,23 @@ var OwnershipIndex = class _OwnershipIndex {
3550
4046
  */
3551
4047
  static load(s) {
3552
4048
  const idx = new _OwnershipIndex();
3553
- for (const [path47, raw] of Object.entries(s.nodes)) {
4049
+ for (const [path48, raw] of Object.entries(s.nodes)) {
3554
4050
  const authorWeights = {};
3555
4051
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3556
4052
  authorWeights[email] = { ...entry };
3557
4053
  }
3558
- idx.nodes.set(path47, { authorWeights, lastTouch: raw.lastTouch });
4054
+ idx.nodes.set(path48, { authorWeights, lastTouch: raw.lastTouch });
3559
4055
  }
3560
4056
  return idx;
3561
4057
  }
3562
4058
  // -------------------------------------------------------------------------
3563
4059
  // Private helpers
3564
4060
  // -------------------------------------------------------------------------
3565
- getOrCreate(path47) {
3566
- const existing = this.nodes.get(path47);
4061
+ getOrCreate(path48) {
4062
+ const existing = this.nodes.get(path48);
3567
4063
  if (existing !== void 0) return existing;
3568
4064
  const fresh = { authorWeights: {}, lastTouch: 0 };
3569
- this.nodes.set(path47, fresh);
4065
+ this.nodes.set(path48, fresh);
3570
4066
  return fresh;
3571
4067
  }
3572
4068
  };
@@ -4326,6 +4822,11 @@ ${methodLines.join("\n")}
4326
4822
  init_embedder();
4327
4823
  init_VectorStore();
4328
4824
 
4825
+ // ../../packages/core/src/db/vectorsCleanup.ts
4826
+ import fs16 from "fs";
4827
+ import path16 from "path";
4828
+ var VECTOR_DB_REL = path16.join(".ctxloom", "vectors.lancedb");
4829
+
4329
4830
  // ../../node_modules/zod/v3/external.js
4330
4831
  var external_exports = {};
4331
4832
  __export(external_exports, {
@@ -4804,8 +5305,8 @@ function getErrorMap() {
4804
5305
 
4805
5306
  // ../../node_modules/zod/v3/helpers/parseUtil.js
4806
5307
  var makeIssue = (params) => {
4807
- const { data, path: path47, errorMaps, issueData } = params;
4808
- const fullPath = [...path47, ...issueData.path || []];
5308
+ const { data, path: path48, errorMaps, issueData } = params;
5309
+ const fullPath = [...path48, ...issueData.path || []];
4809
5310
  const fullIssue = {
4810
5311
  ...issueData,
4811
5312
  path: fullPath
@@ -4921,11 +5422,11 @@ var errorUtil;
4921
5422
 
4922
5423
  // ../../node_modules/zod/v3/types.js
4923
5424
  var ParseInputLazyPath = class {
4924
- constructor(parent, value, path47, key) {
5425
+ constructor(parent, value, path48, key) {
4925
5426
  this._cachedPath = [];
4926
5427
  this.parent = parent;
4927
5428
  this.data = value;
4928
- this._path = path47;
5429
+ this._path = path48;
4929
5430
  this._key = key;
4930
5431
  }
4931
5432
  get path() {
@@ -8379,13 +8880,13 @@ init_logger();
8379
8880
 
8380
8881
  // ../../packages/core/src/budget/eventCollector.ts
8381
8882
  init_logger();
8382
- import fs16 from "fs";
8883
+ import fs17 from "fs";
8383
8884
  import os2 from "os";
8384
- import path16 from "path";
8385
- var DEFAULT_TELEMETRY_DIR = path16.join(os2.homedir(), ".ctxloom", "telemetry");
8885
+ import path17 from "path";
8886
+ var DEFAULT_TELEMETRY_DIR = path17.join(os2.homedir(), ".ctxloom", "telemetry");
8386
8887
  function telemetryDir() {
8387
8888
  const raw = process.env.CTXLOOM_TELEMETRY_DIR ?? DEFAULT_TELEMETRY_DIR;
8388
- if (raw.includes("..") || !path16.isAbsolute(raw)) {
8889
+ if (raw.includes("..") || !path17.isAbsolute(raw)) {
8389
8890
  if (!telemetryDirWarned) {
8390
8891
  telemetryDirWarned = true;
8391
8892
  logger.warn('CTXLOOM_TELEMETRY_DIR rejected \u2014 must be an absolute path with no ".." segments; using default', {
@@ -8395,7 +8896,7 @@ function telemetryDir() {
8395
8896
  }
8396
8897
  return DEFAULT_TELEMETRY_DIR;
8397
8898
  }
8398
- return path16.resolve(raw);
8899
+ return path17.resolve(raw);
8399
8900
  }
8400
8901
  var telemetryDirWarned = false;
8401
8902
  function filenameForDate(date) {
@@ -8408,12 +8909,12 @@ function readEvents(opts = {}) {
8408
8909
  const until = opts.until ?? /* @__PURE__ */ new Date();
8409
8910
  const since = opts.since ?? new Date(until.getTime() - 14 * 24 * 60 * 60 * 1e3);
8410
8911
  const dir = telemetryDir();
8411
- if (!fs16.existsSync(dir)) return [];
8912
+ if (!fs17.existsSync(dir)) return [];
8412
8913
  const out = [];
8413
8914
  for (let cursor = new Date(Date.UTC(since.getUTCFullYear(), since.getUTCMonth(), since.getUTCDate())); cursor.getTime() <= until.getTime(); cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1e3)) {
8414
- const file = path16.join(dir, filenameForDate(cursor));
8415
- if (!fs16.existsSync(file)) continue;
8416
- const text = fs16.readFileSync(file, "utf-8");
8915
+ const file = path17.join(dir, filenameForDate(cursor));
8916
+ if (!fs17.existsSync(file)) continue;
8917
+ const text = fs17.readFileSync(file, "utf-8");
8417
8918
  for (const line of text.split("\n")) {
8418
8919
  if (line.trim() === "") continue;
8419
8920
  let parsed;
@@ -8474,7 +8975,7 @@ var Schema2 = external_exports.object({
8474
8975
  });
8475
8976
 
8476
8977
  // ../../packages/core/src/tools/context-packet.ts
8477
- import path17 from "path";
8978
+ import path18 from "path";
8478
8979
  var Schema3 = external_exports.object({
8479
8980
  target_file: external_exports.string().describe("Relative path to the primary file"),
8480
8981
  mode: external_exports.enum(["edit", "read"]).optional().default("edit").describe("Context mode"),
@@ -8486,7 +8987,7 @@ var Schema3 = external_exports.object({
8486
8987
  });
8487
8988
 
8488
8989
  // ../../packages/core/src/tools/findCallers.ts
8489
- import path18 from "path";
8990
+ import path19 from "path";
8490
8991
 
8491
8992
  // ../../packages/core/src/tools/call-graph.ts
8492
8993
  var Schema4 = external_exports.object({
@@ -8587,7 +9088,7 @@ var Schema12 = external_exports.object({
8587
9088
  });
8588
9089
 
8589
9090
  // ../../packages/core/src/tools/knowledge-gaps.ts
8590
- import path19 from "path";
9091
+ import path20 from "path";
8591
9092
  var Schema13 = external_exports.object({
8592
9093
  min_importers: external_exports.number().min(1).max(50).optional().default(3).describe(
8593
9094
  "Minimum importers to qualify as an untested hub (default: 3)"
@@ -8616,7 +9117,7 @@ var Schema14 = external_exports.object({
8616
9117
  });
8617
9118
 
8618
9119
  // ../../packages/core/src/tools/wiki-generate.ts
8619
- import fs17 from "fs";
9120
+ import fs18 from "fs";
8620
9121
  var Schema15 = external_exports.object({
8621
9122
  force: external_exports.boolean().optional().default(false).describe(
8622
9123
  "Regenerate all pages even if content unchanged (default: false)"
@@ -8664,8 +9165,8 @@ var Schema17 = external_exports.object({
8664
9165
  });
8665
9166
 
8666
9167
  // ../../packages/core/src/tools/refactor-preview.ts
8667
- import fs18 from "fs";
8668
- import path20 from "path";
9168
+ import fs19 from "fs";
9169
+ import path21 from "path";
8669
9170
  var Schema18 = external_exports.object({
8670
9171
  symbol: external_exports.string().min(1).describe("Symbol name to rename (exact match, case-sensitive)"),
8671
9172
  new_name: external_exports.string().min(1).describe("New name for the symbol"),
@@ -8700,8 +9201,8 @@ var Schema19 = external_exports.object({
8700
9201
  init_embedder();
8701
9202
  init_VectorStore();
8702
9203
  init_logger();
8703
- import fs19 from "fs";
8704
- import path21 from "path";
9204
+ import fs20 from "fs";
9205
+ import path22 from "path";
8705
9206
  var Schema20 = external_exports.object({
8706
9207
  query: external_exports.string().min(1).describe("Search query \u2014 natural language or code fragment"),
8707
9208
  limit: external_exports.number().min(1).max(100).optional().default(10).describe(
@@ -8718,8 +9219,8 @@ var Schema20 = external_exports.object({
8718
9219
  });
8719
9220
 
8720
9221
  // ../../packages/core/src/tools/apply-refactor.ts
8721
- import fs20 from "fs";
8722
- import path22 from "path";
9222
+ import fs21 from "fs";
9223
+ import path23 from "path";
8723
9224
  var Schema21 = external_exports.object({
8724
9225
  symbol: external_exports.string().min(1).describe("Symbol name to rename (exact, case-sensitive)"),
8725
9226
  new_name: external_exports.string().min(1).describe("New name for the symbol"),
@@ -8752,8 +9253,8 @@ var Schema22 = external_exports.object({
8752
9253
  });
8753
9254
 
8754
9255
  // ../../packages/core/src/tools/full-text-search.ts
8755
- import fs21 from "fs";
8756
- import path23 from "path";
9256
+ import fs22 from "fs";
9257
+ import path24 from "path";
8757
9258
  var Schema23 = external_exports.object({
8758
9259
  query: external_exports.string().min(1).describe("Search term \u2014 literal or /regex/"),
8759
9260
  mode: external_exports.enum(["hybrid", "keyword", "semantic"]).optional().default("hybrid"),
@@ -8785,8 +9286,8 @@ var Schema25 = external_exports.object({
8785
9286
  });
8786
9287
 
8787
9288
  // ../../packages/core/src/tools/graph-snapshot.ts
8788
- import fs22 from "fs";
8789
- import path24 from "path";
9289
+ import fs23 from "fs";
9290
+ import path25 from "path";
8790
9291
  var schema = external_exports.object({
8791
9292
  name: external_exports.string().min(1).max(64).regex(/^[\w.-]+$/, "Name may only contain letters, digits, dots, underscores, hyphens").describe(
8792
9293
  'Snapshot name (e.g. "before-refactor", "v1.0"). Used as the filename.'
@@ -8798,8 +9299,8 @@ var schema = external_exports.object({
8798
9299
  });
8799
9300
 
8800
9301
  // ../../packages/core/src/tools/graph-diff.ts
8801
- import fs23 from "fs";
8802
- import path25 from "path";
9302
+ import fs24 from "fs";
9303
+ import path26 from "path";
8803
9304
  var schema2 = external_exports.object({
8804
9305
  baseline: external_exports.string().min(1).describe('Name of the baseline snapshot (the "before" state).'),
8805
9306
  current: external_exports.string().min(1).describe('Name of the current snapshot (the "after" state).'),
@@ -8840,8 +9341,8 @@ var Schema27 = external_exports.object({
8840
9341
  });
8841
9342
 
8842
9343
  // ../../packages/core/src/rules/loadConfig.ts
8843
- import fs24 from "fs/promises";
8844
- import path26 from "path";
9344
+ import fs25 from "fs/promises";
9345
+ import path27 from "path";
8845
9346
 
8846
9347
  // ../../node_modules/js-yaml/dist/js-yaml.mjs
8847
9348
  function isNothing(subject) {
@@ -11482,35 +11983,36 @@ var Schema30 = external_exports.object({
11482
11983
  });
11483
11984
 
11484
11985
  // ../../packages/core/src/tools/ruleManager.ts
11485
- import fs25 from "fs";
11486
- import path27 from "path";
11487
-
11488
- // ../../packages/core/src/review/AuthorResolver.ts
11489
- import fs26 from "fs/promises";
11986
+ import fs26 from "fs";
11490
11987
  import path28 from "path";
11491
11988
 
11492
- // ../../packages/core/src/review/CodeownersWriter.ts
11989
+ // ../../packages/core/src/review/AuthorResolver.ts
11493
11990
  import fs27 from "fs/promises";
11494
11991
  import path29 from "path";
11495
11992
 
11496
- // ../../packages/core/src/review/loadConfig.ts
11993
+ // ../../packages/core/src/review/CodeownersWriter.ts
11497
11994
  import fs28 from "fs/promises";
11498
11995
  import path30 from "path";
11499
11996
 
11500
- // ../../packages/core/src/security/PathValidator.ts
11997
+ // ../../packages/core/src/review/loadConfig.ts
11998
+ import fs29 from "fs/promises";
11501
11999
  import path31 from "path";
11502
- import fs29 from "fs";
12000
+
12001
+ // ../../packages/core/src/security/PathValidator.ts
12002
+ import path32 from "path";
12003
+ import fs30 from "fs";
11503
12004
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
11504
12005
 
11505
12006
  // ../../packages/core/src/index.ts
11506
12007
  init_logger();
11507
12008
 
11508
12009
  // ../../packages/core/src/watcher/FileWatcher.ts
12010
+ init_embedder();
11509
12011
  init_logger();
11510
12012
 
11511
12013
  // ../../packages/core/src/license/LicenseStore.ts
11512
12014
  import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSync } from "fs";
11513
- import path32 from "path";
12015
+ import path33 from "path";
11514
12016
 
11515
12017
  // ../../packages/core/src/license/types.ts
11516
12018
  var FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/;
@@ -11541,11 +12043,11 @@ import os6 from "os";
11541
12043
 
11542
12044
  // ../../packages/core/src/license/DistinctIdStore.ts
11543
12045
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
11544
- import path33 from "path";
12046
+ import path34 from "path";
11545
12047
  import os4 from "os";
11546
12048
  var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11547
12049
  function distinctIdPath(home) {
11548
- return path33.join(home ?? os4.homedir(), ".ctxloom", "distinct_id");
12050
+ return path34.join(home ?? os4.homedir(), ".ctxloom", "distinct_id");
11549
12051
  }
11550
12052
  function isValidV4(id) {
11551
12053
  return typeof id === "string" && UUID_V4_REGEX.test(id);
@@ -11566,7 +12068,7 @@ function getOrCreateDistinctId(home) {
11566
12068
  id: crypto.randomUUID(),
11567
12069
  alias_pending: os4.hostname()
11568
12070
  };
11569
- mkdirSync2(path33.dirname(filePath), { recursive: true });
12071
+ mkdirSync2(path34.dirname(filePath), { recursive: true });
11570
12072
  writeFileSync2(filePath, JSON.stringify(record), { mode: 384 });
11571
12073
  return record;
11572
12074
  }
@@ -11592,10 +12094,10 @@ function resolveTelemetryLevel() {
11592
12094
  }
11593
12095
  var TELEMETRY_LEVEL = resolveTelemetryLevel();
11594
12096
  var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
11595
- var CTXLOOM_VERSION = "1.5.5".length > 0 ? "1.5.5" : "dev";
12097
+ var CTXLOOM_VERSION = "1.7.0".length > 0 ? "1.7.0" : "dev";
11596
12098
  var POSTHOG_HOST = "https://eu.i.posthog.com";
11597
12099
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
11598
- var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528\u2028" : "");
12100
+ var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
11599
12101
  var cachedDistinctId = null;
11600
12102
  function resolveDistinctId() {
11601
12103
  if (cachedDistinctId) return cachedDistinctId;
@@ -11725,32 +12227,32 @@ function parseStack(stack) {
11725
12227
 
11726
12228
  // ../../packages/core/src/license/FunnelMilestones.ts
11727
12229
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
11728
- import path34 from "path";
12230
+ import path35 from "path";
11729
12231
  import os5 from "os";
11730
12232
 
11731
12233
  // ../../packages/core/src/license/TelemetryNotice.ts
11732
12234
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
11733
- import path35 from "path";
12235
+ import path36 from "path";
11734
12236
  import os7 from "os";
11735
12237
 
11736
12238
  // ../../packages/core/src/server/ProjectState.ts
11737
- import path37 from "path";
12239
+ import path38 from "path";
11738
12240
 
11739
12241
  // ../../packages/core/src/server/projectId.ts
11740
12242
  import crypto5 from "crypto";
11741
- import path36 from "path";
12243
+ import path37 from "path";
11742
12244
 
11743
12245
  // ../../packages/core/src/server/ProjectStateManager.ts
11744
12246
  init_logger();
11745
12247
 
11746
12248
  // ../../packages/core/src/server/resolveProjectRoot.ts
11747
- import fs30 from "fs";
11748
- import path38 from "path";
11749
-
11750
- // ../../packages/core/src/install/installer.ts
11751
12249
  import fs31 from "fs";
11752
12250
  import path39 from "path";
11753
12251
 
12252
+ // ../../packages/core/src/install/installer.ts
12253
+ import fs32 from "fs";
12254
+ import path40 from "path";
12255
+
11754
12256
  // ../../packages/core/src/install/templates.ts
11755
12257
  var RULES_BLOCK_CONTENT = `## MCP Tools: ctxloom
11756
12258
 
@@ -11760,6 +12262,27 @@ The graph is faster, cheaper (fewer tokens), and gives you
11760
12262
  structural context (callers, dependents, test coverage) that file
11761
12263
  scanning cannot.
11762
12264
 
12265
+ ### Operating principles
12266
+
12267
+ ctxloom's tools exist to operationalize four principles for working
12268
+ with an AI coding agent. They're the *why* behind every tool below.
12269
+ Adapted from Karpathy's LLM-coding-pitfalls notes
12270
+ (<https://github.com/multica-ai/andrej-karpathy-skills>, MIT).
12271
+
12272
+ 1. **Think before coding** \u2014 read the relevant graph slice before
12273
+ editing. \`ctx_blast_radius\`, \`ctx_get_call_graph\`,
12274
+ \`ctx_get_review_context\` are how you do this without re-reading
12275
+ whole files.
12276
+ 2. **Simplicity first** \u2014 prefer the smallest viable change. Use
12277
+ \`ctx_refactor_preview\` to see the full diff *before* applying;
12278
+ if the preview is sprawling, the plan is too big.
12279
+ 3. **Surgical changes** \u2014 every changed line should trace directly
12280
+ to the user's request. \`ctx_detect_changes\` after each edit
12281
+ confirms scope hasn't drifted.
12282
+ 4. **Goal-driven execution** \u2014 stop when the goal is met. Don't
12283
+ "polish" beyond the request. \`ctx_knowledge_gaps\` flags real
12284
+ risk surfaces; everything else is yak-shaving.
12285
+
11763
12286
  ### Start every workflow with \`ctx_get_minimal_context\`
11764
12287
 
11765
12288
  The first MCP call into ctxloom should always be
@@ -11989,7 +12512,7 @@ function summarize(events, windowStart, windowEnd) {
11989
12512
 
11990
12513
  // server/loader.ts
11991
12514
  async function loadContext(root) {
11992
- const absRoot = path40.resolve(root);
12515
+ const absRoot = path41.resolve(root);
11993
12516
  const overlay = new GitOverlayStore(absRoot);
11994
12517
  const gitEnabled = await overlay.loadSnapshot();
11995
12518
  const graph = new DependencyGraph();
@@ -12242,21 +12765,21 @@ function buildOwnershipRouter(ctx) {
12242
12765
 
12243
12766
  // server/routes/file.ts
12244
12767
  import { Router as Router7 } from "express";
12245
- import fs32 from "fs/promises";
12246
- import path41 from "path";
12768
+ import fs33 from "fs/promises";
12769
+ import path42 from "path";
12247
12770
  function buildFileRouter(ctx) {
12248
12771
  const router = Router7();
12249
12772
  router.get("/", async (req, res) => {
12250
12773
  const rel = req.query.path;
12251
12774
  if (!rel) return res.status(400).json({ error: "missing path" });
12252
- const abs = path41.resolve(ctx.root, rel);
12253
- const rootBoundary = ctx.root.endsWith(path41.sep) ? ctx.root : ctx.root + path41.sep;
12775
+ const abs = path42.resolve(ctx.root, rel);
12776
+ const rootBoundary = ctx.root.endsWith(path42.sep) ? ctx.root : ctx.root + path42.sep;
12254
12777
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
12255
12778
  return res.status(403).json({ error: "forbidden" });
12256
12779
  }
12257
12780
  try {
12258
- const content = await fs32.readFile(abs, "utf-8");
12259
- const ext = path41.extname(abs).slice(1);
12781
+ const content = await fs33.readFile(abs, "utf-8");
12782
+ const ext = path42.extname(abs).slice(1);
12260
12783
  res.json({ content, lines: content.split("\n").length, ext });
12261
12784
  } catch {
12262
12785
  res.status(404).json({ error: "not found" });
@@ -12268,7 +12791,7 @@ function buildFileRouter(ctx) {
12268
12791
  // server/routes/open.ts
12269
12792
  import { Router as Router8 } from "express";
12270
12793
  import { execFile as execFile2 } from "child_process";
12271
- import path42 from "path";
12794
+ import path43 from "path";
12272
12795
  function tryOpen(bin, abs) {
12273
12796
  return new Promise((resolve) => {
12274
12797
  execFile2(bin, [abs], { timeout: 5e3 }, (err) => resolve(!err));
@@ -12279,8 +12802,8 @@ function buildOpenRouter(ctx) {
12279
12802
  router.post("/", async (req, res) => {
12280
12803
  const rel = req.body?.path;
12281
12804
  if (!rel || typeof rel !== "string") return res.status(400).json({ error: "missing path" });
12282
- const abs = path42.resolve(ctx.root, rel);
12283
- const rootBoundary = ctx.root.endsWith(path42.sep) ? ctx.root : ctx.root + path42.sep;
12805
+ const abs = path43.resolve(ctx.root, rel);
12806
+ const rootBoundary = ctx.root.endsWith(path43.sep) ? ctx.root : ctx.root + path43.sep;
12284
12807
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
12285
12808
  return res.status(403).json({ error: "forbidden" });
12286
12809
  }
@@ -12292,8 +12815,8 @@ function buildOpenRouter(ctx) {
12292
12815
 
12293
12816
  // server/routes/tokens.ts
12294
12817
  import { Router as Router9 } from "express";
12295
- import path43 from "path";
12296
- import fs33 from "fs";
12818
+ import path44 from "path";
12819
+ import fs34 from "fs";
12297
12820
  var CHARS_PER_TOKEN = 4;
12298
12821
  var cache = null;
12299
12822
  function buildTokensRouter(ctx) {
@@ -12308,9 +12831,9 @@ function buildTokensRouter(ctx) {
12308
12831
  let fullChars = 0;
12309
12832
  let skeletonChars = 0;
12310
12833
  for (const file of files) {
12311
- const absPath = path43.join(ctx.root, file);
12834
+ const absPath = path44.join(ctx.root, file);
12312
12835
  try {
12313
- const content = fs33.readFileSync(absPath, "utf-8");
12836
+ const content = fs34.readFileSync(absPath, "utf-8");
12314
12837
  fullChars += content.length;
12315
12838
  const skeleton = await skeletonizer.skeletonize(absPath);
12316
12839
  skeletonChars += skeleton.length;
@@ -12411,17 +12934,17 @@ function buildFileTrendsRouter(ctx) {
12411
12934
 
12412
12935
  // server/routes/projects.ts
12413
12936
  import { Router as Router12 } from "express";
12414
- import path45 from "path";
12937
+ import path46 from "path";
12415
12938
 
12416
12939
  // server/projects.ts
12417
12940
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
12418
12941
  import os8 from "os";
12419
- import path44 from "path";
12942
+ import path45 from "path";
12420
12943
  import crypto7 from "crypto";
12421
12944
  var HOME = os8.homedir();
12422
- var REGISTRY_PATH = path44.join(HOME, ".ctxloom", "repos.json");
12945
+ var REGISTRY_PATH = path45.join(HOME, ".ctxloom", "repos.json");
12423
12946
  function slugFor(root) {
12424
- const abs = path44.resolve(root);
12947
+ const abs = path45.resolve(root);
12425
12948
  return crypto7.createHash("sha1").update(abs).digest("hex").slice(0, 12);
12426
12949
  }
12427
12950
  function readRegistry() {
@@ -12436,27 +12959,27 @@ function readRegistry() {
12436
12959
  }
12437
12960
  }
12438
12961
  function listProjects(defaultRoot) {
12439
- const absDefault = path44.resolve(defaultRoot);
12962
+ const absDefault = path45.resolve(defaultRoot);
12440
12963
  const out = [
12441
12964
  {
12442
12965
  slug: slugFor(absDefault),
12443
- name: path44.basename(absDefault) || absDefault,
12966
+ name: path45.basename(absDefault) || absDefault,
12444
12967
  root: absDefault,
12445
12968
  isDefault: true,
12446
- hasSnapshot: existsSync5(path44.join(absDefault, ".ctxloom"))
12969
+ hasSnapshot: existsSync5(path45.join(absDefault, ".ctxloom"))
12447
12970
  }
12448
12971
  ];
12449
12972
  const seen = /* @__PURE__ */ new Set([absDefault]);
12450
12973
  for (const entry of readRegistry()) {
12451
- const abs = path44.resolve(entry.root);
12974
+ const abs = path45.resolve(entry.root);
12452
12975
  if (seen.has(abs)) continue;
12453
12976
  seen.add(abs);
12454
12977
  const item = {
12455
12978
  slug: slugFor(abs),
12456
- name: entry.name ?? (path44.basename(abs) || abs),
12979
+ name: entry.name ?? (path45.basename(abs) || abs),
12457
12980
  root: abs,
12458
12981
  isDefault: false,
12459
- hasSnapshot: existsSync5(path44.join(abs, ".ctxloom"))
12982
+ hasSnapshot: existsSync5(path45.join(abs, ".ctxloom"))
12460
12983
  };
12461
12984
  if (entry.alias !== void 0) item.alias = entry.alias;
12462
12985
  out.push(item);
@@ -12509,7 +13032,7 @@ function buildProjectsRouter(deps) {
12509
13032
  } catch (err) {
12510
13033
  const detail = err instanceof Error ? err.message : String(err);
12511
13034
  res.status(500).json({
12512
- error: `failed to switch to ${path45.basename(target.root)}: ${detail}`
13035
+ error: `failed to switch to ${path46.basename(target.root)}: ${detail}`
12513
13036
  });
12514
13037
  }
12515
13038
  });
@@ -12619,7 +13142,7 @@ function buildBudgetEventsRouter() {
12619
13142
  }
12620
13143
 
12621
13144
  // server/index.ts
12622
- var __dirname2 = path46.dirname(fileURLToPath2(import.meta.url));
13145
+ var __dirname2 = path47.dirname(fileURLToPath2(import.meta.url));
12623
13146
  async function startDashboard(options) {
12624
13147
  const { root, port, open } = options;
12625
13148
  console.log(`ctxloom dashboard \u2014 loading context from ${root}...`);
@@ -12679,9 +13202,9 @@ async function startDashboard(options) {
12679
13202
  }
12680
13203
  activeWatcher = null;
12681
13204
  }
12682
- const snapshotDir = path46.join(targetRoot, ".ctxloom");
13205
+ const snapshotDir = path47.join(targetRoot, ".ctxloom");
12683
13206
  try {
12684
- activeWatcher = fs34.watch(snapshotDir, (_event, filename) => {
13207
+ activeWatcher = fs35.watch(snapshotDir, (_event, filename) => {
12685
13208
  if (!filename || !filename.includes("snapshot")) return;
12686
13209
  if (debounce) clearTimeout(debounce);
12687
13210
  debounce = setTimeout(async () => {
@@ -12710,12 +13233,12 @@ async function startDashboard(options) {
12710
13233
  attachSnapshotWatcher(newRoot);
12711
13234
  }
12712
13235
  }));
12713
- const clientDist = path46.join(__dirname2, "../dashboard/client");
12714
- const clientDistExists = fs34.existsSync(path46.join(clientDist, "index.html"));
13236
+ const clientDist = path47.join(__dirname2, "../dashboard/client");
13237
+ const clientDistExists = fs35.existsSync(path47.join(clientDist, "index.html"));
12715
13238
  if (clientDistExists) {
12716
13239
  app.use(express.static(clientDist, { dotfiles: "allow" }));
12717
13240
  app.get(/.*/, (_req, res) => {
12718
- res.sendFile(path46.join(clientDist, "index.html"), { dotfiles: "allow" });
13241
+ res.sendFile(path47.join(clientDist, "index.html"), { dotfiles: "allow" });
12719
13242
  });
12720
13243
  } else {
12721
13244
  app.get(/^\/(?!api\/).*/, (_req, res) => {