ctxloom-pro 1.1.1 → 1.1.3

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ctxloom dashboard</title>
7
- <script type="module" crossorigin src="/assets/index-Bfa2_GyA.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-DNFP0Mer.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-MBoNDzdn.css">
9
9
  </head>
10
10
  <body>
@@ -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 path41 from "path";
172
+ import path42 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 path35 from "path";
177
+ import path36 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 path42 of paths) {
3137
- this.nodeCounts.set(path42, (this.nodeCounts.get(path42) ?? 0) + 1);
3136
+ for (const path43 of paths) {
3137
+ this.nodeCounts.set(path43, (this.nodeCounts.get(path43) ?? 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 [path42, raw] of this.nodes) {
3285
- nodes[path42] = {
3284
+ for (const [path43, raw] of this.nodes) {
3285
+ nodes[path43] = {
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 [path42, raw] of Object.entries(s.nodes)) {
3301
- idx.nodes.set(path42, {
3300
+ for (const [path43, raw] of Object.entries(s.nodes)) {
3301
+ idx.nodes.set(path43, {
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(path42) {
3315
- const existing = this.nodes.get(path42);
3314
+ getOrCreate(path43) {
3315
+ const existing = this.nodes.get(path43);
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(path42, fresh);
3324
+ this.nodes.set(path43, 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 [path42, raw] of this.nodes) {
3405
+ for (const [path43, 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[path42] = { authorWeights, lastTouch: raw.lastTouch };
3410
+ nodes[path43] = { 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 [path42, raw] of Object.entries(s.nodes)) {
3419
+ for (const [path43, 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(path42, { authorWeights, lastTouch: raw.lastTouch });
3424
+ idx.nodes.set(path43, { authorWeights, lastTouch: raw.lastTouch });
3425
3425
  }
3426
3426
  return idx;
3427
3427
  }
3428
3428
  // -------------------------------------------------------------------------
3429
3429
  // Private helpers
3430
3430
  // -------------------------------------------------------------------------
3431
- getOrCreate(path42) {
3432
- const existing = this.nodes.get(path42);
3431
+ getOrCreate(path43) {
3432
+ const existing = this.nodes.get(path43);
3433
3433
  if (existing !== void 0) return existing;
3434
3434
  const fresh = { authorWeights: {}, lastTouch: 0 };
3435
- this.nodes.set(path42, fresh);
3435
+ this.nodes.set(path43, fresh);
3436
3436
  return fresh;
3437
3437
  }
3438
3438
  };
@@ -4458,8 +4458,8 @@ var ZodIssueCode = util.arrayToEnum([
4458
4458
  "not_finite"
4459
4459
  ]);
4460
4460
  var quotelessJson = (obj) => {
4461
- const json2 = JSON.stringify(obj, null, 2);
4462
- return json2.replace(/"([^"]+)":/g, "$1:");
4461
+ const json3 = JSON.stringify(obj, null, 2);
4462
+ return json3.replace(/"([^"]+)":/g, "$1:");
4463
4463
  };
4464
4464
  var ZodError = class _ZodError extends Error {
4465
4465
  get errors() {
@@ -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: path42, errorMaps, issueData } = params;
4674
- const fullPath = [...path42, ...issueData.path || []];
4673
+ const { data, path: path43, errorMaps, issueData } = params;
4674
+ const fullPath = [...path43, ...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, path42, key) {
4790
+ constructor(parent, value, path43, key) {
4791
4791
  this._cachedPath = [];
4792
4792
  this.parent = parent;
4793
4793
  this.data = value;
4794
- this._path = path42;
4794
+ this._path = path43;
4795
4795
  this._key = key;
4796
4796
  }
4797
4797
  get path() {
@@ -11268,30 +11268,198 @@ import { readFileSync as readFileSync2 } from "fs";
11268
11268
  // ../../packages/core/src/license/index.ts
11269
11269
  import os3 from "os";
11270
11270
 
11271
+ // ../../packages/core/src/license/DistinctIdStore.ts
11272
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
11273
+ import path32 from "path";
11274
+ import os4 from "os";
11275
+ var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11276
+ function distinctIdPath(home) {
11277
+ return path32.join(home ?? os4.homedir(), ".ctxloom", "distinct_id");
11278
+ }
11279
+ function isValidV4(id) {
11280
+ return typeof id === "string" && UUID_V4_REGEX.test(id);
11281
+ }
11282
+ function getOrCreateDistinctId(home) {
11283
+ const filePath = distinctIdPath(home);
11284
+ if (existsSync2(filePath)) {
11285
+ try {
11286
+ const raw = readFileSync3(filePath, "utf8");
11287
+ const parsed = JSON.parse(raw);
11288
+ if (parsed !== null && typeof parsed === "object" && isValidV4(parsed["id"])) {
11289
+ return parsed;
11290
+ }
11291
+ } catch {
11292
+ }
11293
+ }
11294
+ const record = {
11295
+ id: crypto.randomUUID(),
11296
+ alias_pending: os4.hostname()
11297
+ };
11298
+ mkdirSync2(path32.dirname(filePath), { recursive: true });
11299
+ writeFileSync2(filePath, JSON.stringify(record), { mode: 384 });
11300
+ return record;
11301
+ }
11302
+ function markAliasSent(home) {
11303
+ const filePath = distinctIdPath(home);
11304
+ try {
11305
+ const raw = readFileSync3(filePath, "utf8");
11306
+ const parsed = JSON.parse(raw);
11307
+ const { alias_pending: _dropped, ...rest } = parsed;
11308
+ writeFileSync2(filePath, JSON.stringify(rest), { mode: 384 });
11309
+ } catch {
11310
+ }
11311
+ }
11312
+
11271
11313
  // ../../packages/core/src/license/telemetry.ts
11272
11314
  var TELEMETRY_DISABLED = process.env["CTXLOOM_NO_TELEMETRY"] === "1" || process.env["DO_NOT_TRACK"] === "1";
11273
11315
  var CTXLOOM_VERSION = typeof __CTXLOOM_VERSION__ === "string" && __CTXLOOM_VERSION__.length > 0 ? __CTXLOOM_VERSION__ : "dev";
11316
+ var POSTHOG_HOST = "https://eu.i.posthog.com";
11274
11317
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (typeof __TELEMETRY_POSTHOG_KEY__ === "string" ? __TELEMETRY_POSTHOG_KEY__ : "");
11275
11318
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (typeof __TELEMETRY_SENTRY_DSN__ === "string" ? __TELEMETRY_SENTRY_DSN__ : "");
11319
+ var cachedDistinctId = null;
11320
+ function resolveDistinctId() {
11321
+ if (cachedDistinctId) return cachedDistinctId;
11322
+ try {
11323
+ cachedDistinctId = getOrCreateDistinctId();
11324
+ return cachedDistinctId;
11325
+ } catch {
11326
+ return null;
11327
+ }
11328
+ }
11329
+ function track(event, props = {}) {
11330
+ if (TELEMETRY_DISABLED || !POSTHOG_KEY) return;
11331
+ const record = resolveDistinctId();
11332
+ if (!record) return;
11333
+ void sendPostHog(event, record.id, props);
11334
+ if (record.alias_pending) {
11335
+ const oldAlias = record.alias_pending;
11336
+ void sendAlias(record.id, oldAlias);
11337
+ }
11338
+ }
11339
+ function captureError(err, context = {}) {
11340
+ if (TELEMETRY_DISABLED || !SENTRY_DSN) return;
11341
+ const record = resolveDistinctId();
11342
+ const augmented = record ? { ...context, distinct_id: record.id } : context;
11343
+ void sendSentry(err, augmented);
11344
+ }
11345
+ async function sendPostHog(event, distinctId, props) {
11346
+ try {
11347
+ await fetch(`${POSTHOG_HOST}/capture/`, {
11348
+ method: "POST",
11349
+ headers: { "Content-Type": "application/json" },
11350
+ body: JSON.stringify({
11351
+ api_key: POSTHOG_KEY,
11352
+ distinct_id: distinctId,
11353
+ event,
11354
+ properties: {
11355
+ $lib: "ctxloom-cli",
11356
+ release: CTXLOOM_VERSION,
11357
+ ...props
11358
+ }
11359
+ }),
11360
+ signal: AbortSignal.timeout(4e3)
11361
+ });
11362
+ } catch {
11363
+ }
11364
+ }
11365
+ async function sendAlias(newId, oldAlias) {
11366
+ try {
11367
+ const res = await fetch(`${POSTHOG_HOST}/capture/`, {
11368
+ method: "POST",
11369
+ headers: { "Content-Type": "application/json" },
11370
+ body: JSON.stringify({
11371
+ api_key: POSTHOG_KEY,
11372
+ distinct_id: newId,
11373
+ event: "$create_alias",
11374
+ properties: {
11375
+ $lib: "ctxloom-cli",
11376
+ release: CTXLOOM_VERSION,
11377
+ alias: oldAlias
11378
+ }
11379
+ }),
11380
+ signal: AbortSignal.timeout(4e3)
11381
+ });
11382
+ if (res.ok) {
11383
+ try {
11384
+ markAliasSent();
11385
+ } catch {
11386
+ }
11387
+ if (cachedDistinctId) cachedDistinctId = { id: cachedDistinctId.id };
11388
+ }
11389
+ } catch {
11390
+ }
11391
+ }
11392
+ async function sendSentry(err, context) {
11393
+ try {
11394
+ const dsn = parseDsn(SENTRY_DSN);
11395
+ if (!dsn) return;
11396
+ const message = err instanceof Error ? err.message : String(err);
11397
+ const stack = err instanceof Error ? err.stack : void 0;
11398
+ await fetch(`https://${dsn.host}/api/${dsn.projectId}/store/`, {
11399
+ method: "POST",
11400
+ headers: {
11401
+ "Content-Type": "application/json",
11402
+ "X-Sentry-Auth": `Sentry sentry_version=7, sentry_key=${dsn.key}`
11403
+ },
11404
+ body: JSON.stringify({
11405
+ event_id: crypto.randomUUID().replace(/-/g, ""),
11406
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11407
+ platform: "node",
11408
+ level: "error",
11409
+ exception: {
11410
+ values: [
11411
+ {
11412
+ type: err instanceof Error ? err.constructor.name : "Error",
11413
+ value: message,
11414
+ stacktrace: stack ? { frames: parseStack(stack) } : void 0
11415
+ }
11416
+ ]
11417
+ },
11418
+ extra: context,
11419
+ tags: { runtime: "node", component: "cli-license", release: CTXLOOM_VERSION }
11420
+ }),
11421
+ signal: AbortSignal.timeout(4e3)
11422
+ });
11423
+ } catch {
11424
+ }
11425
+ }
11426
+ function parseDsn(dsn) {
11427
+ try {
11428
+ const url = new URL(dsn);
11429
+ const projectId = url.pathname.replace(/^\//, "");
11430
+ return { host: url.hostname, key: url.username, projectId };
11431
+ } catch {
11432
+ return null;
11433
+ }
11434
+ }
11435
+ function scrubPath(filename) {
11436
+ return filename.replace(/^\/Users\/[^/]+\//, "/Users/~/").replace(/^\/home\/[^/]+\//, "/home/~/").replace(/^([A-Z]:\\\\Users\\\\)[^\\]+\\\\/, "$1~\\\\").replace(/^([A-Z]:\\Users\\)[^\\]+\\/, "$1~\\");
11437
+ }
11438
+ function parseStack(stack) {
11439
+ return stack.split("\n").slice(1).map((line) => {
11440
+ const m = line.trim().match(/at (.+?) \((.+?):(\d+):\d+\)/);
11441
+ if (!m) return null;
11442
+ return { function: m[1] ?? "", filename: scrubPath(m[2] ?? ""), lineno: Number(m[3]) };
11443
+ }).filter((f) => f !== null).slice(0, 20);
11444
+ }
11276
11445
 
11277
11446
  // ../../packages/core/src/server/ProjectState.ts
11278
- import path33 from "path";
11447
+ import path34 from "path";
11279
11448
 
11280
11449
  // ../../packages/core/src/server/projectId.ts
11281
11450
  import crypto5 from "crypto";
11282
- import path32 from "path";
11451
+ import path33 from "path";
11283
11452
 
11284
11453
  // ../../packages/core/src/server/ProjectStateManager.ts
11285
11454
  init_logger();
11286
- import os4 from "os";
11287
11455
 
11288
11456
  // ../../packages/core/src/server/resolveProjectRoot.ts
11289
11457
  import fs29 from "fs";
11290
- import path34 from "path";
11458
+ import path35 from "path";
11291
11459
 
11292
11460
  // server/loader.ts
11293
11461
  async function loadContext(root) {
11294
- const absRoot = path35.resolve(root);
11462
+ const absRoot = path36.resolve(root);
11295
11463
  const overlay = new GitOverlayStore(absRoot);
11296
11464
  const gitEnabled = await overlay.loadSnapshot();
11297
11465
  const graph = new DependencyGraph();
@@ -11545,20 +11713,20 @@ function buildOwnershipRouter(ctx) {
11545
11713
  // server/routes/file.ts
11546
11714
  import { Router as Router7 } from "express";
11547
11715
  import fs30 from "fs/promises";
11548
- import path36 from "path";
11716
+ import path37 from "path";
11549
11717
  function buildFileRouter(ctx) {
11550
11718
  const router = Router7();
11551
11719
  router.get("/", async (req, res) => {
11552
11720
  const rel = req.query.path;
11553
11721
  if (!rel) return res.status(400).json({ error: "missing path" });
11554
- const abs = path36.resolve(ctx.root, rel);
11555
- const rootBoundary = ctx.root.endsWith(path36.sep) ? ctx.root : ctx.root + path36.sep;
11722
+ const abs = path37.resolve(ctx.root, rel);
11723
+ const rootBoundary = ctx.root.endsWith(path37.sep) ? ctx.root : ctx.root + path37.sep;
11556
11724
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11557
11725
  return res.status(403).json({ error: "forbidden" });
11558
11726
  }
11559
11727
  try {
11560
11728
  const content = await fs30.readFile(abs, "utf-8");
11561
- const ext = path36.extname(abs).slice(1);
11729
+ const ext = path37.extname(abs).slice(1);
11562
11730
  res.json({ content, lines: content.split("\n").length, ext });
11563
11731
  } catch {
11564
11732
  res.status(404).json({ error: "not found" });
@@ -11570,7 +11738,7 @@ function buildFileRouter(ctx) {
11570
11738
  // server/routes/open.ts
11571
11739
  import { Router as Router8 } from "express";
11572
11740
  import { execFile as execFile2 } from "child_process";
11573
- import path37 from "path";
11741
+ import path38 from "path";
11574
11742
  function tryOpen(bin, abs) {
11575
11743
  return new Promise((resolve) => {
11576
11744
  execFile2(bin, [abs], { timeout: 5e3 }, (err) => resolve(!err));
@@ -11581,8 +11749,8 @@ function buildOpenRouter(ctx) {
11581
11749
  router.post("/", async (req, res) => {
11582
11750
  const rel = req.body?.path;
11583
11751
  if (!rel || typeof rel !== "string") return res.status(400).json({ error: "missing path" });
11584
- const abs = path37.resolve(ctx.root, rel);
11585
- const rootBoundary = ctx.root.endsWith(path37.sep) ? ctx.root : ctx.root + path37.sep;
11752
+ const abs = path38.resolve(ctx.root, rel);
11753
+ const rootBoundary = ctx.root.endsWith(path38.sep) ? ctx.root : ctx.root + path38.sep;
11586
11754
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11587
11755
  return res.status(403).json({ error: "forbidden" });
11588
11756
  }
@@ -11594,7 +11762,7 @@ function buildOpenRouter(ctx) {
11594
11762
 
11595
11763
  // server/routes/tokens.ts
11596
11764
  import { Router as Router9 } from "express";
11597
- import path38 from "path";
11765
+ import path39 from "path";
11598
11766
  import fs31 from "fs";
11599
11767
  var CHARS_PER_TOKEN = 4;
11600
11768
  var cache = null;
@@ -11610,7 +11778,7 @@ function buildTokensRouter(ctx) {
11610
11778
  let fullChars = 0;
11611
11779
  let skeletonChars = 0;
11612
11780
  for (const file of files) {
11613
- const absPath = path38.join(ctx.root, file);
11781
+ const absPath = path39.join(ctx.root, file);
11614
11782
  try {
11615
11783
  const content = fs31.readFileSync(absPath, "utf-8");
11616
11784
  fullChars += content.length;
@@ -11713,23 +11881,23 @@ function buildFileTrendsRouter(ctx) {
11713
11881
 
11714
11882
  // server/routes/projects.ts
11715
11883
  import { Router as Router12 } from "express";
11716
- import path40 from "path";
11884
+ import path41 from "path";
11717
11885
 
11718
11886
  // server/projects.ts
11719
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
11887
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
11720
11888
  import os5 from "os";
11721
- import path39 from "path";
11889
+ import path40 from "path";
11722
11890
  import crypto6 from "crypto";
11723
11891
  var HOME = os5.homedir();
11724
- var REGISTRY_PATH = path39.join(HOME, ".ctxloom", "repos.json");
11892
+ var REGISTRY_PATH = path40.join(HOME, ".ctxloom", "repos.json");
11725
11893
  function slugFor(root) {
11726
- const abs = path39.resolve(root);
11894
+ const abs = path40.resolve(root);
11727
11895
  return crypto6.createHash("sha1").update(abs).digest("hex").slice(0, 12);
11728
11896
  }
11729
11897
  function readRegistry() {
11730
- if (!existsSync2(REGISTRY_PATH)) return [];
11898
+ if (!existsSync3(REGISTRY_PATH)) return [];
11731
11899
  try {
11732
- const raw = readFileSync3(REGISTRY_PATH, "utf-8");
11900
+ const raw = readFileSync4(REGISTRY_PATH, "utf-8");
11733
11901
  const parsed = JSON.parse(raw);
11734
11902
  if (!Array.isArray(parsed)) return [];
11735
11903
  return parsed.filter((r) => typeof r?.root === "string");
@@ -11738,27 +11906,27 @@ function readRegistry() {
11738
11906
  }
11739
11907
  }
11740
11908
  function listProjects(defaultRoot) {
11741
- const absDefault = path39.resolve(defaultRoot);
11909
+ const absDefault = path40.resolve(defaultRoot);
11742
11910
  const out = [
11743
11911
  {
11744
11912
  slug: slugFor(absDefault),
11745
- name: path39.basename(absDefault) || absDefault,
11913
+ name: path40.basename(absDefault) || absDefault,
11746
11914
  root: absDefault,
11747
11915
  isDefault: true,
11748
- hasSnapshot: existsSync2(path39.join(absDefault, ".ctxloom"))
11916
+ hasSnapshot: existsSync3(path40.join(absDefault, ".ctxloom"))
11749
11917
  }
11750
11918
  ];
11751
11919
  const seen = /* @__PURE__ */ new Set([absDefault]);
11752
11920
  for (const entry of readRegistry()) {
11753
- const abs = path39.resolve(entry.root);
11921
+ const abs = path40.resolve(entry.root);
11754
11922
  if (seen.has(abs)) continue;
11755
11923
  seen.add(abs);
11756
11924
  const item = {
11757
11925
  slug: slugFor(abs),
11758
- name: entry.name ?? (path39.basename(abs) || abs),
11926
+ name: entry.name ?? (path40.basename(abs) || abs),
11759
11927
  root: abs,
11760
11928
  isDefault: false,
11761
- hasSnapshot: existsSync2(path39.join(abs, ".ctxloom"))
11929
+ hasSnapshot: existsSync3(path40.join(abs, ".ctxloom"))
11762
11930
  };
11763
11931
  if (entry.alias !== void 0) item.alias = entry.alias;
11764
11932
  out.push(item);
@@ -11811,15 +11979,64 @@ function buildProjectsRouter(deps) {
11811
11979
  } catch (err) {
11812
11980
  const detail = err instanceof Error ? err.message : String(err);
11813
11981
  res.status(500).json({
11814
- error: `failed to switch to ${path40.basename(target.root)}: ${detail}`
11982
+ error: `failed to switch to ${path41.basename(target.root)}: ${detail}`
11815
11983
  });
11816
11984
  }
11817
11985
  });
11818
11986
  return router;
11819
11987
  }
11820
11988
 
11989
+ // server/routes/telemetry.ts
11990
+ import { Router as Router13, json as json2 } from "express";
11991
+ var DASHBOARD_EVENT_ALLOWLIST = /* @__PURE__ */ new Set([
11992
+ "dashboard_loaded",
11993
+ "dashboard_page_viewed"
11994
+ ]);
11995
+ var MAX_MESSAGE_LENGTH = 2e3;
11996
+ var MAX_STACK_LENGTH = 1e4;
11997
+ function buildTelemetryRouter() {
11998
+ const router = Router13();
11999
+ router.get("/identity", (_req, res) => {
12000
+ const disabled = process.env.CTXLOOM_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1";
12001
+ res.json({ enabled: !disabled });
12002
+ });
12003
+ router.post("/event", json2(), (req, res) => {
12004
+ const body = req.body;
12005
+ const event = body?.event;
12006
+ if (typeof event !== "string" || !DASHBOARD_EVENT_ALLOWLIST.has(event)) {
12007
+ res.status(400).json({ error: "invalid event" });
12008
+ return;
12009
+ }
12010
+ const rawProps = body?.props;
12011
+ const sanitizedProps = rawProps && typeof rawProps === "object" && !Array.isArray(rawProps) ? rawProps : {};
12012
+ track(event, {
12013
+ ...sanitizedProps,
12014
+ surface: "dashboard"
12015
+ });
12016
+ res.status(204).end();
12017
+ });
12018
+ router.post("/error", json2(), (req, res) => {
12019
+ const body = req.body;
12020
+ const message = body?.message;
12021
+ if (typeof message !== "string" || message.length === 0 || message.length > MAX_MESSAGE_LENGTH) {
12022
+ res.status(400).json({ error: "invalid message" });
12023
+ return;
12024
+ }
12025
+ const stack = body?.stack;
12026
+ const err = new Error(message);
12027
+ if (typeof stack === "string" && stack.length > 0 && stack.length <= MAX_STACK_LENGTH) {
12028
+ err.stack = stack;
12029
+ }
12030
+ const rawContext = body?.context;
12031
+ const sanitizedContext = rawContext && typeof rawContext === "object" && !Array.isArray(rawContext) ? rawContext : {};
12032
+ captureError(err, { ...sanitizedContext, surface: "dashboard" });
12033
+ res.status(204).end();
12034
+ });
12035
+ return router;
12036
+ }
12037
+
11821
12038
  // server/index.ts
11822
- var __dirname2 = path41.dirname(fileURLToPath2(import.meta.url));
12039
+ var __dirname2 = path42.dirname(fileURLToPath2(import.meta.url));
11823
12040
  async function startDashboard(options) {
11824
12041
  const { root, port, open } = options;
11825
12042
  console.log(`ctxloom dashboard \u2014 loading context from ${root}...`);
@@ -11878,7 +12095,7 @@ async function startDashboard(options) {
11878
12095
  }
11879
12096
  activeWatcher = null;
11880
12097
  }
11881
- const snapshotDir = path41.join(targetRoot, ".ctxloom");
12098
+ const snapshotDir = path42.join(targetRoot, ".ctxloom");
11882
12099
  try {
11883
12100
  activeWatcher = fs32.watch(snapshotDir, (_event, filename) => {
11884
12101
  if (!filename || !filename.includes("snapshot")) return;
@@ -11899,6 +12116,7 @@ async function startDashboard(options) {
11899
12116
  }
11900
12117
  }
11901
12118
  attachSnapshotWatcher(ctx.root);
12119
+ app.use("/api/telemetry", buildTelemetryRouter());
11902
12120
  app.use("/api/projects", buildProjectsRouter({
11903
12121
  ctx,
11904
12122
  defaultRoot: root,
@@ -11908,12 +12126,12 @@ async function startDashboard(options) {
11908
12126
  attachSnapshotWatcher(newRoot);
11909
12127
  }
11910
12128
  }));
11911
- const clientDist = path41.join(__dirname2, "../dashboard/client");
11912
- const clientDistExists = fs32.existsSync(path41.join(clientDist, "index.html"));
12129
+ const clientDist = path42.join(__dirname2, "../dashboard/client");
12130
+ const clientDistExists = fs32.existsSync(path42.join(clientDist, "index.html"));
11913
12131
  if (clientDistExists) {
11914
12132
  app.use(express.static(clientDist, { dotfiles: "allow" }));
11915
12133
  app.get(/.*/, (_req, res) => {
11916
- res.sendFile(path41.join(clientDist, "index.html"), { dotfiles: "allow" });
12134
+ res.sendFile(path42.join(clientDist, "index.html"), { dotfiles: "allow" });
11917
12135
  });
11918
12136
  } else {
11919
12137
  app.get(/^\/(?!api\/).*/, (_req, res) => {