ctxloom-pro 1.7.4 → 1.7.6
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.
|
|
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.6`) 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.
|
|
386
|
+
- run: npm install -g ctxloom-pro@1.7.6
|
|
387
387
|
- run: ctxloom index
|
|
388
388
|
- run: ctxloom rules check --json
|
|
389
389
|
```
|
|
@@ -2929,7 +2929,7 @@ var CallGraphIndex = class _CallGraphIndex {
|
|
|
2929
2929
|
var TS_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".vue"]);
|
|
2930
2930
|
var PY_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".ipynb"]);
|
|
2931
2931
|
var AST_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs", ".java", ".cs", ".rb", ".kt", ".kts", ".swift", ".ipynb", ".php", ".dart"]);
|
|
2932
|
-
var CTXLOOM_VERSION = "1.7.
|
|
2932
|
+
var CTXLOOM_VERSION = "1.7.6".length > 0 ? "1.7.6" : "dev";
|
|
2933
2933
|
var SNAPSHOT_SCHEMA_VERSION = 2;
|
|
2934
2934
|
function compareCtxloomVersions(snapshotVer, currentVer) {
|
|
2935
2935
|
if (snapshotVer === currentVer) return "same";
|
|
@@ -2986,6 +2986,28 @@ var DependencyGraph = class {
|
|
|
2986
2986
|
rootDir = "";
|
|
2987
2987
|
snapshotDir = "";
|
|
2988
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;
|
|
2989
3011
|
/**
|
|
2990
3012
|
* Build the graph from all supported files in rootDir using AST parsing.
|
|
2991
3013
|
*/
|
|
@@ -3185,6 +3207,118 @@ var DependencyGraph = class {
|
|
|
3185
3207
|
this.snapshotDir = path7.join(rootDir, ".ctxloom");
|
|
3186
3208
|
return this.loadSnapshot();
|
|
3187
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
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* Absolute root directory this graph was built/loaded against.
|
|
3311
|
+
*
|
|
3312
|
+
* Tools that read file CONTENTS off disk (full-text scan, refactor
|
|
3313
|
+
* preview/apply) must join relpaths against THIS root, not against a
|
|
3314
|
+
* server-level default like ctx.projectRoot — otherwise a call that
|
|
3315
|
+
* passes an explicit project_root different from the default reads
|
|
3316
|
+
* from the wrong directory and silently finds nothing. Returns '' if
|
|
3317
|
+
* the graph was never built (defensive; callers should have a graph).
|
|
3318
|
+
*/
|
|
3319
|
+
getRootDir() {
|
|
3320
|
+
return this.rootDir;
|
|
3321
|
+
}
|
|
3188
3322
|
/**
|
|
3189
3323
|
* Get files that the given file directly imports.
|
|
3190
3324
|
*/
|
|
@@ -3459,6 +3593,10 @@ var DependencyGraph = class {
|
|
|
3459
3593
|
const tmpPath = `${snapshotPath}.${process.pid}.tmp`;
|
|
3460
3594
|
fs7.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
3461
3595
|
fs7.renameSync(tmpPath, snapshotPath);
|
|
3596
|
+
try {
|
|
3597
|
+
this.lastLoadedSnapshotMtimeMs = fs7.statSync(snapshotPath).mtimeMs;
|
|
3598
|
+
} catch {
|
|
3599
|
+
}
|
|
3462
3600
|
const callData = this.callGraphIndex.toJSON();
|
|
3463
3601
|
const callPath = path7.join(this.snapshotDir, "call-graph-snapshot.json");
|
|
3464
3602
|
const callTmp = `${callPath}.${process.pid}.tmp`;
|
|
@@ -3536,6 +3674,10 @@ var DependencyGraph = class {
|
|
|
3536
3674
|
this.callGraphIndex = new CallGraphIndex();
|
|
3537
3675
|
}
|
|
3538
3676
|
}
|
|
3677
|
+
try {
|
|
3678
|
+
this.lastLoadedSnapshotMtimeMs = fs7.statSync(snapshotPath).mtimeMs;
|
|
3679
|
+
} catch {
|
|
3680
|
+
}
|
|
3539
3681
|
return true;
|
|
3540
3682
|
} catch (err) {
|
|
3541
3683
|
logger.error("Failed to load graph snapshot, will rebuild", { detail: err instanceof Error ? err.message : String(err) });
|
|
@@ -12161,7 +12303,7 @@ function resolveTelemetryLevel() {
|
|
|
12161
12303
|
}
|
|
12162
12304
|
var TELEMETRY_LEVEL = resolveTelemetryLevel();
|
|
12163
12305
|
var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
|
|
12164
|
-
var CTXLOOM_VERSION2 = "1.7.
|
|
12306
|
+
var CTXLOOM_VERSION2 = "1.7.6".length > 0 ? "1.7.6" : "dev";
|
|
12165
12307
|
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
12166
12308
|
var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
|
|
12167
12309
|
var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
|
|
@@ -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.
|
|
2708
|
+
var CTXLOOM_VERSION = "1.7.6".length > 0 ? "1.7.6" : "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,118 @@ 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
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Absolute root directory this graph was built/loaded against.
|
|
3087
|
+
*
|
|
3088
|
+
* Tools that read file CONTENTS off disk (full-text scan, refactor
|
|
3089
|
+
* preview/apply) must join relpaths against THIS root, not against a
|
|
3090
|
+
* server-level default like ctx.projectRoot — otherwise a call that
|
|
3091
|
+
* passes an explicit project_root different from the default reads
|
|
3092
|
+
* from the wrong directory and silently finds nothing. Returns '' if
|
|
3093
|
+
* the graph was never built (defensive; callers should have a graph).
|
|
3094
|
+
*/
|
|
3095
|
+
getRootDir() {
|
|
3096
|
+
return this.rootDir;
|
|
3097
|
+
}
|
|
2964
3098
|
/**
|
|
2965
3099
|
* Get files that the given file directly imports.
|
|
2966
3100
|
*/
|
|
@@ -3235,6 +3369,10 @@ var DependencyGraph = class {
|
|
|
3235
3369
|
const tmpPath = `${snapshotPath}.${process.pid}.tmp`;
|
|
3236
3370
|
fs6.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
3237
3371
|
fs6.renameSync(tmpPath, snapshotPath);
|
|
3372
|
+
try {
|
|
3373
|
+
this.lastLoadedSnapshotMtimeMs = fs6.statSync(snapshotPath).mtimeMs;
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
3238
3376
|
const callData = this.callGraphIndex.toJSON();
|
|
3239
3377
|
const callPath = path6.join(this.snapshotDir, "call-graph-snapshot.json");
|
|
3240
3378
|
const callTmp = `${callPath}.${process.pid}.tmp`;
|
|
@@ -3312,6 +3450,10 @@ var DependencyGraph = class {
|
|
|
3312
3450
|
this.callGraphIndex = new CallGraphIndex();
|
|
3313
3451
|
}
|
|
3314
3452
|
}
|
|
3453
|
+
try {
|
|
3454
|
+
this.lastLoadedSnapshotMtimeMs = fs6.statSync(snapshotPath).mtimeMs;
|
|
3455
|
+
} catch {
|
|
3456
|
+
}
|
|
3315
3457
|
return true;
|
|
3316
3458
|
} catch (err) {
|
|
3317
3459
|
logger.error("Failed to load graph snapshot, will rebuild", { detail: err instanceof Error ? err.message : String(err) });
|
|
@@ -6242,10 +6384,11 @@ function registerContextPacketTool(registry, ctx) {
|
|
|
6242
6384
|
const primaryContent = pathValidator.readFile(parsed.target_file);
|
|
6243
6385
|
const imports = graph.getImports(parsed.target_file);
|
|
6244
6386
|
const importers = graph.getImporters(parsed.target_file);
|
|
6387
|
+
const rootDir = graph.getRootDir() || ctx.projectRoot;
|
|
6245
6388
|
const skeletons = await Promise.all(
|
|
6246
6389
|
imports.map(async (dep) => {
|
|
6247
6390
|
try {
|
|
6248
|
-
const absDep = path15.resolve(
|
|
6391
|
+
const absDep = path15.resolve(rootDir, dep);
|
|
6249
6392
|
const sk = await skeletonizer.skeletonize(absDep);
|
|
6250
6393
|
return `
|
|
6251
6394
|
<!-- ${dep} -->
|
|
@@ -6896,15 +7039,16 @@ function registerBlastRadiusTool(registry, ctx) {
|
|
|
6896
7039
|
},
|
|
6897
7040
|
async (args) => {
|
|
6898
7041
|
const { changed_files, depth, use_git, detail_level, project_root } = Schema8.parse(args);
|
|
7042
|
+
const graph = await ctx.getGraph(project_root);
|
|
7043
|
+
const gitRoot = graph.getRootDir() || ctx.projectRoot;
|
|
6899
7044
|
let files = changed_files ?? [];
|
|
6900
7045
|
if (files.length === 0 && use_git) {
|
|
6901
|
-
files = await detectChangedFiles(
|
|
7046
|
+
files = await detectChangedFiles(gitRoot);
|
|
6902
7047
|
}
|
|
6903
7048
|
if (files.length === 0) {
|
|
6904
7049
|
return '<blast_radius changed_files="0">\n <!-- No changed files detected -->\n</blast_radius>';
|
|
6905
7050
|
}
|
|
6906
|
-
const
|
|
6907
|
-
const result = await computeBlastRadius({ changedFiles: files, depth, projectRoot: ctx.projectRoot, graph });
|
|
7051
|
+
const result = await computeBlastRadius({ changedFiles: files, depth, projectRoot: gitRoot, graph });
|
|
6908
7052
|
const report = getImpactRadius({ graph, overlay: ctx.overlay, changedFiles: files, depth });
|
|
6909
7053
|
return buildBlastRadiusXml(result, depth, detail_level, report.historicalCoupling);
|
|
6910
7054
|
}
|
|
@@ -7687,10 +7831,10 @@ async function getFileDiff(projectRoot, file) {
|
|
|
7687
7831
|
return "";
|
|
7688
7832
|
}
|
|
7689
7833
|
}
|
|
7690
|
-
async function trySkeletonize(ctx, filePath, projectRoot) {
|
|
7834
|
+
async function trySkeletonize(ctx, filePath, projectRoot, rootDir) {
|
|
7691
7835
|
try {
|
|
7692
7836
|
const sk = await ctx.getSkeletonizer(projectRoot);
|
|
7693
|
-
const absPath = `${
|
|
7837
|
+
const absPath = `${rootDir}/${filePath}`;
|
|
7694
7838
|
return await sk.skeletonize(absPath);
|
|
7695
7839
|
} catch {
|
|
7696
7840
|
return "";
|
|
@@ -7727,13 +7871,15 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
7727
7871
|
async (args) => {
|
|
7728
7872
|
const { changed_files, depth, use_git, include_skeletons, max_diff_lines, project_root } = Schema17.parse(args);
|
|
7729
7873
|
const validator = ctx.getPathValidator(project_root);
|
|
7874
|
+
const graph = await ctx.getGraph(project_root);
|
|
7875
|
+
const gitRoot = graph.getRootDir() || ctx.projectRoot;
|
|
7730
7876
|
let files = (changed_files ?? []).filter((f) => validator.isWithinRoot(f));
|
|
7731
7877
|
if (files.length === 0 && use_git) {
|
|
7732
7878
|
try {
|
|
7733
7879
|
const { stdout } = await execFileAsync(
|
|
7734
7880
|
"git",
|
|
7735
7881
|
["diff", "HEAD~1", "--name-only"],
|
|
7736
|
-
{ cwd:
|
|
7882
|
+
{ cwd: gitRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
7737
7883
|
);
|
|
7738
7884
|
files = stdout.trim().split("\n").filter(Boolean);
|
|
7739
7885
|
} catch {
|
|
@@ -7757,26 +7903,25 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
7757
7903
|
<!-- No changed files detected -->
|
|
7758
7904
|
</git_diff_review>`);
|
|
7759
7905
|
}
|
|
7760
|
-
const graph = await ctx.getGraph(project_root);
|
|
7761
7906
|
const blast = await computeBlastRadius({
|
|
7762
7907
|
changedFiles: files,
|
|
7763
7908
|
depth,
|
|
7764
|
-
projectRoot:
|
|
7909
|
+
projectRoot: gitRoot,
|
|
7765
7910
|
graph
|
|
7766
7911
|
});
|
|
7767
7912
|
const changedFileData = await Promise.all(files.map(async (file) => {
|
|
7768
|
-
const rawDiff = use_git ? await getFileDiff(
|
|
7913
|
+
const rawDiff = use_git ? await getFileDiff(gitRoot, file) : "";
|
|
7769
7914
|
const diffLines = rawDiff ? rawDiff.split("\n") : [];
|
|
7770
7915
|
const truncated = diffLines.length > max_diff_lines;
|
|
7771
7916
|
const diffContent = truncated ? [...diffLines.slice(0, max_diff_lines), `... (${diffLines.length - max_diff_lines} more lines)`].join("\n") : rawDiff;
|
|
7772
|
-
const skeleton = include_skeletons ? await trySkeletonize(ctx, file, project_root) : "";
|
|
7917
|
+
const skeleton = include_skeletons ? await trySkeletonize(ctx, file, project_root, gitRoot) : "";
|
|
7773
7918
|
return { file, diffLines, truncated, diffContent, skeleton };
|
|
7774
7919
|
}));
|
|
7775
7920
|
const skeletonLimit = 5;
|
|
7776
7921
|
const directImporterSkeletons = await Promise.all(
|
|
7777
7922
|
blast.directImporters.map(async (file, i) => ({
|
|
7778
7923
|
file,
|
|
7779
|
-
skeleton: include_skeletons && i < skeletonLimit ? await trySkeletonize(ctx, file, project_root) : ""
|
|
7924
|
+
skeleton: include_skeletons && i < skeletonLimit ? await trySkeletonize(ctx, file, project_root, gitRoot) : ""
|
|
7780
7925
|
}))
|
|
7781
7926
|
);
|
|
7782
7927
|
const render2 = (withSkeletons, withTransitive) => {
|
|
@@ -7920,8 +8065,9 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
7920
8065
|
const candidates = Array.from(candidateSet).slice(0, max_files);
|
|
7921
8066
|
const fileChanges = [];
|
|
7922
8067
|
let totalOccurrences = 0;
|
|
8068
|
+
const rootDir = graph.getRootDir() || ctx.projectRoot;
|
|
7923
8069
|
for (const relPath of candidates) {
|
|
7924
|
-
const absPath = path18.join(
|
|
8070
|
+
const absPath = path18.join(rootDir, relPath);
|
|
7925
8071
|
const occurrences = scanFile(absPath, symbol, new_name);
|
|
7926
8072
|
if (occurrences.length > 0) {
|
|
7927
8073
|
fileChanges.push({ filePath: relPath, occurrences });
|
|
@@ -8438,8 +8584,9 @@ function registerApplyRefactorTool(registry, ctx) {
|
|
|
8438
8584
|
const candidates = Array.from(candidateSet).slice(0, max_files);
|
|
8439
8585
|
const results = [];
|
|
8440
8586
|
let totalOccurrences = 0;
|
|
8587
|
+
const rootDir = graph.getRootDir() || ctx.projectRoot;
|
|
8441
8588
|
for (const relPath of candidates) {
|
|
8442
|
-
const absPath = path20.join(
|
|
8589
|
+
const absPath = path20.join(rootDir, relPath);
|
|
8443
8590
|
const count = applyToFile(absPath, symbol, new_name, dry_run);
|
|
8444
8591
|
if (count > 0) {
|
|
8445
8592
|
results.push({ filePath: relPath, occurrences: count, written: !dry_run });
|
|
@@ -8530,14 +8677,15 @@ function registerDetectChangesTool(registry, ctx) {
|
|
|
8530
8677
|
},
|
|
8531
8678
|
async (args) => {
|
|
8532
8679
|
const { changed_files, use_git, detail_level, project_root } = Schema22.parse(args);
|
|
8680
|
+
const graph = await ctx.getGraph(project_root);
|
|
8681
|
+
const gitRoot = graph.getRootDir() || ctx.projectRoot;
|
|
8533
8682
|
let files = changed_files ?? [];
|
|
8534
8683
|
if (files.length === 0 && use_git) {
|
|
8535
|
-
files = await detectChangedFiles2(
|
|
8684
|
+
files = await detectChangedFiles2(gitRoot);
|
|
8536
8685
|
}
|
|
8537
8686
|
if (files.length === 0) {
|
|
8538
8687
|
return '<detect_changes count="0">\n <!-- No changed files detected -->\n</detect_changes>';
|
|
8539
8688
|
}
|
|
8540
|
-
const graph = await ctx.getGraph(project_root);
|
|
8541
8689
|
const { changedFiles: scored, summary } = detectChanges({
|
|
8542
8690
|
graph,
|
|
8543
8691
|
overlay: ctx.overlay,
|
|
@@ -8707,9 +8855,10 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
8707
8855
|
}
|
|
8708
8856
|
const graph = await ctx.getGraph(project_root);
|
|
8709
8857
|
const files = graph.allFiles();
|
|
8858
|
+
const rootDir = graph.getRootDir() || ctx.projectRoot;
|
|
8710
8859
|
const keywordResults = [];
|
|
8711
8860
|
for (const relPath of files) {
|
|
8712
|
-
const absPath = path21.join(
|
|
8861
|
+
const absPath = path21.join(rootDir, relPath);
|
|
8713
8862
|
const hit = scanFile2(absPath, pattern, context_lines);
|
|
8714
8863
|
if (hit) {
|
|
8715
8864
|
keywordResults.push({
|
|
@@ -8810,14 +8959,15 @@ function registerSuggestedQuestionsTool(registry, ctx) {
|
|
|
8810
8959
|
},
|
|
8811
8960
|
async (args) => {
|
|
8812
8961
|
const { changed_files, use_git, project_root } = Schema24.parse(args);
|
|
8962
|
+
const graph = await ctx.getGraph(project_root);
|
|
8963
|
+
const gitRoot = graph.getRootDir() || ctx.projectRoot;
|
|
8813
8964
|
let files = changed_files ?? [];
|
|
8814
8965
|
if (files.length === 0 && use_git) {
|
|
8815
|
-
files = await detectChangedFiles3(
|
|
8966
|
+
files = await detectChangedFiles3(gitRoot);
|
|
8816
8967
|
}
|
|
8817
8968
|
if (files.length === 0) {
|
|
8818
8969
|
return '<suggested_questions count="1" changed_files="0"><question>No changed files detected. Are you on a git branch with commits?</question></suggested_questions>';
|
|
8819
8970
|
}
|
|
8820
|
-
const graph = await ctx.getGraph(project_root);
|
|
8821
8971
|
const questions = [];
|
|
8822
8972
|
const allImporters = /* @__PURE__ */ new Set();
|
|
8823
8973
|
const hubFiles = [];
|
|
@@ -9727,14 +9877,15 @@ function registerGetAffectedFlowsTool(registry, ctx) {
|
|
|
9727
9877
|
},
|
|
9728
9878
|
async (args) => {
|
|
9729
9879
|
const { changed_files, use_git, depth, max_flows, max_steps_per_flow, project_root } = Schema29.parse(args);
|
|
9880
|
+
const graph = await ctx.getGraph(project_root);
|
|
9881
|
+
const gitRoot = graph.getRootDir() || ctx.projectRoot;
|
|
9730
9882
|
let files = changed_files ?? [];
|
|
9731
9883
|
if (files.length === 0 && use_git) {
|
|
9732
|
-
files = await detectChangedFiles4(
|
|
9884
|
+
files = await detectChangedFiles4(gitRoot);
|
|
9733
9885
|
}
|
|
9734
9886
|
if (files.length === 0) {
|
|
9735
9887
|
return '<affected_flows changed_files="0" total_flows="0">\n <!-- No changed files detected -->\n</affected_flows>';
|
|
9736
9888
|
}
|
|
9737
|
-
const graph = await ctx.getGraph(project_root);
|
|
9738
9889
|
const callIdx = graph.getCallGraphIndex();
|
|
9739
9890
|
const changedSymbols = [];
|
|
9740
9891
|
for (const file of files) {
|
|
@@ -10959,7 +11110,7 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
|
|
|
10959
11110
|
function getTelemetryLevel() {
|
|
10960
11111
|
return TELEMETRY_LEVEL;
|
|
10961
11112
|
}
|
|
10962
|
-
var CTXLOOM_VERSION2 = "1.7.
|
|
11113
|
+
var CTXLOOM_VERSION2 = "1.7.6".length > 0 ? "1.7.6" : "dev";
|
|
10963
11114
|
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
10964
11115
|
var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
|
|
10965
11116
|
var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
|
|
@@ -11294,6 +11445,11 @@ async function disposeProjectState(state) {
|
|
|
11294
11445
|
await state.watcher?.stop();
|
|
11295
11446
|
} catch {
|
|
11296
11447
|
}
|
|
11448
|
+
try {
|
|
11449
|
+
const graph = state.graphPromise ? await state.graphPromise.catch(() => null) : null;
|
|
11450
|
+
graph?.stopSnapshotWatcher();
|
|
11451
|
+
} catch {
|
|
11452
|
+
}
|
|
11297
11453
|
try {
|
|
11298
11454
|
const store = state.storePromise ? await state.storePromise : null;
|
|
11299
11455
|
await store?.close();
|
|
@@ -12640,4 +12796,4 @@ export {
|
|
|
12640
12796
|
skillFilePath,
|
|
12641
12797
|
installHarness
|
|
12642
12798
|
};
|
|
12643
|
-
//# sourceMappingURL=chunk-
|
|
12799
|
+
//# sourceMappingURL=chunk-ISXDIRSN.js.map
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
validateDefaultRoot,
|
|
48
48
|
wrapWithIndexingEnvelope,
|
|
49
49
|
writeCODEOWNERS
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-ISXDIRSN.js";
|
|
51
51
|
import {
|
|
52
52
|
addCtxloomToConfig,
|
|
53
53
|
detectInstalledClients
|
|
@@ -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.
|
|
1071
|
+
var ctxloomVersion = "1.7.6".length > 0 ? "1.7.6" : "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-
|
|
1166
|
+
const { ApiClient } = await import("./src-FB2SJJZR.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-
|
|
1559
|
+
const { installHarness } = await import("./src-FB2SJJZR.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-
|
|
1622
|
+
const { validateAlias } = await import("./src-FB2SJJZR.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-
|
|
1981
|
+
const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-FB2SJJZR.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-
|
|
2005
|
+
const { DependencyGraph: DG } = await import("./src-FB2SJJZR.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-
|
|
2014
|
+
const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-FB2SJJZR.js");
|
|
2014
2015
|
let parser;
|
|
2015
2016
|
try {
|
|
2016
2017
|
parser = new ASTParser2();
|
|
@@ -132,7 +132,7 @@ import {
|
|
|
132
132
|
wrapBlock,
|
|
133
133
|
wrapWithIndexingEnvelope,
|
|
134
134
|
writeCODEOWNERS
|
|
135
|
-
} from "./chunk-
|
|
135
|
+
} from "./chunk-ISXDIRSN.js";
|
|
136
136
|
import {
|
|
137
137
|
VectorStore
|
|
138
138
|
} from "./chunk-XQEQLXY5.js";
|
|
@@ -304,4 +304,4 @@ export {
|
|
|
304
304
|
wrapWithIndexingEnvelope,
|
|
305
305
|
writeCODEOWNERS
|
|
306
306
|
};
|
|
307
|
-
//# sourceMappingURL=src-
|
|
307
|
+
//# sourceMappingURL=src-FB2SJJZR.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ctxloom-pro",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.6",
|
|
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",
|