ctxloom-pro 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -630,6 +630,19 @@ Token counts use the standard 4 chars/token approximation. Per-repo range (57–
630
630
  | `CTXLOOM_GRAMMAR_CDN` | CDN base URL for grammar downloads (air-gapped environments) | Built-in |
631
631
  | `CTXLOOM_MAX_PROJECTS` | LRU cache cap for multi-project state (v1.1.0+) | `5` |
632
632
  | `CTXLOOM_DISABLE_MULTIPROJECT` | Set to `1` to revert to v1.0.31 single-project mode (v1.1.0+) | (unset) |
633
+ | `CTXLOOM_NO_TELEMETRY` | Set to `1` to disable anonymous telemetry entirely (v1.2.0+) | (unset) |
634
+ | `CTXLOOM_TELEMETRY_LEVEL` | `all` / `error` / `off` — granular telemetry scope (v1.2.0+) | `all` |
635
+ | `DO_NOT_TRACK` | Universal cross-tool opt-out — equivalent to `CTXLOOM_NO_TELEMETRY=1` | (unset) |
636
+
637
+ ---
638
+
639
+ ## Telemetry
640
+
641
+ ctxloom collects **anonymous, opt-out telemetry** to understand which features are used and to catch crashes. **No file contents, paths, project names, or aliases are ever transmitted.** Project identifiers are SHA-256 hashes of the absolute path. The `distinct_id` is a random UUID at `~/.ctxloom/distinct_id`.
642
+
643
+ Disable with `CTXLOOM_NO_TELEMETRY=1` or the cross-tool `DO_NOT_TRACK=1`. For a granular middle ground (crash reports yes, usage analytics no) use `CTXLOOM_TELEMETRY_LEVEL=error`.
644
+
645
+ The complete list of events, properties, what is *never* collected, and how project paths are anonymized is documented in **[docs/TELEMETRY.md](docs/TELEMETRY.md)**.
633
646
 
634
647
  ---
635
648
 
@@ -169,12 +169,12 @@ var init_VectorStore = __esm({
169
169
  // server/index.ts
170
170
  import express from "express";
171
171
  import cors from "cors";
172
- import path42 from "path";
172
+ import path43 from "path";
173
173
  import fs32 from "fs";
174
174
  import { fileURLToPath as fileURLToPath2 } from "url";
175
175
 
176
176
  // server/loader.ts
177
- import path36 from "path";
177
+ import path37 from "path";
178
178
 
179
179
  // ../../packages/core/src/graph/DependencyGraph.ts
180
180
  import fs7 from "fs";
@@ -3133,8 +3133,8 @@ var CoChangeIndex = class _CoChangeIndex {
3133
3133
  if (event.isBulk || event.isMerge) return;
3134
3134
  const paths = event.files.map((f) => f.path);
3135
3135
  if (paths.length === 0) return;
3136
- for (const path43 of paths) {
3137
- this.nodeCounts.set(path43, (this.nodeCounts.get(path43) ?? 0) + 1);
3136
+ for (const path44 of paths) {
3137
+ this.nodeCounts.set(path44, (this.nodeCounts.get(path44) ?? 0) + 1);
3138
3138
  }
3139
3139
  for (let i = 0; i < paths.length; i++) {
3140
3140
  for (let j = i + 1; j < paths.length; j++) {
@@ -3281,8 +3281,8 @@ var ChurnIndex = class _ChurnIndex {
3281
3281
  */
3282
3282
  snapshot() {
3283
3283
  const nodes = {};
3284
- for (const [path43, raw] of this.nodes) {
3285
- nodes[path43] = {
3284
+ for (const [path44, raw] of this.nodes) {
3285
+ nodes[path44] = {
3286
3286
  commits: raw.commits,
3287
3287
  churnLines: raw.churnLines,
3288
3288
  bugCommits: raw.bugCommits,
@@ -3297,8 +3297,8 @@ var ChurnIndex = class _ChurnIndex {
3297
3297
  */
3298
3298
  static load(s) {
3299
3299
  const idx = new _ChurnIndex();
3300
- for (const [path43, raw] of Object.entries(s.nodes)) {
3301
- idx.nodes.set(path43, {
3300
+ for (const [path44, raw] of Object.entries(s.nodes)) {
3301
+ idx.nodes.set(path44, {
3302
3302
  commits: raw.commits,
3303
3303
  churnLines: raw.churnLines,
3304
3304
  bugCommits: raw.bugCommits,
@@ -3311,8 +3311,8 @@ var ChurnIndex = class _ChurnIndex {
3311
3311
  // -------------------------------------------------------------------------
3312
3312
  // Private helpers
3313
3313
  // -------------------------------------------------------------------------
3314
- getOrCreate(path43) {
3315
- const existing = this.nodes.get(path43);
3314
+ getOrCreate(path44) {
3315
+ const existing = this.nodes.get(path44);
3316
3316
  if (existing !== void 0) return existing;
3317
3317
  const fresh = {
3318
3318
  commits: 0,
@@ -3321,7 +3321,7 @@ var ChurnIndex = class _ChurnIndex {
3321
3321
  authorCounts: {},
3322
3322
  lastTouch: 0
3323
3323
  };
3324
- this.nodes.set(path43, fresh);
3324
+ this.nodes.set(path44, fresh);
3325
3325
  return fresh;
3326
3326
  }
3327
3327
  };
@@ -3402,12 +3402,12 @@ var OwnershipIndex = class _OwnershipIndex {
3402
3402
  */
3403
3403
  snapshot() {
3404
3404
  const nodes = {};
3405
- for (const [path43, raw] of this.nodes) {
3405
+ for (const [path44, raw] of this.nodes) {
3406
3406
  const authorWeights = {};
3407
3407
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3408
3408
  authorWeights[email] = { ...entry };
3409
3409
  }
3410
- nodes[path43] = { authorWeights, lastTouch: raw.lastTouch };
3410
+ nodes[path44] = { authorWeights, lastTouch: raw.lastTouch };
3411
3411
  }
3412
3412
  return { version: 1, nodes };
3413
3413
  }
@@ -3416,23 +3416,23 @@ var OwnershipIndex = class _OwnershipIndex {
3416
3416
  */
3417
3417
  static load(s) {
3418
3418
  const idx = new _OwnershipIndex();
3419
- for (const [path43, raw] of Object.entries(s.nodes)) {
3419
+ for (const [path44, raw] of Object.entries(s.nodes)) {
3420
3420
  const authorWeights = {};
3421
3421
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3422
3422
  authorWeights[email] = { ...entry };
3423
3423
  }
3424
- idx.nodes.set(path43, { authorWeights, lastTouch: raw.lastTouch });
3424
+ idx.nodes.set(path44, { authorWeights, lastTouch: raw.lastTouch });
3425
3425
  }
3426
3426
  return idx;
3427
3427
  }
3428
3428
  // -------------------------------------------------------------------------
3429
3429
  // Private helpers
3430
3430
  // -------------------------------------------------------------------------
3431
- getOrCreate(path43) {
3432
- const existing = this.nodes.get(path43);
3431
+ getOrCreate(path44) {
3432
+ const existing = this.nodes.get(path44);
3433
3433
  if (existing !== void 0) return existing;
3434
3434
  const fresh = { authorWeights: {}, lastTouch: 0 };
3435
- this.nodes.set(path43, fresh);
3435
+ this.nodes.set(path44, fresh);
3436
3436
  return fresh;
3437
3437
  }
3438
3438
  };
@@ -4670,8 +4670,8 @@ function getErrorMap() {
4670
4670
 
4671
4671
  // ../../node_modules/zod/v3/helpers/parseUtil.js
4672
4672
  var makeIssue = (params) => {
4673
- const { data, path: path43, errorMaps, issueData } = params;
4674
- const fullPath = [...path43, ...issueData.path || []];
4673
+ const { data, path: path44, errorMaps, issueData } = params;
4674
+ const fullPath = [...path44, ...issueData.path || []];
4675
4675
  const fullIssue = {
4676
4676
  ...issueData,
4677
4677
  path: fullPath
@@ -4787,11 +4787,11 @@ var errorUtil;
4787
4787
 
4788
4788
  // ../../node_modules/zod/v3/types.js
4789
4789
  var ParseInputLazyPath = class {
4790
- constructor(parent, value, path43, key) {
4790
+ constructor(parent, value, path44, key) {
4791
4791
  this._cachedPath = [];
4792
4792
  this.parent = parent;
4793
4793
  this.data = value;
4794
- this._path = path43;
4794
+ this._path = path44;
4795
4795
  this._key = key;
4796
4796
  }
4797
4797
  get path() {
@@ -11311,8 +11311,17 @@ function markAliasSent(home) {
11311
11311
  }
11312
11312
 
11313
11313
  // ../../packages/core/src/license/telemetry.ts
11314
- var TELEMETRY_DISABLED = process.env["CTXLOOM_NO_TELEMETRY"] === "1" || process.env["DO_NOT_TRACK"] === "1";
11315
- var CTXLOOM_VERSION = "1.1.5".length > 0 ? "1.1.5" : "dev";
11314
+ function resolveTelemetryLevel() {
11315
+ if (process.env["CTXLOOM_NO_TELEMETRY"] === "1" || process.env["DO_NOT_TRACK"] === "1") {
11316
+ return "off";
11317
+ }
11318
+ const raw = process.env["CTXLOOM_TELEMETRY_LEVEL"]?.toLowerCase();
11319
+ if (raw === "off" || raw === "error" || raw === "all") return raw;
11320
+ return "all";
11321
+ }
11322
+ var TELEMETRY_LEVEL = resolveTelemetryLevel();
11323
+ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
11324
+ var CTXLOOM_VERSION = "1.2.0".length > 0 ? "1.2.0" : "dev";
11316
11325
  var POSTHOG_HOST = "https://eu.i.posthog.com";
11317
11326
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
11318
11327
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -11327,7 +11336,7 @@ function resolveDistinctId() {
11327
11336
  }
11328
11337
  }
11329
11338
  function track(event, props = {}) {
11330
- if (TELEMETRY_DISABLED || !POSTHOG_KEY) return;
11339
+ if (TELEMETRY_LEVEL !== "all" || !POSTHOG_KEY) return;
11331
11340
  const record = resolveDistinctId();
11332
11341
  if (!record) return;
11333
11342
  void sendPostHog(event, record.id, props);
@@ -11443,23 +11452,28 @@ function parseStack(stack) {
11443
11452
  }).filter((f) => f !== null).slice(0, 20);
11444
11453
  }
11445
11454
 
11455
+ // ../../packages/core/src/license/TelemetryNotice.ts
11456
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
11457
+ import path33 from "path";
11458
+ import os5 from "os";
11459
+
11446
11460
  // ../../packages/core/src/server/ProjectState.ts
11447
- import path34 from "path";
11461
+ import path35 from "path";
11448
11462
 
11449
11463
  // ../../packages/core/src/server/projectId.ts
11450
11464
  import crypto5 from "crypto";
11451
- import path33 from "path";
11465
+ import path34 from "path";
11452
11466
 
11453
11467
  // ../../packages/core/src/server/ProjectStateManager.ts
11454
11468
  init_logger();
11455
11469
 
11456
11470
  // ../../packages/core/src/server/resolveProjectRoot.ts
11457
11471
  import fs29 from "fs";
11458
- import path35 from "path";
11472
+ import path36 from "path";
11459
11473
 
11460
11474
  // server/loader.ts
11461
11475
  async function loadContext(root) {
11462
- const absRoot = path36.resolve(root);
11476
+ const absRoot = path37.resolve(root);
11463
11477
  const overlay = new GitOverlayStore(absRoot);
11464
11478
  const gitEnabled = await overlay.loadSnapshot();
11465
11479
  const graph = new DependencyGraph();
@@ -11713,20 +11727,20 @@ function buildOwnershipRouter(ctx) {
11713
11727
  // server/routes/file.ts
11714
11728
  import { Router as Router7 } from "express";
11715
11729
  import fs30 from "fs/promises";
11716
- import path37 from "path";
11730
+ import path38 from "path";
11717
11731
  function buildFileRouter(ctx) {
11718
11732
  const router = Router7();
11719
11733
  router.get("/", async (req, res) => {
11720
11734
  const rel = req.query.path;
11721
11735
  if (!rel) return res.status(400).json({ error: "missing path" });
11722
- const abs = path37.resolve(ctx.root, rel);
11723
- const rootBoundary = ctx.root.endsWith(path37.sep) ? ctx.root : ctx.root + path37.sep;
11736
+ const abs = path38.resolve(ctx.root, rel);
11737
+ const rootBoundary = ctx.root.endsWith(path38.sep) ? ctx.root : ctx.root + path38.sep;
11724
11738
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11725
11739
  return res.status(403).json({ error: "forbidden" });
11726
11740
  }
11727
11741
  try {
11728
11742
  const content = await fs30.readFile(abs, "utf-8");
11729
- const ext = path37.extname(abs).slice(1);
11743
+ const ext = path38.extname(abs).slice(1);
11730
11744
  res.json({ content, lines: content.split("\n").length, ext });
11731
11745
  } catch {
11732
11746
  res.status(404).json({ error: "not found" });
@@ -11738,7 +11752,7 @@ function buildFileRouter(ctx) {
11738
11752
  // server/routes/open.ts
11739
11753
  import { Router as Router8 } from "express";
11740
11754
  import { execFile as execFile2 } from "child_process";
11741
- import path38 from "path";
11755
+ import path39 from "path";
11742
11756
  function tryOpen(bin, abs) {
11743
11757
  return new Promise((resolve) => {
11744
11758
  execFile2(bin, [abs], { timeout: 5e3 }, (err) => resolve(!err));
@@ -11749,8 +11763,8 @@ function buildOpenRouter(ctx) {
11749
11763
  router.post("/", async (req, res) => {
11750
11764
  const rel = req.body?.path;
11751
11765
  if (!rel || typeof rel !== "string") return res.status(400).json({ error: "missing path" });
11752
- const abs = path38.resolve(ctx.root, rel);
11753
- const rootBoundary = ctx.root.endsWith(path38.sep) ? ctx.root : ctx.root + path38.sep;
11766
+ const abs = path39.resolve(ctx.root, rel);
11767
+ const rootBoundary = ctx.root.endsWith(path39.sep) ? ctx.root : ctx.root + path39.sep;
11754
11768
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11755
11769
  return res.status(403).json({ error: "forbidden" });
11756
11770
  }
@@ -11762,7 +11776,7 @@ function buildOpenRouter(ctx) {
11762
11776
 
11763
11777
  // server/routes/tokens.ts
11764
11778
  import { Router as Router9 } from "express";
11765
- import path39 from "path";
11779
+ import path40 from "path";
11766
11780
  import fs31 from "fs";
11767
11781
  var CHARS_PER_TOKEN = 4;
11768
11782
  var cache = null;
@@ -11778,7 +11792,7 @@ function buildTokensRouter(ctx) {
11778
11792
  let fullChars = 0;
11779
11793
  let skeletonChars = 0;
11780
11794
  for (const file of files) {
11781
- const absPath = path39.join(ctx.root, file);
11795
+ const absPath = path40.join(ctx.root, file);
11782
11796
  try {
11783
11797
  const content = fs31.readFileSync(absPath, "utf-8");
11784
11798
  fullChars += content.length;
@@ -11881,21 +11895,21 @@ function buildFileTrendsRouter(ctx) {
11881
11895
 
11882
11896
  // server/routes/projects.ts
11883
11897
  import { Router as Router12 } from "express";
11884
- import path41 from "path";
11898
+ import path42 from "path";
11885
11899
 
11886
11900
  // server/projects.ts
11887
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
11888
- import os5 from "os";
11889
- import path40 from "path";
11901
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
11902
+ import os6 from "os";
11903
+ import path41 from "path";
11890
11904
  import crypto6 from "crypto";
11891
- var HOME = os5.homedir();
11892
- var REGISTRY_PATH = path40.join(HOME, ".ctxloom", "repos.json");
11905
+ var HOME = os6.homedir();
11906
+ var REGISTRY_PATH = path41.join(HOME, ".ctxloom", "repos.json");
11893
11907
  function slugFor(root) {
11894
- const abs = path40.resolve(root);
11908
+ const abs = path41.resolve(root);
11895
11909
  return crypto6.createHash("sha1").update(abs).digest("hex").slice(0, 12);
11896
11910
  }
11897
11911
  function readRegistry() {
11898
- if (!existsSync3(REGISTRY_PATH)) return [];
11912
+ if (!existsSync4(REGISTRY_PATH)) return [];
11899
11913
  try {
11900
11914
  const raw = readFileSync4(REGISTRY_PATH, "utf-8");
11901
11915
  const parsed = JSON.parse(raw);
@@ -11906,27 +11920,27 @@ function readRegistry() {
11906
11920
  }
11907
11921
  }
11908
11922
  function listProjects(defaultRoot) {
11909
- const absDefault = path40.resolve(defaultRoot);
11923
+ const absDefault = path41.resolve(defaultRoot);
11910
11924
  const out = [
11911
11925
  {
11912
11926
  slug: slugFor(absDefault),
11913
- name: path40.basename(absDefault) || absDefault,
11927
+ name: path41.basename(absDefault) || absDefault,
11914
11928
  root: absDefault,
11915
11929
  isDefault: true,
11916
- hasSnapshot: existsSync3(path40.join(absDefault, ".ctxloom"))
11930
+ hasSnapshot: existsSync4(path41.join(absDefault, ".ctxloom"))
11917
11931
  }
11918
11932
  ];
11919
11933
  const seen = /* @__PURE__ */ new Set([absDefault]);
11920
11934
  for (const entry of readRegistry()) {
11921
- const abs = path40.resolve(entry.root);
11935
+ const abs = path41.resolve(entry.root);
11922
11936
  if (seen.has(abs)) continue;
11923
11937
  seen.add(abs);
11924
11938
  const item = {
11925
11939
  slug: slugFor(abs),
11926
- name: entry.name ?? (path40.basename(abs) || abs),
11940
+ name: entry.name ?? (path41.basename(abs) || abs),
11927
11941
  root: abs,
11928
11942
  isDefault: false,
11929
- hasSnapshot: existsSync3(path40.join(abs, ".ctxloom"))
11943
+ hasSnapshot: existsSync4(path41.join(abs, ".ctxloom"))
11930
11944
  };
11931
11945
  if (entry.alias !== void 0) item.alias = entry.alias;
11932
11946
  out.push(item);
@@ -11979,7 +11993,7 @@ function buildProjectsRouter(deps) {
11979
11993
  } catch (err) {
11980
11994
  const detail = err instanceof Error ? err.message : String(err);
11981
11995
  res.status(500).json({
11982
- error: `failed to switch to ${path41.basename(target.root)}: ${detail}`
11996
+ error: `failed to switch to ${path42.basename(target.root)}: ${detail}`
11983
11997
  });
11984
11998
  }
11985
11999
  });
@@ -12036,7 +12050,7 @@ function buildTelemetryRouter() {
12036
12050
  }
12037
12051
 
12038
12052
  // server/index.ts
12039
- var __dirname2 = path42.dirname(fileURLToPath2(import.meta.url));
12053
+ var __dirname2 = path43.dirname(fileURLToPath2(import.meta.url));
12040
12054
  async function startDashboard(options) {
12041
12055
  const { root, port, open } = options;
12042
12056
  console.log(`ctxloom dashboard \u2014 loading context from ${root}...`);
@@ -12095,7 +12109,7 @@ async function startDashboard(options) {
12095
12109
  }
12096
12110
  activeWatcher = null;
12097
12111
  }
12098
- const snapshotDir = path42.join(targetRoot, ".ctxloom");
12112
+ const snapshotDir = path43.join(targetRoot, ".ctxloom");
12099
12113
  try {
12100
12114
  activeWatcher = fs32.watch(snapshotDir, (_event, filename) => {
12101
12115
  if (!filename || !filename.includes("snapshot")) return;
@@ -12126,12 +12140,12 @@ async function startDashboard(options) {
12126
12140
  attachSnapshotWatcher(newRoot);
12127
12141
  }
12128
12142
  }));
12129
- const clientDist = path42.join(__dirname2, "../dashboard/client");
12130
- const clientDistExists = fs32.existsSync(path42.join(clientDist, "index.html"));
12143
+ const clientDist = path43.join(__dirname2, "../dashboard/client");
12144
+ const clientDistExists = fs32.existsSync(path43.join(clientDist, "index.html"));
12131
12145
  if (clientDistExists) {
12132
12146
  app.use(express.static(clientDist, { dotfiles: "allow" }));
12133
12147
  app.get(/.*/, (_req, res) => {
12134
- res.sendFile(path42.join(clientDist, "index.html"), { dotfiles: "allow" });
12148
+ res.sendFile(path43.join(clientDist, "index.html"), { dotfiles: "allow" });
12135
12149
  });
12136
12150
  } else {
12137
12151
  app.get(/^\/(?!api\/).*/, (_req, res) => {
@@ -3406,8 +3406,8 @@ var CoChangeIndex = class _CoChangeIndex {
3406
3406
  if (event.isBulk || event.isMerge) return;
3407
3407
  const paths = event.files.map((f) => f.path);
3408
3408
  if (paths.length === 0) return;
3409
- for (const path34 of paths) {
3410
- this.nodeCounts.set(path34, (this.nodeCounts.get(path34) ?? 0) + 1);
3409
+ for (const path35 of paths) {
3410
+ this.nodeCounts.set(path35, (this.nodeCounts.get(path35) ?? 0) + 1);
3411
3411
  }
3412
3412
  for (let i = 0; i < paths.length; i++) {
3413
3413
  for (let j = i + 1; j < paths.length; j++) {
@@ -3554,8 +3554,8 @@ var ChurnIndex = class _ChurnIndex {
3554
3554
  */
3555
3555
  snapshot() {
3556
3556
  const nodes = {};
3557
- for (const [path34, raw] of this.nodes) {
3558
- nodes[path34] = {
3557
+ for (const [path35, raw] of this.nodes) {
3558
+ nodes[path35] = {
3559
3559
  commits: raw.commits,
3560
3560
  churnLines: raw.churnLines,
3561
3561
  bugCommits: raw.bugCommits,
@@ -3570,8 +3570,8 @@ var ChurnIndex = class _ChurnIndex {
3570
3570
  */
3571
3571
  static load(s) {
3572
3572
  const idx = new _ChurnIndex();
3573
- for (const [path34, raw] of Object.entries(s.nodes)) {
3574
- idx.nodes.set(path34, {
3573
+ for (const [path35, raw] of Object.entries(s.nodes)) {
3574
+ idx.nodes.set(path35, {
3575
3575
  commits: raw.commits,
3576
3576
  churnLines: raw.churnLines,
3577
3577
  bugCommits: raw.bugCommits,
@@ -3584,8 +3584,8 @@ var ChurnIndex = class _ChurnIndex {
3584
3584
  // -------------------------------------------------------------------------
3585
3585
  // Private helpers
3586
3586
  // -------------------------------------------------------------------------
3587
- getOrCreate(path34) {
3588
- const existing = this.nodes.get(path34);
3587
+ getOrCreate(path35) {
3588
+ const existing = this.nodes.get(path35);
3589
3589
  if (existing !== void 0) return existing;
3590
3590
  const fresh = {
3591
3591
  commits: 0,
@@ -3594,7 +3594,7 @@ var ChurnIndex = class _ChurnIndex {
3594
3594
  authorCounts: {},
3595
3595
  lastTouch: 0
3596
3596
  };
3597
- this.nodes.set(path34, fresh);
3597
+ this.nodes.set(path35, fresh);
3598
3598
  return fresh;
3599
3599
  }
3600
3600
  };
@@ -3675,12 +3675,12 @@ var OwnershipIndex = class _OwnershipIndex {
3675
3675
  */
3676
3676
  snapshot() {
3677
3677
  const nodes = {};
3678
- for (const [path34, raw] of this.nodes) {
3678
+ for (const [path35, raw] of this.nodes) {
3679
3679
  const authorWeights = {};
3680
3680
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3681
3681
  authorWeights[email] = { ...entry };
3682
3682
  }
3683
- nodes[path34] = { authorWeights, lastTouch: raw.lastTouch };
3683
+ nodes[path35] = { authorWeights, lastTouch: raw.lastTouch };
3684
3684
  }
3685
3685
  return { version: 1, nodes };
3686
3686
  }
@@ -3689,23 +3689,23 @@ var OwnershipIndex = class _OwnershipIndex {
3689
3689
  */
3690
3690
  static load(s) {
3691
3691
  const idx = new _OwnershipIndex();
3692
- for (const [path34, raw] of Object.entries(s.nodes)) {
3692
+ for (const [path35, raw] of Object.entries(s.nodes)) {
3693
3693
  const authorWeights = {};
3694
3694
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3695
3695
  authorWeights[email] = { ...entry };
3696
3696
  }
3697
- idx.nodes.set(path34, { authorWeights, lastTouch: raw.lastTouch });
3697
+ idx.nodes.set(path35, { authorWeights, lastTouch: raw.lastTouch });
3698
3698
  }
3699
3699
  return idx;
3700
3700
  }
3701
3701
  // -------------------------------------------------------------------------
3702
3702
  // Private helpers
3703
3703
  // -------------------------------------------------------------------------
3704
- getOrCreate(path34) {
3705
- const existing = this.nodes.get(path34);
3704
+ getOrCreate(path35) {
3705
+ const existing = this.nodes.get(path35);
3706
3706
  if (existing !== void 0) return existing;
3707
3707
  const fresh = { authorWeights: {}, lastTouch: 0 };
3708
- this.nodes.set(path34, fresh);
3708
+ this.nodes.set(path35, fresh);
3709
3709
  return fresh;
3710
3710
  }
3711
3711
  };
@@ -8837,8 +8837,20 @@ function markAliasSent(home) {
8837
8837
  }
8838
8838
 
8839
8839
  // packages/core/src/license/telemetry.ts
8840
- var TELEMETRY_DISABLED = process.env["CTXLOOM_NO_TELEMETRY"] === "1" || process.env["DO_NOT_TRACK"] === "1";
8841
- var CTXLOOM_VERSION = "1.1.5".length > 0 ? "1.1.5" : "dev";
8840
+ function resolveTelemetryLevel() {
8841
+ if (process.env["CTXLOOM_NO_TELEMETRY"] === "1" || process.env["DO_NOT_TRACK"] === "1") {
8842
+ return "off";
8843
+ }
8844
+ const raw = process.env["CTXLOOM_TELEMETRY_LEVEL"]?.toLowerCase();
8845
+ if (raw === "off" || raw === "error" || raw === "all") return raw;
8846
+ return "all";
8847
+ }
8848
+ var TELEMETRY_LEVEL = resolveTelemetryLevel();
8849
+ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
8850
+ function getTelemetryLevel() {
8851
+ return TELEMETRY_LEVEL;
8852
+ }
8853
+ var CTXLOOM_VERSION = "1.2.0".length > 0 ? "1.2.0" : "dev";
8842
8854
  var POSTHOG_HOST = "https://eu.i.posthog.com";
8843
8855
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
8844
8856
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -8853,7 +8865,7 @@ function resolveDistinctId() {
8853
8865
  }
8854
8866
  }
8855
8867
  function track(event, props = {}) {
8856
- if (TELEMETRY_DISABLED || !POSTHOG_KEY) return;
8868
+ if (TELEMETRY_LEVEL !== "all" || !POSTHOG_KEY) return;
8857
8869
  const record = resolveDistinctId();
8858
8870
  if (!record) return;
8859
8871
  void sendPostHog(event, record.id, props);
@@ -8969,14 +8981,32 @@ function parseStack(stack) {
8969
8981
  }).filter((f) => f !== null).slice(0, 20);
8970
8982
  }
8971
8983
 
8984
+ // packages/core/src/license/TelemetryNotice.ts
8985
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
8986
+ import path31 from "path";
8987
+ import os5 from "os";
8988
+ function noticePath(home) {
8989
+ return path31.join(home ?? os5.homedir(), ".ctxloom", "telemetry_notice_shown");
8990
+ }
8991
+ function shouldShowTelemetryNotice(home) {
8992
+ const filePath = noticePath(home);
8993
+ if (existsSync3(filePath)) return false;
8994
+ try {
8995
+ mkdirSync3(path31.dirname(filePath), { recursive: true });
8996
+ writeFileSync3(filePath, (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
8997
+ } catch {
8998
+ }
8999
+ return true;
9000
+ }
9001
+
8972
9002
  // packages/core/src/server/ProjectState.ts
8973
- import path32 from "path";
9003
+ import path33 from "path";
8974
9004
 
8975
9005
  // packages/core/src/server/projectId.ts
8976
9006
  import crypto5 from "crypto";
8977
- import path31 from "path";
9007
+ import path32 from "path";
8978
9008
  function hashProjectRoot(absPath) {
8979
- const canonical = path31.resolve(absPath);
9009
+ const canonical = path32.resolve(absPath);
8980
9010
  return crypto5.createHash("sha256").update(canonical).digest("hex").slice(0, 16);
8981
9011
  }
8982
9012
 
@@ -8984,7 +9014,7 @@ function hashProjectRoot(absPath) {
8984
9014
  function createProjectState(projectRoot, opts = {}) {
8985
9015
  return {
8986
9016
  projectRoot,
8987
- dbPath: path32.join(projectRoot, ".ctxloom", "vectors.lancedb"),
9017
+ dbPath: path33.join(projectRoot, ".ctxloom", "vectors.lancedb"),
8988
9018
  pinned: opts.pinned ?? false,
8989
9019
  lastTouchedAt: Date.now(),
8990
9020
  vectorsInitialized: false,
@@ -9137,7 +9167,7 @@ var ProjectStateManager = class {
9137
9167
 
9138
9168
  // packages/core/src/server/resolveProjectRoot.ts
9139
9169
  import fs27 from "fs";
9140
- import path33 from "path";
9170
+ import path34 from "path";
9141
9171
  var PATH_SEPARATOR_PATTERN = /[/\\~]|^[A-Za-z]:/;
9142
9172
  function looksLikePath(value) {
9143
9173
  return PATH_SEPARATOR_PATTERN.test(value);
@@ -9162,9 +9192,9 @@ function resolvePathSafely(p, cwd) {
9162
9192
  let expanded = p;
9163
9193
  if (p === "~" || p.startsWith("~/")) {
9164
9194
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
9165
- expanded = p === "~" ? home : path33.join(home, p.slice(2));
9195
+ expanded = p === "~" ? home : path34.join(home, p.slice(2));
9166
9196
  }
9167
- return path33.isAbsolute(expanded) ? path33.resolve(expanded) : path33.resolve(cwd, expanded);
9197
+ return path34.isAbsolute(expanded) ? path34.resolve(expanded) : path34.resolve(cwd, expanded);
9168
9198
  }
9169
9199
  function realpathOrSame(p) {
9170
9200
  try {
@@ -9236,7 +9266,7 @@ function validateDefaultRoot(candidate) {
9236
9266
  } catch {
9237
9267
  return false;
9238
9268
  }
9239
- return PROJECT_MARKERS.some((m) => fs27.existsSync(path33.join(candidate, m)));
9269
+ return PROJECT_MARKERS.some((m) => fs27.existsSync(path34.join(candidate, m)));
9240
9270
  }
9241
9271
 
9242
9272
  // packages/core/src/server/structuredErrors.ts
@@ -9391,8 +9421,10 @@ export {
9391
9421
  startTrial,
9392
9422
  getOrCreateDistinctId,
9393
9423
  markAliasSent,
9424
+ getTelemetryLevel,
9394
9425
  track,
9395
9426
  captureError,
9427
+ shouldShowTelemetryNotice,
9396
9428
  hashProjectRoot,
9397
9429
  createProjectState,
9398
9430
  ensureVectorsInitialized,
@@ -9409,4 +9441,4 @@ export {
9409
9441
  FirstTouchTracker,
9410
9442
  EmittedOnceTracker
9411
9443
  };
9412
- //# sourceMappingURL=chunk-7BJDA4UT.js.map
9444
+ //# sourceMappingURL=chunk-4B2S3WJ5.js.map
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  ensureVectorsInitialized,
32
32
  generateCODEOWNERS,
33
33
  getLicenseInfo,
34
+ getTelemetryLevel,
34
35
  hashProjectRoot,
35
36
  isActive,
36
37
  loadReviewConfig,
@@ -40,12 +41,13 @@ import {
40
41
  resolveProjectRoot,
41
42
  resolveViaGitHubApi,
42
43
  scoreReviewers,
44
+ shouldShowTelemetryNotice,
43
45
  startTrial,
44
46
  track,
45
47
  validateDefaultRoot,
46
48
  wrapWithIndexingEnvelope,
47
49
  writeCODEOWNERS
48
- } from "./chunk-7BJDA4UT.js";
50
+ } from "./chunk-4B2S3WJ5.js";
49
51
  import {
50
52
  VectorStore
51
53
  } from "./chunk-NEHYSE2Y.js";
@@ -892,7 +894,7 @@ try {
892
894
  } catch {
893
895
  }
894
896
  var args = process.argv.slice(2);
895
- var ctxloomVersion = "1.1.5".length > 0 ? "1.1.5" : "dev";
897
+ var ctxloomVersion = "1.2.0".length > 0 ? "1.2.0" : "dev";
896
898
  if (args.includes("--version") || args.includes("-v")) {
897
899
  process.stdout.write(`ctxloom ${ctxloomVersion}
898
900
  `);
@@ -965,7 +967,7 @@ async function checkLicense() {
965
967
  if (command !== void 0 && LICENSE_GATE_BYPASS_COMMANDS.has(command)) return;
966
968
  const ciKey = process.env["CTXLOOM_LICENSE_KEY"];
967
969
  if (ciKey) {
968
- const { ApiClient } = await import("./src-WDWBHNN6.js");
970
+ const { ApiClient } = await import("./src-CNHVHDBT.js");
969
971
  const client = new ApiClient(process.env["CTXLOOM_API_BASE"]);
970
972
  try {
971
973
  const result = await client.validate(ciKey, "ci-ephemeral");
@@ -1169,7 +1171,26 @@ async function runStatus() {
1169
1171
  ]));
1170
1172
  process.stdout.write("\n");
1171
1173
  }
1174
+ function maybePrintTelemetryNotice() {
1175
+ if (command === void 0) return;
1176
+ if (getTelemetryLevel() === "off") return;
1177
+ if (!shouldShowTelemetryNotice()) return;
1178
+ process.stderr.write(
1179
+ `
1180
+ ${style.dim("\u2500".repeat(60))}
1181
+ ${style.bold("ctxloom collects anonymous usage telemetry")} to improve the tool.
1182
+ No file contents, paths, or aliases are ever transmitted.
1183
+
1184
+ Disable with: ${style.highlight("CTXLOOM_NO_TELEMETRY=1")}
1185
+ Errors only: ${style.highlight("CTXLOOM_TELEMETRY_LEVEL=error")}
1186
+ Details: ${style.highlight("https://github.com/kodiii/ctxloom/blob/main/docs/TELEMETRY.md")}
1187
+ ${style.dim("\u2500".repeat(60))}
1188
+
1189
+ `
1190
+ );
1191
+ }
1172
1192
  async function main() {
1193
+ maybePrintTelemetryNotice();
1173
1194
  await checkLicense();
1174
1195
  switch (command) {
1175
1196
  case "trial": {
@@ -1318,7 +1339,7 @@ async function main() {
1318
1339
  process.exit(1);
1319
1340
  }
1320
1341
  if (alias !== void 0) {
1321
- const { validateAlias } = await import("./src-WDWBHNN6.js");
1342
+ const { validateAlias } = await import("./src-CNHVHDBT.js");
1322
1343
  const v = validateAlias(alias);
1323
1344
  if (!v.ok) {
1324
1345
  console.error(`[ctxloom] Invalid alias: ${v.reason}`);
@@ -1562,7 +1583,7 @@ Suggested reviewers for ${files.length} file(s):`);
1562
1583
  process.stderr.write("[ctxloom] --limit must be a non-negative integer (0 for unlimited)\n");
1563
1584
  process.exit(2);
1564
1585
  }
1565
- const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-WDWBHNN6.js");
1586
+ const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-CNHVHDBT.js");
1566
1587
  let config;
1567
1588
  try {
1568
1589
  config = await loadRulesConfig(root);
@@ -1586,7 +1607,7 @@ Suggested reviewers for ${files.length} file(s):`);
1586
1607
  }
1587
1608
  let graph;
1588
1609
  if (useSnapshot) {
1589
- const { DependencyGraph: DG } = await import("./src-WDWBHNN6.js");
1610
+ const { DependencyGraph: DG } = await import("./src-CNHVHDBT.js");
1590
1611
  graph = new DG();
1591
1612
  const loaded = await graph.loadSnapshotOnly(root);
1592
1613
  if (!loaded) {
@@ -1595,7 +1616,7 @@ Suggested reviewers for ${files.length} file(s):`);
1595
1616
  }
1596
1617
  } else {
1597
1618
  process.stderr.write("[ctxloom] Building dependency graph...\n");
1598
- const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-WDWBHNN6.js");
1619
+ const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-CNHVHDBT.js");
1599
1620
  let parser;
1600
1621
  try {
1601
1622
  parser = new ASTParser2();
@@ -68,6 +68,7 @@ import {
68
68
  getImpactRadius,
69
69
  getLicenseInfo,
70
70
  getOrCreateDistinctId,
71
+ getTelemetryLevel,
71
72
  hashProjectRoot,
72
73
  isActive,
73
74
  isSiloed,
@@ -93,13 +94,14 @@ import {
93
94
  scoreAll,
94
95
  scoreFromBreakdown,
95
96
  scoreReviewers,
97
+ shouldShowTelemetryNotice,
96
98
  startTrial,
97
99
  track,
98
100
  validateAlias,
99
101
  validateDefaultRoot,
100
102
  wrapWithIndexingEnvelope,
101
103
  writeCODEOWNERS
102
- } from "./chunk-7BJDA4UT.js";
104
+ } from "./chunk-4B2S3WJ5.js";
103
105
  import {
104
106
  VectorStore
105
107
  } from "./chunk-NEHYSE2Y.js";
@@ -186,6 +188,7 @@ export {
186
188
  getImpactRadius,
187
189
  getLicenseInfo,
188
190
  getOrCreateDistinctId,
191
+ getTelemetryLevel,
189
192
  hashProjectRoot,
190
193
  indexDirectory,
191
194
  isActive,
@@ -213,6 +216,7 @@ export {
213
216
  scoreAll,
214
217
  scoreFromBreakdown,
215
218
  scoreReviewers,
219
+ shouldShowTelemetryNotice,
216
220
  startTrial,
217
221
  track,
218
222
  validateAlias,
@@ -220,4 +224,4 @@ export {
220
224
  wrapWithIndexingEnvelope,
221
225
  writeCODEOWNERS
222
226
  };
223
- //# sourceMappingURL=src-WDWBHNN6.js.map
227
+ //# sourceMappingURL=src-CNHVHDBT.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctxloom-pro",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
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",