ctxloom-pro 1.7.3 → 1.7.5

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/README.md CHANGED
@@ -69,7 +69,7 @@ The full first-run flow is **one install + one trial + one init per project.** E
69
69
  npm install -g ctxloom-pro
70
70
  ```
71
71
 
72
- > **For local trial / dev use the unpinned command above is fine.** For unattended CI usage, pin to the exact version (`ctxloom-pro@1.7.3`) so future CLI releases don't silently desync your agent-spec coverage — see the workflow example below.
72
+ > **For local trial / dev use the unpinned command above is fine.** For unattended CI usage, pin to the exact version (`ctxloom-pro@1.7.5`) so future CLI releases don't silently desync your agent-spec coverage — see the workflow example below.
73
73
 
74
74
  ### 2 — Start your free trial (once per email)
75
75
 
@@ -383,7 +383,7 @@ jobs:
383
383
  # Exact pin (not `@^1`) so future CLI releases that add/remove MCP
384
384
  # tools don't silently desync your reviewer-agent specs. Bump on
385
385
  # every release; see CHANGELOG.md for the live version table.
386
- - run: npm install -g ctxloom-pro@1.7.3
386
+ - run: npm install -g ctxloom-pro@1.7.5
387
387
  - run: ctxloom index
388
388
  - run: ctxloom rules check --json
389
389
  ```
@@ -101,71 +101,28 @@ function resolveEmbeddingModel(env = process.env) {
101
101
  };
102
102
  }
103
103
  function collectFiles(dir, results = []) {
104
- const IGNORED_DIRS = /* @__PURE__ */ new Set([
105
- // Build artifacts + dependency caches
106
- "node_modules",
107
- "dist",
108
- "build",
109
- "out",
110
- "target",
111
- "coverage",
112
- ".cache",
113
- ".turbo",
114
- ".next",
115
- ".nuxt",
116
- // Version control + ctxloom state
117
- ".git",
118
- ".ctxloom",
119
- // Other tools' working state (often contains duplicated source)
120
- ".claude",
121
- ".code-review-graph",
122
- ".vscode-test"
123
- ]);
124
- const SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
125
- ".ts",
126
- ".tsx",
127
- ".js",
128
- ".jsx",
129
- ".mjs",
130
- ".vue",
131
- ".py",
132
- ".rs",
133
- ".go",
134
- ".java",
135
- ".cs",
136
- ".rb",
137
- ".kt",
138
- ".kts",
139
- ".swift",
140
- ".php",
141
- ".dart",
142
- ".c",
143
- ".cpp",
144
- ".h",
145
- ".md",
146
- ".json",
147
- ".yaml",
148
- ".yml",
149
- ".toml",
150
- ".ipynb"
151
- ]);
152
104
  const entries = fs3.readdirSync(dir, { withFileTypes: true });
153
105
  for (const entry of entries) {
154
106
  const fullPath = path3.join(dir, entry.name);
155
107
  if (entry.isDirectory()) {
156
- if (!IGNORED_DIRS.has(entry.name)) {
108
+ if (!isIgnoredDir(entry.name)) {
157
109
  collectFiles(fullPath, results);
158
110
  }
159
111
  } else if (entry.isFile()) {
160
112
  const ext = path3.extname(entry.name);
161
- if (SUPPORTED_EXTENSIONS.has(ext)) {
113
+ if (INDEX_SUPPORTED_EXTENSIONS.has(ext)) {
162
114
  results.push(fullPath);
163
115
  }
164
116
  }
165
117
  }
166
118
  return results;
167
119
  }
168
- var MODEL_REGISTRY, ACTIVE_MODEL, EMBEDDING_DIMENSION, MODEL_ID, MIN_MODEL_BYTES;
120
+ function isIgnoredDir(name) {
121
+ if (INDEXER_IGNORED_DIRS.has(name)) return true;
122
+ if (name.endsWith(".egg-info") || name.endsWith(".dist-info")) return true;
123
+ return false;
124
+ }
125
+ var MODEL_REGISTRY, ACTIVE_MODEL, EMBEDDING_DIMENSION, MODEL_ID, MIN_MODEL_BYTES, INDEXER_IGNORED_DIRS, INDEX_SUPPORTED_EXTENSIONS;
169
126
  var init_embedder = __esm({
170
127
  "../../packages/core/src/indexer/embedder.ts"() {
171
128
  "use strict";
@@ -196,6 +153,69 @@ var init_embedder = __esm({
196
153
  EMBEDDING_DIMENSION = ACTIVE_MODEL.dim;
197
154
  MODEL_ID = ACTIVE_MODEL.hfId;
198
155
  MIN_MODEL_BYTES = ACTIVE_MODEL.minBytes;
156
+ INDEXER_IGNORED_DIRS = /* @__PURE__ */ new Set([
157
+ // Build artifacts + dependency caches (JS/TS, Rust, Java)
158
+ "node_modules",
159
+ "dist",
160
+ "build",
161
+ "out",
162
+ "target",
163
+ "coverage",
164
+ ".cache",
165
+ ".turbo",
166
+ ".next",
167
+ ".nuxt",
168
+ // Python virtualenvs + caches. Real-world repro: EasyMoney (a 63-
169
+ // source-file FastAPI project) had a `.venv/` with 8,192 installed-
170
+ // package files. Pre-fix, `ctxloom index` reported 8,120 files /
171
+ // 14,138 edges instead of the expected 63 / 97, because none of the
172
+ // standard Python virtualenv/cache directory names were in this set.
173
+ // The `__pycache__` + `.pytest_cache` + `.ruff_cache` + `.mypy_cache`
174
+ // additions catch the bulk of Python noise on top of the venv.
175
+ ".venv",
176
+ "venv",
177
+ "env",
178
+ "__pycache__",
179
+ ".pytest_cache",
180
+ ".ruff_cache",
181
+ ".mypy_cache",
182
+ ".tox",
183
+ // Version control + ctxloom state
184
+ ".git",
185
+ ".ctxloom",
186
+ // Other tools' working state (often contains duplicated source)
187
+ ".claude",
188
+ ".code-review-graph",
189
+ ".vscode-test"
190
+ ]);
191
+ INDEX_SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
192
+ ".ts",
193
+ ".tsx",
194
+ ".js",
195
+ ".jsx",
196
+ ".mjs",
197
+ ".vue",
198
+ ".py",
199
+ ".rs",
200
+ ".go",
201
+ ".java",
202
+ ".cs",
203
+ ".rb",
204
+ ".kt",
205
+ ".kts",
206
+ ".swift",
207
+ ".php",
208
+ ".dart",
209
+ ".c",
210
+ ".cpp",
211
+ ".h",
212
+ ".md",
213
+ ".json",
214
+ ".yaml",
215
+ ".yml",
216
+ ".toml",
217
+ ".ipynb"
218
+ ]);
199
219
  }
200
220
  });
201
221
 
@@ -2909,7 +2929,7 @@ var CallGraphIndex = class _CallGraphIndex {
2909
2929
  var TS_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".vue"]);
2910
2930
  var PY_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".ipynb"]);
2911
2931
  var AST_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs", ".java", ".cs", ".rb", ".kt", ".kts", ".swift", ".ipynb", ".php", ".dart"]);
2912
- var CTXLOOM_VERSION = "1.7.3".length > 0 ? "1.7.3" : "dev";
2932
+ var CTXLOOM_VERSION = "1.7.5".length > 0 ? "1.7.5" : "dev";
2913
2933
  var SNAPSHOT_SCHEMA_VERSION = 2;
2914
2934
  function compareCtxloomVersions(snapshotVer, currentVer) {
2915
2935
  if (snapshotVer === currentVer) return "same";
@@ -2966,6 +2986,28 @@ var DependencyGraph = class {
2966
2986
  rootDir = "";
2967
2987
  snapshotDir = "";
2968
2988
  tsPathsResolver = null;
2989
+ /**
2990
+ * fs.watch handle on `.ctxloom/graph-snapshot.json`. When the file is
2991
+ * rewritten externally (e.g. the user runs `ctxloom index` from a
2992
+ * terminal while an MCP server is live), this watcher triggers an
2993
+ * in-memory rehydrate so subsequent tool calls reflect the new graph
2994
+ * without needing the user to restart their MCP client.
2995
+ *
2996
+ * Real repro that motivated this (v1.7.5): EasyMoney user ran
2997
+ * `rm -rf .ctxloom && ctxloom index` from the terminal but the
2998
+ * Claude Desktop MCP server kept serving the pre-wipe in-memory
2999
+ * graph (`Files: 2`) because there was no mechanism to detect the
3000
+ * fresh snapshot on disk. Confusing diagnostic loop.
3001
+ */
3002
+ snapshotWatcher = null;
3003
+ /** Debounce timer for snapshot-change events. */
3004
+ snapshotReloadTimer = null;
3005
+ /**
3006
+ * Tracks the snapshot mtime we last loaded from disk so the watcher
3007
+ * can suppress its own-write echo. saveSnapshot() updates this just
3008
+ * before writing; the watcher ignores any change whose mtime <= this.
3009
+ */
3010
+ lastLoadedSnapshotMtimeMs = 0;
2969
3011
  /**
2970
3012
  * Build the graph from all supported files in rootDir using AST parsing.
2971
3013
  */
@@ -3165,6 +3207,105 @@ var DependencyGraph = class {
3165
3207
  this.snapshotDir = path7.join(rootDir, ".ctxloom");
3166
3208
  return this.loadSnapshot();
3167
3209
  }
3210
+ /**
3211
+ * Start watching `.ctxloom/graph-snapshot.json` for external rewrites.
3212
+ * When the user runs `ctxloom index` (or any other tool) from a
3213
+ * terminal against the same project root, the watcher detects the
3214
+ * new mtime, reloads the snapshot, and atomically swaps the
3215
+ * in-memory graph — so subsequent MCP tool calls see the fresh
3216
+ * graph without requiring an MCP client restart.
3217
+ *
3218
+ * Own writes (via saveSnapshot) update lastLoadedSnapshotMtimeMs
3219
+ * BEFORE the rename completes, so the watcher's own-write echo is
3220
+ * filtered out by the mtime check.
3221
+ *
3222
+ * Debounced 200 ms — matches the FileWatcher cadence and absorbs
3223
+ * the burst of fs.watch events some platforms emit for a single
3224
+ * write (Linux can fire `rename` + `change`, macOS sometimes
3225
+ * fires multiple `change` for atomic rename).
3226
+ *
3227
+ * Idempotent: calling twice is harmless (re-uses the existing
3228
+ * watcher). Call stopSnapshotWatcher() before disposing the graph
3229
+ * to release the FD.
3230
+ */
3231
+ startSnapshotWatcher(debounceMs = 200) {
3232
+ if (this.snapshotWatcher) return;
3233
+ if (!this.snapshotDir) {
3234
+ logger.warn("startSnapshotWatcher: no snapshotDir set, call buildFromDirectory first");
3235
+ return;
3236
+ }
3237
+ const snapshotPath = this.getSnapshotPath();
3238
+ if (!fs7.existsSync(snapshotPath)) {
3239
+ logger.warn("startSnapshotWatcher: snapshot file does not exist yet", { path: snapshotPath });
3240
+ return;
3241
+ }
3242
+ try {
3243
+ this.snapshotWatcher = fs7.watch(snapshotPath, { persistent: false }, (eventType) => {
3244
+ if (this.snapshotReloadTimer) clearTimeout(this.snapshotReloadTimer);
3245
+ this.snapshotReloadTimer = setTimeout(() => {
3246
+ this.snapshotReloadTimer = null;
3247
+ void this.maybeReloadSnapshot(eventType);
3248
+ }, debounceMs);
3249
+ });
3250
+ this.snapshotWatcher.on("error", (err) => {
3251
+ logger.warn("snapshot watcher error", { detail: err instanceof Error ? err.message : String(err) });
3252
+ });
3253
+ logger.info("Graph snapshot hot-reload watcher started", { path: snapshotPath });
3254
+ } catch (err) {
3255
+ logger.warn("startSnapshotWatcher: failed to attach fs.watch", {
3256
+ detail: err instanceof Error ? err.message : String(err)
3257
+ });
3258
+ this.snapshotWatcher = null;
3259
+ }
3260
+ }
3261
+ /**
3262
+ * Stop the snapshot watcher and clear any pending debounce timer.
3263
+ * Call before disposing the graph to release the FD held by fs.watch.
3264
+ * Idempotent.
3265
+ */
3266
+ stopSnapshotWatcher() {
3267
+ if (this.snapshotReloadTimer) {
3268
+ clearTimeout(this.snapshotReloadTimer);
3269
+ this.snapshotReloadTimer = null;
3270
+ }
3271
+ if (this.snapshotWatcher) {
3272
+ try {
3273
+ this.snapshotWatcher.close();
3274
+ } catch {
3275
+ }
3276
+ this.snapshotWatcher = null;
3277
+ }
3278
+ }
3279
+ /**
3280
+ * Internal: called from the debounced watcher. Compares on-disk
3281
+ * mtime against `lastLoadedSnapshotMtimeMs` to filter own-write
3282
+ * echoes, then re-hydrates the in-memory maps from disk via
3283
+ * loadSnapshot(). Atomic from the perspective of MCP tool calls
3284
+ * because Node's event loop ensures loadSnapshot's map assignments
3285
+ * happen in a single tick (no other code interleaves).
3286
+ */
3287
+ async maybeReloadSnapshot(eventType) {
3288
+ const snapshotPath = this.getSnapshotPath();
3289
+ let currentMtime = 0;
3290
+ try {
3291
+ currentMtime = fs7.statSync(snapshotPath).mtimeMs;
3292
+ } catch {
3293
+ return;
3294
+ }
3295
+ if (currentMtime <= this.lastLoadedSnapshotMtimeMs) {
3296
+ return;
3297
+ }
3298
+ const ok = await this.loadSnapshot();
3299
+ if (ok) {
3300
+ logger.info("Graph snapshot hot-reloaded", {
3301
+ event: eventType,
3302
+ files: this.forwardEdges.size,
3303
+ edges: this.edgeCount()
3304
+ });
3305
+ } else {
3306
+ logger.warn("Graph snapshot hot-reload skipped (snapshot invalid or version-stale)");
3307
+ }
3308
+ }
3168
3309
  /**
3169
3310
  * Get files that the given file directly imports.
3170
3311
  */
@@ -3439,6 +3580,10 @@ var DependencyGraph = class {
3439
3580
  const tmpPath = `${snapshotPath}.${process.pid}.tmp`;
3440
3581
  fs7.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
3441
3582
  fs7.renameSync(tmpPath, snapshotPath);
3583
+ try {
3584
+ this.lastLoadedSnapshotMtimeMs = fs7.statSync(snapshotPath).mtimeMs;
3585
+ } catch {
3586
+ }
3442
3587
  const callData = this.callGraphIndex.toJSON();
3443
3588
  const callPath = path7.join(this.snapshotDir, "call-graph-snapshot.json");
3444
3589
  const callTmp = `${callPath}.${process.pid}.tmp`;
@@ -3516,6 +3661,10 @@ var DependencyGraph = class {
3516
3661
  this.callGraphIndex = new CallGraphIndex();
3517
3662
  }
3518
3663
  }
3664
+ try {
3665
+ this.lastLoadedSnapshotMtimeMs = fs7.statSync(snapshotPath).mtimeMs;
3666
+ } catch {
3667
+ }
3519
3668
  return true;
3520
3669
  } catch (err) {
3521
3670
  logger.error("Failed to load graph snapshot, will rebuild", { detail: err instanceof Error ? err.message : String(err) });
@@ -12141,7 +12290,7 @@ function resolveTelemetryLevel() {
12141
12290
  }
12142
12291
  var TELEMETRY_LEVEL = resolveTelemetryLevel();
12143
12292
  var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
12144
- var CTXLOOM_VERSION2 = "1.7.3".length > 0 ? "1.7.3" : "dev";
12293
+ var CTXLOOM_VERSION2 = "1.7.5".length > 0 ? "1.7.5" : "dev";
12145
12294
  var POSTHOG_HOST = "https://eu.i.posthog.com";
12146
12295
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
12147
12296
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -0,0 +1,9 @@
1
+ import {
2
+ VectorStore
3
+ } from "./chunk-XQEQLXY5.js";
4
+ import "./chunk-JZOJC3S7.js";
5
+ import "./chunk-TYDMSHV7.js";
6
+ export {
7
+ VectorStore
8
+ };
9
+ //# sourceMappingURL=VectorStore-5ALWL6XF.js.map
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  VectorStore
3
- } from "./chunk-7S2ELKNU.js";
3
+ } from "./chunk-XQEQLXY5.js";
4
4
  import {
5
- INDEXER_IGNORED_DIRS,
6
5
  collectFiles,
7
- generateEmbedding
8
- } from "./chunk-6FGTNOCP.js";
6
+ generateEmbedding,
7
+ isIgnoredDir
8
+ } from "./chunk-JZOJC3S7.js";
9
9
  import {
10
10
  diskSink,
11
11
  readEvents
@@ -2705,7 +2705,7 @@ var CallGraphIndex = class _CallGraphIndex {
2705
2705
  var TS_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".vue"]);
2706
2706
  var PY_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".ipynb"]);
2707
2707
  var AST_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs", ".java", ".cs", ".rb", ".kt", ".kts", ".swift", ".ipynb", ".php", ".dart"]);
2708
- var CTXLOOM_VERSION = "1.7.3".length > 0 ? "1.7.3" : "dev";
2708
+ var CTXLOOM_VERSION = "1.7.5".length > 0 ? "1.7.5" : "dev";
2709
2709
  var SNAPSHOT_SCHEMA_VERSION = 2;
2710
2710
  function compareCtxloomVersions(snapshotVer, currentVer) {
2711
2711
  if (snapshotVer === currentVer) return "same";
@@ -2762,6 +2762,28 @@ var DependencyGraph = class {
2762
2762
  rootDir = "";
2763
2763
  snapshotDir = "";
2764
2764
  tsPathsResolver = null;
2765
+ /**
2766
+ * fs.watch handle on `.ctxloom/graph-snapshot.json`. When the file is
2767
+ * rewritten externally (e.g. the user runs `ctxloom index` from a
2768
+ * terminal while an MCP server is live), this watcher triggers an
2769
+ * in-memory rehydrate so subsequent tool calls reflect the new graph
2770
+ * without needing the user to restart their MCP client.
2771
+ *
2772
+ * Real repro that motivated this (v1.7.5): EasyMoney user ran
2773
+ * `rm -rf .ctxloom && ctxloom index` from the terminal but the
2774
+ * Claude Desktop MCP server kept serving the pre-wipe in-memory
2775
+ * graph (`Files: 2`) because there was no mechanism to detect the
2776
+ * fresh snapshot on disk. Confusing diagnostic loop.
2777
+ */
2778
+ snapshotWatcher = null;
2779
+ /** Debounce timer for snapshot-change events. */
2780
+ snapshotReloadTimer = null;
2781
+ /**
2782
+ * Tracks the snapshot mtime we last loaded from disk so the watcher
2783
+ * can suppress its own-write echo. saveSnapshot() updates this just
2784
+ * before writing; the watcher ignores any change whose mtime <= this.
2785
+ */
2786
+ lastLoadedSnapshotMtimeMs = 0;
2765
2787
  /**
2766
2788
  * Build the graph from all supported files in rootDir using AST parsing.
2767
2789
  */
@@ -2961,6 +2983,105 @@ var DependencyGraph = class {
2961
2983
  this.snapshotDir = path6.join(rootDir, ".ctxloom");
2962
2984
  return this.loadSnapshot();
2963
2985
  }
2986
+ /**
2987
+ * Start watching `.ctxloom/graph-snapshot.json` for external rewrites.
2988
+ * When the user runs `ctxloom index` (or any other tool) from a
2989
+ * terminal against the same project root, the watcher detects the
2990
+ * new mtime, reloads the snapshot, and atomically swaps the
2991
+ * in-memory graph — so subsequent MCP tool calls see the fresh
2992
+ * graph without requiring an MCP client restart.
2993
+ *
2994
+ * Own writes (via saveSnapshot) update lastLoadedSnapshotMtimeMs
2995
+ * BEFORE the rename completes, so the watcher's own-write echo is
2996
+ * filtered out by the mtime check.
2997
+ *
2998
+ * Debounced 200 ms — matches the FileWatcher cadence and absorbs
2999
+ * the burst of fs.watch events some platforms emit for a single
3000
+ * write (Linux can fire `rename` + `change`, macOS sometimes
3001
+ * fires multiple `change` for atomic rename).
3002
+ *
3003
+ * Idempotent: calling twice is harmless (re-uses the existing
3004
+ * watcher). Call stopSnapshotWatcher() before disposing the graph
3005
+ * to release the FD.
3006
+ */
3007
+ startSnapshotWatcher(debounceMs = 200) {
3008
+ if (this.snapshotWatcher) return;
3009
+ if (!this.snapshotDir) {
3010
+ logger.warn("startSnapshotWatcher: no snapshotDir set, call buildFromDirectory first");
3011
+ return;
3012
+ }
3013
+ const snapshotPath = this.getSnapshotPath();
3014
+ if (!fs6.existsSync(snapshotPath)) {
3015
+ logger.warn("startSnapshotWatcher: snapshot file does not exist yet", { path: snapshotPath });
3016
+ return;
3017
+ }
3018
+ try {
3019
+ this.snapshotWatcher = fs6.watch(snapshotPath, { persistent: false }, (eventType) => {
3020
+ if (this.snapshotReloadTimer) clearTimeout(this.snapshotReloadTimer);
3021
+ this.snapshotReloadTimer = setTimeout(() => {
3022
+ this.snapshotReloadTimer = null;
3023
+ void this.maybeReloadSnapshot(eventType);
3024
+ }, debounceMs);
3025
+ });
3026
+ this.snapshotWatcher.on("error", (err) => {
3027
+ logger.warn("snapshot watcher error", { detail: err instanceof Error ? err.message : String(err) });
3028
+ });
3029
+ logger.info("Graph snapshot hot-reload watcher started", { path: snapshotPath });
3030
+ } catch (err) {
3031
+ logger.warn("startSnapshotWatcher: failed to attach fs.watch", {
3032
+ detail: err instanceof Error ? err.message : String(err)
3033
+ });
3034
+ this.snapshotWatcher = null;
3035
+ }
3036
+ }
3037
+ /**
3038
+ * Stop the snapshot watcher and clear any pending debounce timer.
3039
+ * Call before disposing the graph to release the FD held by fs.watch.
3040
+ * Idempotent.
3041
+ */
3042
+ stopSnapshotWatcher() {
3043
+ if (this.snapshotReloadTimer) {
3044
+ clearTimeout(this.snapshotReloadTimer);
3045
+ this.snapshotReloadTimer = null;
3046
+ }
3047
+ if (this.snapshotWatcher) {
3048
+ try {
3049
+ this.snapshotWatcher.close();
3050
+ } catch {
3051
+ }
3052
+ this.snapshotWatcher = null;
3053
+ }
3054
+ }
3055
+ /**
3056
+ * Internal: called from the debounced watcher. Compares on-disk
3057
+ * mtime against `lastLoadedSnapshotMtimeMs` to filter own-write
3058
+ * echoes, then re-hydrates the in-memory maps from disk via
3059
+ * loadSnapshot(). Atomic from the perspective of MCP tool calls
3060
+ * because Node's event loop ensures loadSnapshot's map assignments
3061
+ * happen in a single tick (no other code interleaves).
3062
+ */
3063
+ async maybeReloadSnapshot(eventType) {
3064
+ const snapshotPath = this.getSnapshotPath();
3065
+ let currentMtime = 0;
3066
+ try {
3067
+ currentMtime = fs6.statSync(snapshotPath).mtimeMs;
3068
+ } catch {
3069
+ return;
3070
+ }
3071
+ if (currentMtime <= this.lastLoadedSnapshotMtimeMs) {
3072
+ return;
3073
+ }
3074
+ const ok = await this.loadSnapshot();
3075
+ if (ok) {
3076
+ logger.info("Graph snapshot hot-reloaded", {
3077
+ event: eventType,
3078
+ files: this.forwardEdges.size,
3079
+ edges: this.edgeCount()
3080
+ });
3081
+ } else {
3082
+ logger.warn("Graph snapshot hot-reload skipped (snapshot invalid or version-stale)");
3083
+ }
3084
+ }
2964
3085
  /**
2965
3086
  * Get files that the given file directly imports.
2966
3087
  */
@@ -3235,6 +3356,10 @@ var DependencyGraph = class {
3235
3356
  const tmpPath = `${snapshotPath}.${process.pid}.tmp`;
3236
3357
  fs6.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
3237
3358
  fs6.renameSync(tmpPath, snapshotPath);
3359
+ try {
3360
+ this.lastLoadedSnapshotMtimeMs = fs6.statSync(snapshotPath).mtimeMs;
3361
+ } catch {
3362
+ }
3238
3363
  const callData = this.callGraphIndex.toJSON();
3239
3364
  const callPath = path6.join(this.snapshotDir, "call-graph-snapshot.json");
3240
3365
  const callTmp = `${callPath}.${process.pid}.tmp`;
@@ -3312,6 +3437,10 @@ var DependencyGraph = class {
3312
3437
  this.callGraphIndex = new CallGraphIndex();
3313
3438
  }
3314
3439
  }
3440
+ try {
3441
+ this.lastLoadedSnapshotMtimeMs = fs6.statSync(snapshotPath).mtimeMs;
3442
+ } catch {
3443
+ }
3315
3444
  return true;
3316
3445
  } catch (err) {
3317
3446
  logger.error("Failed to load graph snapshot, will rebuild", { detail: err instanceof Error ? err.message : String(err) });
@@ -8687,7 +8816,7 @@ function registerFullTextSearchTool(registry, ctx) {
8687
8816
  };
8688
8817
  if (mode === "semantic") {
8689
8818
  try {
8690
- const { generateEmbedding: generateEmbedding2 } = await import("./embedder-2JWDJUE2.js");
8819
+ const { generateEmbedding: generateEmbedding2 } = await import("./embedder-4MM7D3UE.js");
8691
8820
  const store = await ctx.getStore(project_root);
8692
8821
  const embedding = await generateEmbedding2(query);
8693
8822
  const results = await store.search(embedding, limit);
@@ -8724,7 +8853,7 @@ function registerFullTextSearchTool(registry, ctx) {
8724
8853
  let merged = keywordResults.slice(0, limit);
8725
8854
  if (mode === "hybrid") {
8726
8855
  try {
8727
- const { generateEmbedding: generateEmbedding2 } = await import("./embedder-2JWDJUE2.js");
8856
+ const { generateEmbedding: generateEmbedding2 } = await import("./embedder-4MM7D3UE.js");
8728
8857
  const store = await ctx.getStore(project_root);
8729
8858
  const embedding = await generateEmbedding2(query);
8730
8859
  const vectorResults = await store.search(embedding, Math.ceil(limit / 2));
@@ -10599,7 +10728,7 @@ import chokidar from "chokidar";
10599
10728
  function isIgnoredPath(absPath) {
10600
10729
  const segments = absPath.split(/[\\/]/);
10601
10730
  for (const seg of segments) {
10602
- if (INDEXER_IGNORED_DIRS.has(seg)) return true;
10731
+ if (isIgnoredDir(seg)) return true;
10603
10732
  }
10604
10733
  return false;
10605
10734
  }
@@ -10959,7 +11088,7 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
10959
11088
  function getTelemetryLevel() {
10960
11089
  return TELEMETRY_LEVEL;
10961
11090
  }
10962
- var CTXLOOM_VERSION2 = "1.7.3".length > 0 ? "1.7.3" : "dev";
11091
+ var CTXLOOM_VERSION2 = "1.7.5".length > 0 ? "1.7.5" : "dev";
10963
11092
  var POSTHOG_HOST = "https://eu.i.posthog.com";
10964
11093
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
10965
11094
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -11294,6 +11423,11 @@ async function disposeProjectState(state) {
11294
11423
  await state.watcher?.stop();
11295
11424
  } catch {
11296
11425
  }
11426
+ try {
11427
+ const graph = state.graphPromise ? await state.graphPromise.catch(() => null) : null;
11428
+ graph?.stopSnapshotWatcher();
11429
+ } catch {
11430
+ }
11297
11431
  try {
11298
11432
  const store = state.storePromise ? await state.storePromise : null;
11299
11433
  await store?.close();
@@ -12640,4 +12774,4 @@ export {
12640
12774
  skillFilePath,
12641
12775
  installHarness
12642
12776
  };
12643
- //# sourceMappingURL=chunk-KQ5CQLIA.js.map
12777
+ //# sourceMappingURL=chunk-2YELCPTS.js.map
@@ -153,64 +153,16 @@ async function generateEmbeddingBatch(texts) {
153
153
  return data;
154
154
  }
155
155
  function collectFiles(dir, results = []) {
156
- const IGNORED_DIRS = /* @__PURE__ */ new Set([
157
- // Build artifacts + dependency caches
158
- "node_modules",
159
- "dist",
160
- "build",
161
- "out",
162
- "target",
163
- "coverage",
164
- ".cache",
165
- ".turbo",
166
- ".next",
167
- ".nuxt",
168
- // Version control + ctxloom state
169
- ".git",
170
- ".ctxloom",
171
- // Other tools' working state (often contains duplicated source)
172
- ".claude",
173
- ".code-review-graph",
174
- ".vscode-test"
175
- ]);
176
- const SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
177
- ".ts",
178
- ".tsx",
179
- ".js",
180
- ".jsx",
181
- ".mjs",
182
- ".vue",
183
- ".py",
184
- ".rs",
185
- ".go",
186
- ".java",
187
- ".cs",
188
- ".rb",
189
- ".kt",
190
- ".kts",
191
- ".swift",
192
- ".php",
193
- ".dart",
194
- ".c",
195
- ".cpp",
196
- ".h",
197
- ".md",
198
- ".json",
199
- ".yaml",
200
- ".yml",
201
- ".toml",
202
- ".ipynb"
203
- ]);
204
156
  const entries = fs.readdirSync(dir, { withFileTypes: true });
205
157
  for (const entry of entries) {
206
158
  const fullPath = path.join(dir, entry.name);
207
159
  if (entry.isDirectory()) {
208
- if (!IGNORED_DIRS.has(entry.name)) {
160
+ if (!isIgnoredDir(entry.name)) {
209
161
  collectFiles(fullPath, results);
210
162
  }
211
163
  } else if (entry.isFile()) {
212
164
  const ext = path.extname(entry.name);
213
- if (SUPPORTED_EXTENSIONS.has(ext)) {
165
+ if (INDEX_SUPPORTED_EXTENSIONS.has(ext)) {
214
166
  results.push(fullPath);
215
167
  }
216
168
  }
@@ -222,7 +174,7 @@ async function* collectFilesStream(dir) {
222
174
  for (const entry of entries) {
223
175
  const fullPath = path.join(dir, entry.name);
224
176
  if (entry.isDirectory()) {
225
- if (!INDEX_IGNORED_DIRS.has(entry.name)) {
177
+ if (!isIgnoredDir(entry.name)) {
226
178
  yield* collectFilesStream(fullPath);
227
179
  }
228
180
  } else if (entry.isFile()) {
@@ -233,7 +185,7 @@ async function* collectFilesStream(dir) {
233
185
  }
234
186
  }
235
187
  var INDEXER_IGNORED_DIRS = /* @__PURE__ */ new Set([
236
- // Build artifacts + dependency caches
188
+ // Build artifacts + dependency caches (JS/TS, Rust, Java)
237
189
  "node_modules",
238
190
  "dist",
239
191
  "build",
@@ -244,6 +196,21 @@ var INDEXER_IGNORED_DIRS = /* @__PURE__ */ new Set([
244
196
  ".turbo",
245
197
  ".next",
246
198
  ".nuxt",
199
+ // Python virtualenvs + caches. Real-world repro: EasyMoney (a 63-
200
+ // source-file FastAPI project) had a `.venv/` with 8,192 installed-
201
+ // package files. Pre-fix, `ctxloom index` reported 8,120 files /
202
+ // 14,138 edges instead of the expected 63 / 97, because none of the
203
+ // standard Python virtualenv/cache directory names were in this set.
204
+ // The `__pycache__` + `.pytest_cache` + `.ruff_cache` + `.mypy_cache`
205
+ // additions catch the bulk of Python noise on top of the venv.
206
+ ".venv",
207
+ "venv",
208
+ "env",
209
+ "__pycache__",
210
+ ".pytest_cache",
211
+ ".ruff_cache",
212
+ ".mypy_cache",
213
+ ".tox",
247
214
  // Version control + ctxloom state
248
215
  ".git",
249
216
  ".ctxloom",
@@ -252,7 +219,11 @@ var INDEXER_IGNORED_DIRS = /* @__PURE__ */ new Set([
252
219
  ".code-review-graph",
253
220
  ".vscode-test"
254
221
  ]);
255
- var INDEX_IGNORED_DIRS = INDEXER_IGNORED_DIRS;
222
+ function isIgnoredDir(name) {
223
+ if (INDEXER_IGNORED_DIRS.has(name)) return true;
224
+ if (name.endsWith(".egg-info") || name.endsWith(".dist-info")) return true;
225
+ return false;
226
+ }
256
227
  var INDEX_SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
257
228
  ".ts",
258
229
  ".tsx",
@@ -282,7 +253,7 @@ var INDEX_SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
282
253
  ".ipynb"
283
254
  ]);
284
255
  async function indexDirectory(rootDir, onProgress) {
285
- const { VectorStore } = await import("./VectorStore-WDL3H7QT.js");
256
+ const { VectorStore } = await import("./VectorStore-5ALWL6XF.js");
286
257
  const store = new VectorStore(path.join(rootDir, ".ctxloom", "vectors.lancedb"));
287
258
  await store.init();
288
259
  let indexed = 0;
@@ -390,8 +361,9 @@ export {
390
361
  collectFiles,
391
362
  collectFilesStream,
392
363
  INDEXER_IGNORED_DIRS,
364
+ isIgnoredDir,
393
365
  indexDirectory,
394
366
  EMBEDDING_MODEL_ID,
395
367
  getActiveEmbeddingModel
396
368
  };
397
- //# sourceMappingURL=chunk-6FGTNOCP.js.map
369
+ //# sourceMappingURL=chunk-JZOJC3S7.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EMBEDDING_DIMENSION,
3
3
  EMBEDDING_MODEL_ID
4
- } from "./chunk-6FGTNOCP.js";
4
+ } from "./chunk-JZOJC3S7.js";
5
5
  import {
6
6
  logger
7
7
  } from "./chunk-TYDMSHV7.js";
@@ -307,4 +307,4 @@ Or revert CTXLOOM_EMBEDDING_MODEL to "${existing.model}" to keep the existing in
307
307
  export {
308
308
  VectorStore
309
309
  };
310
- //# sourceMappingURL=chunk-7S2ELKNU.js.map
310
+ //# sourceMappingURL=chunk-XQEQLXY5.js.map
@@ -8,8 +8,9 @@ import {
8
8
  generateEmbeddingBatch,
9
9
  getActiveEmbeddingModel,
10
10
  indexDirectory,
11
+ isIgnoredDir,
11
12
  resolveEmbeddingModel
12
- } from "./chunk-6FGTNOCP.js";
13
+ } from "./chunk-JZOJC3S7.js";
13
14
  import "./chunk-TYDMSHV7.js";
14
15
  export {
15
16
  EMBEDDING_DIMENSION,
@@ -21,6 +22,7 @@ export {
21
22
  generateEmbeddingBatch,
22
23
  getActiveEmbeddingModel,
23
24
  indexDirectory,
25
+ isIgnoredDir,
24
26
  resolveEmbeddingModel
25
27
  };
26
- //# sourceMappingURL=embedder-2JWDJUE2.js.map
28
+ //# sourceMappingURL=embedder-4MM7D3UE.js.map
package/dist/index.js CHANGED
@@ -47,19 +47,19 @@ import {
47
47
  validateDefaultRoot,
48
48
  wrapWithIndexingEnvelope,
49
49
  writeCODEOWNERS
50
- } from "./chunk-KQ5CQLIA.js";
50
+ } from "./chunk-2YELCPTS.js";
51
51
  import {
52
52
  addCtxloomToConfig,
53
53
  detectInstalledClients
54
54
  } from "./chunk-YHLMQVBV.js";
55
55
  import {
56
56
  VectorStore
57
- } from "./chunk-7S2ELKNU.js";
57
+ } from "./chunk-XQEQLXY5.js";
58
58
  import {
59
59
  collectFiles,
60
60
  generateEmbedding,
61
61
  indexDirectory
62
- } from "./chunk-6FGTNOCP.js";
62
+ } from "./chunk-JZOJC3S7.js";
63
63
  import "./chunk-5I6CJITG.js";
64
64
  import {
65
65
  logger
@@ -127,6 +127,7 @@ async function initGraph(state) {
127
127
  const graph = new DependencyGraph();
128
128
  graph.setParser(parser);
129
129
  await graph.buildFromDirectory(state.projectRoot);
130
+ graph.startSnapshotWatcher();
130
131
  state.graphInitialized = true;
131
132
  return graph;
132
133
  } catch (err) {
@@ -1067,7 +1068,7 @@ try {
1067
1068
  } catch {
1068
1069
  }
1069
1070
  var args = process.argv.slice(2);
1070
- var ctxloomVersion = "1.7.3".length > 0 ? "1.7.3" : "dev";
1071
+ var ctxloomVersion = "1.7.5".length > 0 ? "1.7.5" : "dev";
1071
1072
  if (args.includes("--version") || args.includes("-v")) {
1072
1073
  process.stdout.write(`ctxloom ${ctxloomVersion}
1073
1074
  `);
@@ -1162,7 +1163,7 @@ async function checkLicense() {
1162
1163
  if (command !== void 0 && LICENSE_GATE_BYPASS_COMMANDS.has(command)) return;
1163
1164
  const ciKey = process.env["CTXLOOM_LICENSE_KEY"];
1164
1165
  if (ciKey) {
1165
- const { ApiClient } = await import("./src-Y4TTG5HV.js");
1166
+ const { ApiClient } = await import("./src-NCWA7NVW.js");
1166
1167
  const client = new ApiClient(process.env["CTXLOOM_API_BASE"]);
1167
1168
  try {
1168
1169
  const result = await client.validate(ciKey, "ci-ephemeral");
@@ -1555,7 +1556,7 @@ async function main() {
1555
1556
  }
1556
1557
  if (!skipHarness) {
1557
1558
  process.stdout.write("\n");
1558
- const { installHarness } = await import("./src-Y4TTG5HV.js");
1559
+ const { installHarness } = await import("./src-NCWA7NVW.js");
1559
1560
  const h = installHarness({ cwd: initRoot, dryRun, force, extraHosts });
1560
1561
  const harnessFiles = [
1561
1562
  h.claudeMd,
@@ -1618,7 +1619,7 @@ async function main() {
1618
1619
  process.exit(1);
1619
1620
  }
1620
1621
  if (alias !== void 0) {
1621
- const { validateAlias } = await import("./src-Y4TTG5HV.js");
1622
+ const { validateAlias } = await import("./src-NCWA7NVW.js");
1622
1623
  const v = validateAlias(alias);
1623
1624
  if (!v.ok) {
1624
1625
  console.error(`[ctxloom] Invalid alias: ${v.reason}`);
@@ -1977,7 +1978,7 @@ Suggested reviewers for ${files.length} file(s):`);
1977
1978
  process.stderr.write("[ctxloom] --limit must be a non-negative integer (0 for unlimited)\n");
1978
1979
  process.exit(2);
1979
1980
  }
1980
- const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-Y4TTG5HV.js");
1981
+ const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-NCWA7NVW.js");
1981
1982
  let config;
1982
1983
  try {
1983
1984
  config = await loadRulesConfig(root);
@@ -2001,7 +2002,7 @@ Suggested reviewers for ${files.length} file(s):`);
2001
2002
  }
2002
2003
  let graph;
2003
2004
  if (useSnapshot) {
2004
- const { DependencyGraph: DG } = await import("./src-Y4TTG5HV.js");
2005
+ const { DependencyGraph: DG } = await import("./src-NCWA7NVW.js");
2005
2006
  graph = new DG();
2006
2007
  const loaded = await graph.loadSnapshotOnly(root);
2007
2008
  if (!loaded) {
@@ -2010,7 +2011,7 @@ Suggested reviewers for ${files.length} file(s):`);
2010
2011
  }
2011
2012
  } else {
2012
2013
  process.stderr.write("[ctxloom] Building dependency graph...\n");
2013
- const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-Y4TTG5HV.js");
2014
+ const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-NCWA7NVW.js");
2014
2015
  let parser;
2015
2016
  try {
2016
2017
  parser = new ASTParser2();
@@ -132,16 +132,18 @@ import {
132
132
  wrapBlock,
133
133
  wrapWithIndexingEnvelope,
134
134
  writeCODEOWNERS
135
- } from "./chunk-KQ5CQLIA.js";
135
+ } from "./chunk-2YELCPTS.js";
136
136
  import {
137
137
  VectorStore
138
- } from "./chunk-7S2ELKNU.js";
138
+ } from "./chunk-XQEQLXY5.js";
139
139
  import {
140
140
  EMBEDDING_DIMENSION,
141
+ INDEXER_IGNORED_DIRS,
141
142
  collectFiles,
142
143
  generateEmbedding,
143
- indexDirectory
144
- } from "./chunk-6FGTNOCP.js";
144
+ indexDirectory,
145
+ isIgnoredDir
146
+ } from "./chunk-JZOJC3S7.js";
145
147
  import {
146
148
  filenameForDate,
147
149
  readEvents,
@@ -182,6 +184,7 @@ export {
182
184
  GrammarLoader,
183
185
  GraphExporter,
184
186
  HOST_ADAPTERS,
187
+ INDEXER_IGNORED_DIRS,
185
188
  InvalidKeyError,
186
189
  LicenseRequiredError,
187
190
  LicenseRevokedError,
@@ -256,6 +259,7 @@ export {
256
259
  inspectVectorsDb,
257
260
  installHarness,
258
261
  isActive,
262
+ isIgnoredDir,
259
263
  isSiloed,
260
264
  learnSuggestionsFromTelemetry,
261
265
  listNamedSnapshots,
@@ -300,4 +304,4 @@ export {
300
304
  wrapWithIndexingEnvelope,
301
305
  writeCODEOWNERS
302
306
  };
303
- //# sourceMappingURL=src-Y4TTG5HV.js.map
307
+ //# sourceMappingURL=src-NCWA7NVW.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  VectorStore
3
- } from "../chunk-7S2ELKNU.js";
3
+ } from "../chunk-XQEQLXY5.js";
4
4
  import {
5
5
  generateEmbedding
6
- } from "../chunk-6FGTNOCP.js";
6
+ } from "../chunk-JZOJC3S7.js";
7
7
  import "../chunk-TYDMSHV7.js";
8
8
 
9
9
  // packages/core/src/workers/indexerWorker.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctxloom-pro",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "ctxloom — The Universal Code Context Engine. A local-first MCP server providing intelligent code context via hybrid Vector + AST + Graph search with Skeletonization (92% token reduction).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,9 +0,0 @@
1
- import {
2
- VectorStore
3
- } from "./chunk-7S2ELKNU.js";
4
- import "./chunk-6FGTNOCP.js";
5
- import "./chunk-TYDMSHV7.js";
6
- export {
7
- VectorStore
8
- };
9
- //# sourceMappingURL=VectorStore-WDL3H7QT.js.map