lens-engine 0.1.16 → 0.1.18

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/daemon.js CHANGED
@@ -7605,6 +7605,7 @@ var init_esm2 = __esm({
7605
7605
  var dist_exports = {};
7606
7606
  __export(dist_exports, {
7607
7607
  DEFAULT_CHUNKING_PARAMS: () => DEFAULT_CHUNKING_PARAMS,
7608
+ RequestTrace: () => RequestTrace,
7608
7609
  agglomerativeCluster: () => agglomerativeCluster,
7609
7610
  analyzeGitHistory: () => analyzeGitHistory,
7610
7611
  buildAndPersistImportGraph: () => buildAndPersistImportGraph,
@@ -7659,6 +7660,7 @@ __export(dist_exports, {
7659
7660
  resolveImport: () => resolveImport,
7660
7661
  runIndex: () => runIndex,
7661
7662
  setTelemetryEnabled: () => setTelemetryEnabled,
7663
+ settingsQueries: () => settingsQueries,
7662
7664
  startWatcher: () => startWatcher,
7663
7665
  statsQueries: () => statsQueries,
7664
7666
  stopWatcher: () => stopWatcher,
@@ -7722,6 +7724,18 @@ function openDb(customPath) {
7722
7724
  );
7723
7725
  if (!cols.has("sections")) sqlite.exec("ALTER TABLE file_metadata ADD COLUMN sections TEXT DEFAULT '[]'");
7724
7726
  if (!cols.has("internals")) sqlite.exec("ALTER TABLE file_metadata ADD COLUMN internals TEXT DEFAULT '[]'");
7727
+ const repoCols = new Set(
7728
+ sqlite.pragma("table_info(repos)").map((c) => c.name)
7729
+ );
7730
+ if (!repoCols.has("enable_embeddings")) sqlite.exec("ALTER TABLE repos ADD COLUMN enable_embeddings INTEGER NOT NULL DEFAULT 1");
7731
+ if (!repoCols.has("enable_summaries")) sqlite.exec("ALTER TABLE repos ADD COLUMN enable_summaries INTEGER NOT NULL DEFAULT 1");
7732
+ if (!repoCols.has("enable_vocab_clusters")) sqlite.exec("ALTER TABLE repos ADD COLUMN enable_vocab_clusters INTEGER NOT NULL DEFAULT 1");
7733
+ if (!repoCols.has("last_vocab_cluster_commit")) sqlite.exec("ALTER TABLE repos ADD COLUMN last_vocab_cluster_commit TEXT");
7734
+ const logCols = new Set(
7735
+ sqlite.pragma("table_info(request_logs)").map((c) => c.name)
7736
+ );
7737
+ if (!logCols.has("response_body")) sqlite.exec("ALTER TABLE request_logs ADD COLUMN response_body TEXT");
7738
+ if (!logCols.has("trace")) sqlite.exec("ALTER TABLE request_logs ADD COLUMN trace TEXT");
7725
7739
  _db = db;
7726
7740
  _raw = sqlite;
7727
7741
  return db;
@@ -7753,6 +7767,10 @@ function createTablesSql() {
7753
7767
  last_git_analysis_commit TEXT,
7754
7768
  max_import_depth INTEGER DEFAULT 0,
7755
7769
  vocab_clusters TEXT,
7770
+ last_vocab_cluster_commit TEXT,
7771
+ enable_embeddings INTEGER NOT NULL DEFAULT 1,
7772
+ enable_summaries INTEGER NOT NULL DEFAULT 1,
7773
+ enable_vocab_clusters INTEGER NOT NULL DEFAULT 1,
7756
7774
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
7757
7775
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7758
7776
  );
@@ -7847,11 +7865,19 @@ function createTablesSql() {
7847
7865
  source TEXT NOT NULL DEFAULT 'api',
7848
7866
  request_body TEXT,
7849
7867
  response_size INTEGER,
7868
+ response_body TEXT,
7869
+ trace TEXT,
7850
7870
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
7851
7871
  );
7852
7872
  CREATE INDEX IF NOT EXISTS idx_request_logs_created ON request_logs(created_at);
7853
7873
  CREATE INDEX IF NOT EXISTS idx_request_logs_source ON request_logs(source);
7854
7874
 
7875
+ CREATE TABLE IF NOT EXISTS settings (
7876
+ key TEXT PRIMARY KEY,
7877
+ value TEXT NOT NULL,
7878
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7879
+ );
7880
+
7855
7881
  CREATE TABLE IF NOT EXISTS telemetry_events (
7856
7882
  id TEXT PRIMARY KEY,
7857
7883
  event_type TEXT NOT NULL,
@@ -8206,9 +8232,15 @@ function extractExports(content, language) {
8206
8232
  case "rust":
8207
8233
  re = new RegExp(RUST_EXPORT_RE.source, "gm");
8208
8234
  break;
8209
- case "csharp":
8210
- re = new RegExp(CSHARP_EXPORT_RE.source, "gm");
8211
- break;
8235
+ case "csharp": {
8236
+ const exports2 = [];
8237
+ for (const pattern of [CSHARP_EXPORT_RE, CSHARP_EXPORT_METHOD_RE]) {
8238
+ for (const m of content.matchAll(new RegExp(pattern.source, "gm"))) {
8239
+ if (m[1] && !exports2.includes(m[1])) exports2.push(m[1]);
8240
+ }
8241
+ }
8242
+ return exports2.slice(0, 30);
8243
+ }
8212
8244
  case "java":
8213
8245
  case "kotlin":
8214
8246
  re = new RegExp(JAVA_EXPORT_RE.source, "gm");
@@ -8239,6 +8271,15 @@ function extractDocstring(content, language) {
8239
8271
  case "go":
8240
8272
  m = content.match(GO_PKG_RE);
8241
8273
  break;
8274
+ case "rust": {
8275
+ const rustMatch = content.match(RUST_DOC_RE);
8276
+ if (!rustMatch) return "";
8277
+ return rustMatch[0].replace(/^\s*\/\/!\s?/gm, "").replace(/\n/g, " ").trim().slice(0, 200);
8278
+ }
8279
+ case "java":
8280
+ case "kotlin":
8281
+ m = content.match(JSDOC_RE);
8282
+ break;
8242
8283
  default:
8243
8284
  return "";
8244
8285
  }
@@ -8260,33 +8301,69 @@ function extractSections(content) {
8260
8301
  }
8261
8302
  return sections.slice(0, 15);
8262
8303
  }
8263
- function extractInternals(content, language, exports) {
8264
- switch (language) {
8265
- case "typescript":
8266
- case "javascript":
8267
- case "tsx":
8268
- case "jsx":
8269
- break;
8270
- default:
8271
- return [];
8272
- }
8273
- const exportSet = new Set(exports);
8304
+ function collectMatches(content, regexes, exportSet, skipLine, skipName) {
8274
8305
  const seen = /* @__PURE__ */ new Set();
8275
- const internals = [];
8306
+ const results = [];
8276
8307
  for (const line of content.split("\n")) {
8277
- if (line.trimStart().startsWith("export")) continue;
8278
- for (const re of [TS_INTERNAL_FN_RE, TS_INTERNAL_CONST_RE]) {
8308
+ if (skipLine?.(line)) continue;
8309
+ for (const re of regexes) {
8279
8310
  const pattern = new RegExp(re.source, re.flags);
8280
8311
  for (const m of line.matchAll(pattern)) {
8281
8312
  const name = m[1];
8282
- if (name && name.length >= 6 && !exportSet.has(name) && !seen.has(name)) {
8313
+ if (name && name.length >= 6 && !exportSet.has(name) && !seen.has(name) && !skipName?.(name)) {
8283
8314
  seen.add(name);
8284
- internals.push(name);
8315
+ results.push(name);
8285
8316
  }
8286
8317
  }
8287
8318
  }
8288
8319
  }
8289
- return internals.slice(0, 20);
8320
+ return results.slice(0, 20);
8321
+ }
8322
+ function extractInternals(content, language, exports) {
8323
+ const exportSet = new Set(exports);
8324
+ switch (language) {
8325
+ case "typescript":
8326
+ case "javascript":
8327
+ case "tsx":
8328
+ case "jsx":
8329
+ return collectMatches(
8330
+ content,
8331
+ [TS_INTERNAL_FN_RE, TS_INTERNAL_CONST_RE],
8332
+ exportSet,
8333
+ (line) => line.trimStart().startsWith("export")
8334
+ );
8335
+ case "go":
8336
+ return collectMatches(content, GO_INTERNAL_RES, exportSet);
8337
+ case "python":
8338
+ return collectMatches(
8339
+ content,
8340
+ PY_INTERNAL_RES,
8341
+ exportSet,
8342
+ void 0,
8343
+ (name) => name.startsWith("_")
8344
+ );
8345
+ case "rust":
8346
+ return collectMatches(
8347
+ content,
8348
+ RUST_INTERNAL_RES,
8349
+ exportSet,
8350
+ (line) => /^\s*pub\s/.test(line)
8351
+ );
8352
+ case "csharp":
8353
+ return collectMatches(
8354
+ content,
8355
+ CSHARP_INTERNAL_RES,
8356
+ exportSet,
8357
+ (line) => /^\s*public\s/.test(line),
8358
+ (name) => /^(?:if|for|foreach|while|switch|using|catch|lock|return|throw|yield|try|do|else|new|await|base|this)$/.test(name)
8359
+ );
8360
+ case "java":
8361
+ return collectMatches(content, JAVA_INTERNAL_RES, exportSet);
8362
+ case "kotlin":
8363
+ return collectMatches(content, KOTLIN_INTERNAL_RES, exportSet);
8364
+ default:
8365
+ return [];
8366
+ }
8290
8367
  }
8291
8368
  function extractFileMetadata(path, content, language) {
8292
8369
  const exports = extractExports(content, language);
@@ -8317,7 +8394,17 @@ function extractAndPersistMetadata(db, repoId) {
8317
8394
  let count = 0;
8318
8395
  for (const [path, { content, language }] of files) {
8319
8396
  const meta = extractFileMetadata(path, content, language);
8320
- metadataQueries.upsert(db, repoId, path, language, meta.exports, meta.imports, meta.docstring, meta.sections, meta.internals);
8397
+ metadataQueries.upsert(
8398
+ db,
8399
+ repoId,
8400
+ path,
8401
+ language,
8402
+ meta.exports,
8403
+ meta.imports,
8404
+ meta.docstring,
8405
+ meta.sections,
8406
+ meta.internals
8407
+ );
8321
8408
  count++;
8322
8409
  }
8323
8410
  return count;
@@ -8351,140 +8438,6 @@ function buildAndPersistImportGraph(db, repoId) {
8351
8438
  }
8352
8439
  return edgeCount;
8353
8440
  }
8354
- function splitIdentifier(name) {
8355
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_\-./\\]/g, " ").toLowerCase().split(/\s+/).filter((w) => w.length >= 3 && !STOPWORDS.has(w));
8356
- }
8357
- function extractVocab(files) {
8358
- const termToFiles = /* @__PURE__ */ new Map();
8359
- for (const f of files) {
8360
- const pathParts = f.path.split("/");
8361
- for (const seg of pathParts) {
8362
- const base = seg.replace(/\.[^.]+$/, "");
8363
- for (const word of splitIdentifier(base)) {
8364
- const s = termToFiles.get(word) ?? /* @__PURE__ */ new Set();
8365
- s.add(f.path);
8366
- termToFiles.set(word, s);
8367
- }
8368
- }
8369
- for (const exp of f.exports) {
8370
- for (const word of splitIdentifier(exp)) {
8371
- const s = termToFiles.get(word) ?? /* @__PURE__ */ new Set();
8372
- s.add(f.path);
8373
- termToFiles.set(word, s);
8374
- }
8375
- }
8376
- }
8377
- const totalFiles = files.length;
8378
- const maxDf = Math.max(10, Math.floor(totalFiles * 0.3));
8379
- const filtered = [];
8380
- for (const [term, fileSet] of termToFiles) {
8381
- if (fileSet.size >= 2 && fileSet.size <= maxDf) {
8382
- filtered.push(term);
8383
- }
8384
- }
8385
- filtered.sort((a, b) => (termToFiles.get(b)?.size ?? 0) - (termToFiles.get(a)?.size ?? 0));
8386
- const terms = filtered.slice(0, MAX_TERMS);
8387
- return { terms, termToFiles };
8388
- }
8389
- function cosine(a, b) {
8390
- let dot = 0, na = 0, nb = 0;
8391
- for (let i = 0; i < a.length; i++) {
8392
- dot += a[i] * b[i];
8393
- na += a[i] * a[i];
8394
- nb += b[i] * b[i];
8395
- }
8396
- const denom = Math.sqrt(na) * Math.sqrt(nb);
8397
- return denom > 0 ? dot / denom : 0;
8398
- }
8399
- function agglomerativeCluster(terms, embeddings) {
8400
- const n = terms.length;
8401
- const pairs = [];
8402
- for (let i = 0; i < n; i++) {
8403
- for (let j = i + 1; j < n; j++) {
8404
- const sim = cosine(embeddings[i], embeddings[j]);
8405
- if (sim >= SIMILARITY_THRESHOLD) pairs.push({ i, j, sim });
8406
- }
8407
- }
8408
- pairs.sort((a, b) => b.sim - a.sim);
8409
- const parent = new Int32Array(n);
8410
- const size = new Uint16Array(n);
8411
- for (let i = 0; i < n; i++) {
8412
- parent[i] = i;
8413
- size[i] = 1;
8414
- }
8415
- function find(x) {
8416
- while (parent[x] !== x) {
8417
- parent[x] = parent[parent[x]];
8418
- x = parent[x];
8419
- }
8420
- return x;
8421
- }
8422
- for (const { i, j } of pairs) {
8423
- const ri = find(i), rj = find(j);
8424
- if (ri === rj) continue;
8425
- if (size[ri] + size[rj] > MAX_CLUSTER_SIZE) continue;
8426
- if (size[ri] >= size[rj]) {
8427
- parent[rj] = ri;
8428
- size[ri] += size[rj];
8429
- } else {
8430
- parent[ri] = rj;
8431
- size[rj] += size[ri];
8432
- }
8433
- }
8434
- const clusterMap = /* @__PURE__ */ new Map();
8435
- for (let i = 0; i < n; i++) {
8436
- const root = find(i);
8437
- const arr = clusterMap.get(root) ?? [];
8438
- arr.push(i);
8439
- clusterMap.set(root, arr);
8440
- }
8441
- return [...clusterMap.values()].filter((c) => c.length >= 2);
8442
- }
8443
- async function buildVocabClusters(db, repoId, caps) {
8444
- if (!caps?.embedTexts) {
8445
- console.error("[LENS] Vocab clusters: skipped (no embedTexts capability)");
8446
- return;
8447
- }
8448
- const rows = metadataQueries.getByRepo(db, repoId).map((r) => ({
8449
- path: r.path,
8450
- exports: Array.isArray(r.exports) ? r.exports : jsonParse(r.exports, [])
8451
- }));
8452
- if (rows.length === 0) {
8453
- console.error("[LENS] Vocab clusters: skipped (no metadata rows)");
8454
- return;
8455
- }
8456
- const { terms, termToFiles } = extractVocab(rows);
8457
- if (terms.length < 4) {
8458
- console.error(`[LENS] Vocab clusters: skipped (only ${terms.length} terms, need \u22654)`);
8459
- return;
8460
- }
8461
- console.error(`[LENS] Vocab clusters: embedding ${terms.length} terms in ${Math.ceil(terms.length / TERM_BATCH_SIZE)} batches...`);
8462
- let embeddings;
8463
- try {
8464
- const results = [];
8465
- for (let i = 0; i < terms.length; i += TERM_BATCH_SIZE) {
8466
- const batch = terms.slice(i, i + TERM_BATCH_SIZE);
8467
- const vecs = await caps.embedTexts(batch, false);
8468
- results.push(...vecs);
8469
- }
8470
- embeddings = results;
8471
- } catch (err) {
8472
- console.error(`[LENS] Vocab clusters: embed failed (${terms.length} terms):`, err.message);
8473
- return;
8474
- }
8475
- const rawClusters = agglomerativeCluster(terms, embeddings);
8476
- console.error(`[LENS] Vocab clusters: ${rawClusters.length} clusters from ${terms.length} terms`);
8477
- const clusters = rawClusters.map((indices) => {
8478
- const clusterTerms = indices.map((i) => terms[i]);
8479
- const fileSet = /* @__PURE__ */ new Set();
8480
- for (const t of clusterTerms) {
8481
- const files = termToFiles.get(t);
8482
- if (files) for (const f of files) fileSet.add(f);
8483
- }
8484
- return { terms: clusterTerms, files: [...fileSet].sort() };
8485
- }).filter((c) => c.files.length > 0).sort((a, b) => b.files.length - a.files.length).slice(0, MAX_CLUSTERS);
8486
- repoQueries.updateVocabClusters(db, repoId, clusters);
8487
- }
8488
8441
  async function analyzeGitHistory(db, repoId, rootPath, lastAnalyzedCommit) {
8489
8442
  const args = ["log", "--name-only", "--format=%H %aI", "--no-merges", `-n`, `${MAX_COMMITS}`];
8490
8443
  if (lastAnalyzedCommit) args.push(`${lastAnalyzedCommit}..HEAD`);
@@ -8605,7 +8558,7 @@ async function withLock(key, fn) {
8605
8558
  resolve22();
8606
8559
  }
8607
8560
  }
8608
- async function runIndex(db, repoId, caps, force = false, onProgress) {
8561
+ async function runIndex(db, repoId, caps, force = false, onProgress, trace) {
8609
8562
  return withLock(repoId, async () => {
8610
8563
  const start = Date.now();
8611
8564
  const repo = repoQueries.getById(db, repoId);
@@ -8621,17 +8574,21 @@ async function runIndex(db, repoId, caps, force = false, onProgress) {
8621
8574
  };
8622
8575
  }
8623
8576
  repoQueries.setIndexing(db, repoId);
8577
+ trace?.step("discovery");
8624
8578
  let files;
8625
8579
  if (!force && repo.last_indexed_commit) {
8626
8580
  files = await diffScan(repo.root_path, repo.last_indexed_commit, headCommit);
8627
8581
  } else {
8628
8582
  files = await fullScan(repo.root_path);
8629
8583
  }
8584
+ trace?.end("discovery", `${files.length} files`);
8585
+ trace?.step("chunking");
8630
8586
  let currentChunks = chunkQueries.countByRepo(db, repoId);
8631
8587
  let chunksCreated = 0;
8632
8588
  let chunksUnchanged = 0;
8633
8589
  let chunksDeleted = 0;
8634
- for (const file of files) {
8590
+ for (let fi = 0; fi < files.length; fi++) {
8591
+ const file = files[fi];
8635
8592
  if (file.status === "deleted") {
8636
8593
  chunksDeleted += chunkQueries.deleteByRepoPath(db, repoId, file.path);
8637
8594
  continue;
@@ -8680,14 +8637,39 @@ async function runIndex(db, repoId, caps, force = false, onProgress) {
8680
8637
  }
8681
8638
  }
8682
8639
  }
8640
+ const isFullScan = force || !repo.last_indexed_commit;
8641
+ if (isFullScan) {
8642
+ const scannedPaths = new Set(files.map((f) => f.path));
8643
+ const allChunkPaths = new Set(chunkQueries.getAllByRepo(db, repoId).map((c) => c.path));
8644
+ for (const path of allChunkPaths) {
8645
+ if (!scannedPaths.has(path)) {
8646
+ chunksDeleted += chunkQueries.deleteByRepoPath(db, repoId, path);
8647
+ }
8648
+ }
8649
+ }
8650
+ trace?.end("chunking", `+${chunksCreated} -${chunksDeleted}`);
8683
8651
  onProgress?.();
8652
+ trace?.step("extractMetadata");
8684
8653
  extractAndPersistMetadata(db, repoId);
8654
+ if (isFullScan) {
8655
+ const scannedPaths = new Set(files.map((f) => f.path));
8656
+ const allMeta = metadataQueries.getByRepo(db, repoId);
8657
+ for (const m of allMeta) {
8658
+ if (!scannedPaths.has(m.path)) {
8659
+ metadataQueries.deleteByPath(db, repoId, m.path);
8660
+ }
8661
+ }
8662
+ }
8663
+ trace?.end("extractMetadata");
8685
8664
  onProgress?.();
8686
- await buildVocabClusters(db, repoId, caps);
8665
+ trace?.step("importGraph");
8687
8666
  buildAndPersistImportGraph(db, repoId);
8688
8667
  computeMaxImportDepth(db, repoId);
8668
+ trace?.end("importGraph");
8689
8669
  onProgress?.();
8670
+ trace?.step("gitAnalysis");
8690
8671
  await analyzeGitHistory(db, repoId, repo.root_path, repo.last_git_analysis_commit);
8672
+ trace?.end("gitAnalysis");
8691
8673
  onProgress?.();
8692
8674
  repoQueries.updateIndexState(db, repoId, headCommit, "ready");
8693
8675
  onProgress?.();
@@ -8796,42 +8778,182 @@ async function ensureEmbedded(db, repoId, caps) {
8796
8778
  }
8797
8779
  if (poolFailed) break;
8798
8780
  }
8799
- return { embedded_count: embedded, skipped_count: 0, duration_ms: Date.now() - start };
8800
- }
8801
- async function enrichPurpose(db, repoId, caps, fullRun = false) {
8802
- const start = Date.now();
8803
- if (!caps?.generatePurpose) {
8804
- return { enriched: 0, skipped: 0, duration_ms: 0 };
8781
+ return { embedded_count: embedded, skipped_count: 0, duration_ms: Date.now() - start };
8782
+ }
8783
+ async function enrichPurpose(db, repoId, caps, fullRun = false) {
8784
+ const start = Date.now();
8785
+ if (!caps?.generatePurpose) {
8786
+ return { enriched: 0, skipped: 0, duration_ms: 0 };
8787
+ }
8788
+ let totalEnriched = 0;
8789
+ let totalSkipped = 0;
8790
+ while (true) {
8791
+ const candidates = metadataQueries.getCandidatesForPurpose(db, repoId, PURPOSE_BATCH_LIMIT);
8792
+ if (candidates.length === 0) break;
8793
+ for (let i = 0; i < candidates.length; i += PURPOSE_CONCURRENCY) {
8794
+ const batch = candidates.slice(i, i + PURPOSE_CONCURRENCY);
8795
+ const results = await Promise.allSettled(
8796
+ batch.map(async (row) => {
8797
+ const exports = Array.isArray(row.exports) ? row.exports : jsonParse(row.exports, []);
8798
+ const purpose = await caps.generatePurpose(row.path, row.first_chunk, exports, row.docstring ?? "");
8799
+ return { row, purpose };
8800
+ })
8801
+ );
8802
+ for (let j = 0; j < results.length; j++) {
8803
+ const result = results[j];
8804
+ const row = batch[j];
8805
+ if (result.status === "fulfilled" && result.value.purpose) {
8806
+ metadataQueries.updatePurpose(db, repoId, row.path, result.value.purpose, row.chunk_hash);
8807
+ totalEnriched++;
8808
+ } else {
8809
+ metadataQueries.setPurposeHash(db, repoId, row.path, row.chunk_hash);
8810
+ totalSkipped++;
8811
+ }
8812
+ }
8813
+ }
8814
+ if (!fullRun) break;
8815
+ }
8816
+ return { enriched: totalEnriched, skipped: totalSkipped, duration_ms: Date.now() - start };
8817
+ }
8818
+ function splitIdentifier(name) {
8819
+ return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_\-./\\]/g, " ").toLowerCase().split(/\s+/).filter((w) => w.length >= 3 && !STOPWORDS.has(w));
8820
+ }
8821
+ function extractVocab(files) {
8822
+ const termToFiles = /* @__PURE__ */ new Map();
8823
+ for (const f of files) {
8824
+ const pathParts = f.path.split("/");
8825
+ for (const seg of pathParts) {
8826
+ const base = seg.replace(/\.[^.]+$/, "");
8827
+ for (const word of splitIdentifier(base)) {
8828
+ const s = termToFiles.get(word) ?? /* @__PURE__ */ new Set();
8829
+ s.add(f.path);
8830
+ termToFiles.set(word, s);
8831
+ }
8832
+ }
8833
+ for (const exp of f.exports) {
8834
+ for (const word of splitIdentifier(exp)) {
8835
+ const s = termToFiles.get(word) ?? /* @__PURE__ */ new Set();
8836
+ s.add(f.path);
8837
+ termToFiles.set(word, s);
8838
+ }
8839
+ }
8840
+ }
8841
+ const totalFiles = files.length;
8842
+ const maxDf = Math.max(10, Math.floor(totalFiles * 0.3));
8843
+ const filtered = [];
8844
+ for (const [term, fileSet] of termToFiles) {
8845
+ if (fileSet.size >= 2 && fileSet.size <= maxDf) {
8846
+ filtered.push(term);
8847
+ }
8848
+ }
8849
+ filtered.sort((a, b) => (termToFiles.get(b)?.size ?? 0) - (termToFiles.get(a)?.size ?? 0));
8850
+ const terms = filtered.slice(0, MAX_TERMS);
8851
+ return { terms, termToFiles };
8852
+ }
8853
+ function cosine(a, b) {
8854
+ let dot = 0, na = 0, nb = 0;
8855
+ for (let i = 0; i < a.length; i++) {
8856
+ dot += a[i] * b[i];
8857
+ na += a[i] * a[i];
8858
+ nb += b[i] * b[i];
8859
+ }
8860
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
8861
+ return denom > 0 ? dot / denom : 0;
8862
+ }
8863
+ function agglomerativeCluster(terms, embeddings) {
8864
+ const n = terms.length;
8865
+ const pairs = [];
8866
+ for (let i = 0; i < n; i++) {
8867
+ for (let j = i + 1; j < n; j++) {
8868
+ const sim = cosine(embeddings[i], embeddings[j]);
8869
+ if (sim >= SIMILARITY_THRESHOLD) pairs.push({ i, j, sim });
8870
+ }
8871
+ }
8872
+ pairs.sort((a, b) => b.sim - a.sim);
8873
+ const parent = new Int32Array(n);
8874
+ const size = new Uint16Array(n);
8875
+ for (let i = 0; i < n; i++) {
8876
+ parent[i] = i;
8877
+ size[i] = 1;
8878
+ }
8879
+ function find(x) {
8880
+ while (parent[x] !== x) {
8881
+ parent[x] = parent[parent[x]];
8882
+ x = parent[x];
8883
+ }
8884
+ return x;
8885
+ }
8886
+ for (const { i, j } of pairs) {
8887
+ const ri = find(i), rj = find(j);
8888
+ if (ri === rj) continue;
8889
+ if (size[ri] + size[rj] > MAX_CLUSTER_SIZE) continue;
8890
+ if (size[ri] >= size[rj]) {
8891
+ parent[rj] = ri;
8892
+ size[ri] += size[rj];
8893
+ } else {
8894
+ parent[ri] = rj;
8895
+ size[rj] += size[ri];
8896
+ }
8897
+ }
8898
+ const clusterMap = /* @__PURE__ */ new Map();
8899
+ for (let i = 0; i < n; i++) {
8900
+ const root = find(i);
8901
+ const arr = clusterMap.get(root) ?? [];
8902
+ arr.push(i);
8903
+ clusterMap.set(root, arr);
8904
+ }
8905
+ return [...clusterMap.values()].filter((c) => c.length >= 2);
8906
+ }
8907
+ async function buildVocabClusters(db, repoId, caps, force = false) {
8908
+ if (!caps?.embedTexts) {
8909
+ console.error("[LENS] Vocab clusters: skipped (no embedTexts capability)");
8910
+ return;
8911
+ }
8912
+ const repo = repoQueries.getById(db, repoId);
8913
+ if (!repo) return;
8914
+ if (!force && repo.last_vocab_cluster_commit && repo.last_vocab_cluster_commit === repo.last_indexed_commit) {
8915
+ console.error("[LENS] Vocab clusters: skipped (unchanged since last build)");
8916
+ return;
8917
+ }
8918
+ const rows = metadataQueries.getByRepo(db, repoId).map((r) => ({
8919
+ path: r.path,
8920
+ exports: Array.isArray(r.exports) ? r.exports : jsonParse(r.exports, [])
8921
+ }));
8922
+ if (rows.length === 0) {
8923
+ console.error("[LENS] Vocab clusters: skipped (no metadata rows)");
8924
+ return;
8925
+ }
8926
+ const { terms, termToFiles } = extractVocab(rows);
8927
+ if (terms.length < 4) {
8928
+ console.error(`[LENS] Vocab clusters: skipped (only ${terms.length} terms, need \u22654)`);
8929
+ return;
8805
8930
  }
8806
- let totalEnriched = 0;
8807
- let totalSkipped = 0;
8808
- while (true) {
8809
- const candidates = metadataQueries.getCandidatesForPurpose(db, repoId, PURPOSE_BATCH_LIMIT);
8810
- if (candidates.length === 0) break;
8811
- for (let i = 0; i < candidates.length; i += PURPOSE_CONCURRENCY) {
8812
- const batch = candidates.slice(i, i + PURPOSE_CONCURRENCY);
8813
- const results = await Promise.allSettled(
8814
- batch.map(async (row) => {
8815
- const exports = Array.isArray(row.exports) ? row.exports : jsonParse(row.exports, []);
8816
- const purpose = await caps.generatePurpose(row.path, row.first_chunk, exports, row.docstring ?? "");
8817
- return { row, purpose };
8818
- })
8819
- );
8820
- for (let j = 0; j < results.length; j++) {
8821
- const result = results[j];
8822
- const row = batch[j];
8823
- if (result.status === "fulfilled" && result.value.purpose) {
8824
- metadataQueries.updatePurpose(db, repoId, row.path, result.value.purpose, row.chunk_hash);
8825
- totalEnriched++;
8826
- } else {
8827
- metadataQueries.setPurposeHash(db, repoId, row.path, row.chunk_hash);
8828
- totalSkipped++;
8829
- }
8830
- }
8931
+ console.error(`[LENS] Vocab clusters: embedding ${terms.length} terms in ${Math.ceil(terms.length / TERM_BATCH_SIZE)} batches...`);
8932
+ let embeddings;
8933
+ try {
8934
+ const results = [];
8935
+ for (let i = 0; i < terms.length; i += TERM_BATCH_SIZE) {
8936
+ const batch = terms.slice(i, i + TERM_BATCH_SIZE);
8937
+ const vecs = await caps.embedTexts(batch, false);
8938
+ results.push(...vecs);
8831
8939
  }
8832
- if (!fullRun) break;
8940
+ embeddings = results;
8941
+ } catch (err) {
8942
+ console.error(`[LENS] Vocab clusters: embed failed (${terms.length} terms):`, err.message);
8943
+ return;
8833
8944
  }
8834
- return { enriched: totalEnriched, skipped: totalSkipped, duration_ms: Date.now() - start };
8945
+ const rawClusters = agglomerativeCluster(terms, embeddings);
8946
+ console.error(`[LENS] Vocab clusters: ${rawClusters.length} clusters from ${terms.length} terms`);
8947
+ const clusters = rawClusters.map((indices) => {
8948
+ const clusterTerms = indices.map((i) => terms[i]);
8949
+ const fileSet = /* @__PURE__ */ new Set();
8950
+ for (const t of clusterTerms) {
8951
+ const files = termToFiles.get(t);
8952
+ if (files) for (const f of files) fileSet.add(f);
8953
+ }
8954
+ return { terms: clusterTerms, files: [...fileSet].sort() };
8955
+ }).filter((c) => c.files.length > 0).sort((a, b) => b.files.length - a.files.length).slice(0, MAX_CLUSTERS);
8956
+ repoQueries.updateVocabClusters(db, repoId, clusters, repo.last_indexed_commit ?? void 0);
8835
8957
  }
8836
8958
  async function handleFileChange(db, repoId, repoRoot, absPath) {
8837
8959
  const relPath = relative3(repoRoot, absPath);
@@ -9129,6 +9251,16 @@ function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, m
9129
9251
  const { exact, stemmed, clusterFiles } = expandKeywords(rawWords, vocabClusters ?? null);
9130
9252
  const allTerms = [...exact, ...stemmed];
9131
9253
  const termWeights = buildTermWeights(allTerms, metadata);
9254
+ const exportTokensMap = /* @__PURE__ */ new Map();
9255
+ for (const f of metadata) {
9256
+ const tokens = /* @__PURE__ */ new Set();
9257
+ for (const exp of f.exports ?? []) {
9258
+ for (const part of exp.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s_-]+/)) {
9259
+ if (part.length >= 3 && !STOPWORDS2.has(part)) tokens.add(part);
9260
+ }
9261
+ }
9262
+ exportTokensMap.set(f.path, tokens);
9263
+ }
9132
9264
  const scored = metadata.map((f) => {
9133
9265
  let score = 0;
9134
9266
  let matchedTerms = 0;
@@ -9143,12 +9275,14 @@ function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, m
9143
9275
  const fileTokens = fileName.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[._-]/g, " ").toLowerCase().split(/\s+/).filter((t) => t.length >= 2);
9144
9276
  const dirTokens = pathSegments.slice(-4, -1).flatMap((s) => s.replace(/\./g, " ").split(/\s+/)).filter((t) => t.length >= 2);
9145
9277
  const pathTokenSet = /* @__PURE__ */ new Set([...fileTokens, ...dirTokens]);
9278
+ const expTokens = exportTokensMap.get(f.path);
9146
9279
  for (const w of exact) {
9147
9280
  const weight = termWeights.get(w) ?? 1;
9148
9281
  let termScore = 0;
9149
9282
  if (fileTokens.some((t) => t === w || t.includes(w))) termScore += 4 * weight;
9150
9283
  else if (pathTokenSet.has(w)) termScore += 2 * weight;
9151
- if (exportsLower.includes(w)) termScore += 2 * weight;
9284
+ if (expTokens?.has(w)) termScore += 2.5 * weight;
9285
+ else if (exportsLower.includes(w)) termScore += 2 * weight;
9152
9286
  if (docLower.includes(w) || purposeLower.includes(w)) termScore += 1 * weight;
9153
9287
  if (sectionsLower.includes(w)) termScore += 1 * weight;
9154
9288
  if (internalsLower.includes(w)) termScore += 1.5 * weight;
@@ -9172,6 +9306,10 @@ function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, m
9172
9306
  if (score > 0 && isNoisePath(f.path)) {
9173
9307
  score *= 0.3;
9174
9308
  }
9309
+ const exportCount = (f.exports ?? []).length;
9310
+ if (exportCount > 5 && score > 0) {
9311
+ score *= 1 / (1 + Math.log2(exportCount / 5) * 0.3);
9312
+ }
9175
9313
  const stats = fileStats2?.get(f.path);
9176
9314
  if (stats && stats.recent_count > 0 && score > 0) {
9177
9315
  score += Math.min(stats.recent_count, 5) * 0.5;
@@ -9310,8 +9448,8 @@ async function vectorSearch(db, repoId, query, limit, caps, codeOnly = true) {
9310
9448
  })).filter((c) => !codeOnly || !isDocFile(c.path)).sort((a, b) => b.score - a.score).slice(0, limit);
9311
9449
  return scored;
9312
9450
  }
9313
- function cacheKey(repoId, goal, commit) {
9314
- return `${repoId}:${commit ?? ""}:${goal}`;
9451
+ function cacheKey(repoId, goal, commit, emb = true) {
9452
+ return `${repoId}:${commit ?? ""}:${emb ? "e" : "n"}:${goal}`;
9315
9453
  }
9316
9454
  function cacheGet(key) {
9317
9455
  const entry = cache.get(key);
@@ -9329,30 +9467,41 @@ function cacheSet(key, response) {
9329
9467
  }
9330
9468
  cache.set(key, { response, expires: Date.now() + CACHE_TTL });
9331
9469
  }
9332
- async function buildContext(db, repoId, goal, caps) {
9470
+ async function buildContext(db, repoId, goal, caps, trace, options) {
9333
9471
  const start = Date.now();
9334
9472
  try {
9473
+ trace?.step("ensureIndexed");
9335
9474
  await ensureIndexed(db, repoId, caps);
9475
+ trace?.end("ensureIndexed");
9336
9476
  const repo = repoQueries.getById(db, repoId);
9337
9477
  const commit = repo?.last_indexed_commit ?? null;
9338
- const key = cacheKey(repoId, goal, commit);
9478
+ const useEmb = options?.useEmbeddings !== false;
9479
+ const key = cacheKey(repoId, goal, commit, useEmb);
9339
9480
  const cached2 = cacheGet(key);
9340
9481
  if (cached2) {
9482
+ trace?.add("cache", 0, "HIT");
9341
9483
  track(db, "context", { duration_ms: Date.now() - start, result_count: cached2.stats.files_in_context, cache_hit: true });
9342
9484
  return { ...cached2, stats: { ...cached2.stats, cached: true, duration_ms: Date.now() - start } };
9343
9485
  }
9344
- const embAvailable = (await Promise.resolve().then(() => (init_queries(), queries_exports))).chunkQueries.hasEmbeddings(db, repoId);
9486
+ const embAvailable = useEmb && (await Promise.resolve().then(() => (init_queries(), queries_exports))).chunkQueries.hasEmbeddings(db, repoId);
9487
+ trace?.step("loadStructural");
9345
9488
  const metadata = loadFileMetadata(db, repoId);
9346
9489
  const allStats = getAllFileStats(db, repoId);
9347
9490
  const vocabClusters = loadVocabClusters(db, repoId);
9348
9491
  const indegreeMap = getIndegrees(db, repoId);
9349
9492
  const maxImportDepth = repo?.max_import_depth ?? 0;
9493
+ trace?.end("loadStructural");
9494
+ trace?.step("vectorSearch");
9350
9495
  const vecResults = embAvailable ? await vectorSearch(db, repoId, goal, 10, caps, true).catch(() => []) : [];
9496
+ trace?.end("vectorSearch", `${vecResults.length} results`);
9351
9497
  const statsForInterpreter = /* @__PURE__ */ new Map();
9352
9498
  for (const [path, stat32] of allStats) {
9353
9499
  statsForInterpreter.set(path, { commit_count: stat32.commit_count, recent_count: stat32.recent_count });
9354
9500
  }
9501
+ trace?.step("interpretQuery");
9355
9502
  const interpreted = interpretQuery(goal, metadata, statsForInterpreter, vocabClusters, indegreeMap, maxImportDepth);
9503
+ trace?.end("interpretQuery", `${interpreted.files.length} files`);
9504
+ trace?.step("cochangePromotion");
9356
9505
  const topForCochange = interpreted.files.slice(0, 5).map((f) => f.path);
9357
9506
  const cochangePartners = getCochangePartners(db, repoId, topForCochange, 3, 20);
9358
9507
  const existingPathSet = new Set(interpreted.files.map((f) => f.path));
@@ -9367,6 +9516,7 @@ async function buildContext(db, repoId, goal, caps) {
9367
9516
  existingPathSet.add(cp.partner);
9368
9517
  promoted++;
9369
9518
  }
9519
+ trace?.end("cochangePromotion", `${promoted} promoted`);
9370
9520
  if (vecResults.length > 0) {
9371
9521
  const vecByFile = /* @__PURE__ */ new Map();
9372
9522
  for (const r of vecResults) {
@@ -9396,6 +9546,7 @@ async function buildContext(db, repoId, goal, caps) {
9396
9546
  }
9397
9547
  const hitPaths = interpreted.files.map((f) => f.path);
9398
9548
  const topPaths = hitPaths.slice(0, 3);
9549
+ trace?.step("structuralEnrichment");
9399
9550
  const reverseImports = getReverseImports(db, repoId, hitPaths);
9400
9551
  const forwardImports = getForwardImports(db, repoId, hitPaths);
9401
9552
  const hop2Deps = get2HopReverseDeps(db, repoId, topPaths);
@@ -9431,6 +9582,8 @@ async function buildContext(db, repoId, goal, caps) {
9431
9582
  }
9432
9583
  }
9433
9584
  }
9585
+ trace?.end("structuralEnrichment");
9586
+ trace?.step("formatContextPack");
9434
9587
  const data = {
9435
9588
  goal,
9436
9589
  files: interpreted.files,
@@ -9442,6 +9595,7 @@ async function buildContext(db, repoId, goal, caps) {
9442
9595
  fileStats: allStats
9443
9596
  };
9444
9597
  const contextPack = formatContextPack(data);
9598
+ trace?.end("formatContextPack");
9445
9599
  const response = {
9446
9600
  context_pack: contextPack,
9447
9601
  stats: {
@@ -9463,7 +9617,7 @@ Context generation failed.`,
9463
9617
  };
9464
9618
  }
9465
9619
  }
9466
- var __defProp2, __getOwnPropNames2, __esm2, __export2, schema_exports, uuid, now, updatedAt, repos, chunks, fileMetadata, fileImports, fileStats, fileCochanges, usageCounters, requestLogs, telemetryEvents, init_schema, queries_exports, repoQueries, chunkQueries, metadataQueries, importQueries, statsQueries, cochangeQueries, logQueries, usageQueries, telemetryQueries, init_queries, _db, _raw, execFileAsync, MAX_FILE_SIZE, BINARY_EXTENSIONS, DOCS_EXTENSIONS, LANG_MAP, DEFAULT_CHUNKING_PARAMS, TS_IMPORT_RE, PY_IMPORT_RE, GO_IMPORT_RE, RUST_USE_RE, TS_EXTENSIONS, PY_EXTENSIONS, TS_EXPORT_RE, PY_EXPORT_RE, GO_EXPORT_RE, RUST_EXPORT_RE, CSHARP_EXPORT_RE, JAVA_EXPORT_RE, JSDOC_RE, PY_DOCSTRING_RE, CSHARP_DOC_RE, GO_PKG_RE, SECTION_SINGLE_RE, SECTION_BLOCK_RE, TS_INTERNAL_FN_RE, TS_INTERNAL_CONST_RE, TERM_BATCH_SIZE, SIMILARITY_THRESHOLD, MAX_TERMS, MAX_CLUSTERS, MAX_CLUSTER_SIZE, STOPWORDS, execFileAsync2, MAX_COMMITS, MAX_FILES_PER_COMMIT, RECENT_DAYS, _enabled, MAX_CHUNKS_PER_REPO, locks, MAX_API_CALLS, POOL_SIZE, MAX_BATCH_TOKENS, CHARS_PER_TOKEN, PURPOSE_BATCH_LIMIT, PURPOSE_CONCURRENCY, watchers, debounceTimers, IGNORED, DEBOUNCE_MS, STOPWORDS2, NOISE_EXTENSIONS, NOISE_PATHS, CONCEPT_SYNONYMS, CACHE_TTL, CACHE_MAX, cache;
9620
+ var __defProp2, __getOwnPropNames2, __esm2, __export2, schema_exports, uuid, now, updatedAt, repos, chunks, fileMetadata, fileImports, fileStats, fileCochanges, usageCounters, requestLogs, settings, telemetryEvents, init_schema, queries_exports, repoQueries, chunkQueries, metadataQueries, importQueries, statsQueries, cochangeQueries, logQueries, usageQueries, telemetryQueries, settingsQueries, init_queries, _db, _raw, execFileAsync, MAX_FILE_SIZE, BINARY_EXTENSIONS, DOCS_EXTENSIONS, LANG_MAP, DEFAULT_CHUNKING_PARAMS, TS_IMPORT_RE, PY_IMPORT_RE, GO_IMPORT_RE, RUST_USE_RE, TS_EXTENSIONS, PY_EXTENSIONS, TS_EXPORT_RE, PY_EXPORT_RE, GO_EXPORT_RE, RUST_EXPORT_RE, CSHARP_EXPORT_RE, CSHARP_EXPORT_METHOD_RE, JAVA_EXPORT_RE, JSDOC_RE, PY_DOCSTRING_RE, CSHARP_DOC_RE, GO_PKG_RE, RUST_DOC_RE, SECTION_SINGLE_RE, SECTION_BLOCK_RE, TS_INTERNAL_FN_RE, TS_INTERNAL_CONST_RE, GO_INTERNAL_RES, PY_INTERNAL_RES, RUST_INTERNAL_RES, CSHARP_INTERNAL_RES, JAVA_INTERNAL_RES, KOTLIN_INTERNAL_RES, execFileAsync2, MAX_COMMITS, MAX_FILES_PER_COMMIT, RECENT_DAYS, _enabled, MAX_CHUNKS_PER_REPO, locks, MAX_API_CALLS, POOL_SIZE, MAX_BATCH_TOKENS, CHARS_PER_TOKEN, PURPOSE_BATCH_LIMIT, PURPOSE_CONCURRENCY, TERM_BATCH_SIZE, SIMILARITY_THRESHOLD, MAX_TERMS, MAX_CLUSTERS, MAX_CLUSTER_SIZE, STOPWORDS, watchers, debounceTimers, IGNORED, DEBOUNCE_MS, STOPWORDS2, NOISE_EXTENSIONS, NOISE_PATHS, CONCEPT_SYNONYMS, CACHE_TTL, CACHE_MAX, cache, RequestTrace;
9467
9621
  var init_dist = __esm({
9468
9622
  "packages/engine/dist/index.js"() {
9469
9623
  "use strict";
@@ -9490,6 +9644,7 @@ var init_dist = __esm({
9490
9644
  fileStats: () => fileStats,
9491
9645
  repos: () => repos,
9492
9646
  requestLogs: () => requestLogs,
9647
+ settings: () => settings,
9493
9648
  telemetryEvents: () => telemetryEvents,
9494
9649
  usageCounters: () => usageCounters
9495
9650
  });
@@ -9513,6 +9668,10 @@ var init_dist = __esm({
9513
9668
  last_git_analysis_commit: text("last_git_analysis_commit"),
9514
9669
  max_import_depth: integer("max_import_depth").default(0),
9515
9670
  vocab_clusters: text("vocab_clusters"),
9671
+ last_vocab_cluster_commit: text("last_vocab_cluster_commit"),
9672
+ enable_embeddings: integer("enable_embeddings").notNull().default(1),
9673
+ enable_summaries: integer("enable_summaries").notNull().default(1),
9674
+ enable_vocab_clusters: integer("enable_vocab_clusters").notNull().default(1),
9516
9675
  created_at: now(),
9517
9676
  updated_at: updatedAt()
9518
9677
  },
@@ -9626,10 +9785,17 @@ var init_dist = __esm({
9626
9785
  source: text("source").notNull().default("api"),
9627
9786
  request_body: text("request_body"),
9628
9787
  response_size: integer("response_size"),
9788
+ response_body: text("response_body"),
9789
+ trace: text("trace"),
9629
9790
  created_at: now()
9630
9791
  },
9631
9792
  (t) => [index("idx_request_logs_created").on(t.created_at), index("idx_request_logs_source").on(t.source)]
9632
9793
  );
9794
+ settings = sqliteTable("settings", {
9795
+ key: text("key").primaryKey(),
9796
+ value: text("value").notNull(),
9797
+ updated_at: updatedAt()
9798
+ });
9633
9799
  telemetryEvents = sqliteTable(
9634
9800
  "telemetry_events",
9635
9801
  {
@@ -9656,6 +9822,7 @@ var init_dist = __esm({
9656
9822
  logQueries: () => logQueries,
9657
9823
  metadataQueries: () => metadataQueries,
9658
9824
  repoQueries: () => repoQueries,
9825
+ settingsQueries: () => settingsQueries,
9659
9826
  statsQueries: () => statsQueries,
9660
9827
  telemetryQueries: () => telemetryQueries,
9661
9828
  toEmbeddingBlob: () => toEmbeddingBlob,
@@ -9716,8 +9883,10 @@ var init_dist = __esm({
9716
9883
  updateMaxDepth(db, id, depth) {
9717
9884
  db.update(repos).set({ max_import_depth: depth }).where(eq(repos.id, id)).run();
9718
9885
  },
9719
- updateVocabClusters(db, id, clusters) {
9720
- db.update(repos).set({ vocab_clusters: JSON.stringify(clusters) }).where(eq(repos.id, id)).run();
9886
+ updateVocabClusters(db, id, clusters, commit) {
9887
+ const set = { vocab_clusters: JSON.stringify(clusters) };
9888
+ if (commit) set.last_vocab_cluster_commit = commit;
9889
+ db.update(repos).set(set).where(eq(repos.id, id)).run();
9721
9890
  },
9722
9891
  updateGitAnalysisCommit(db, id, commit) {
9723
9892
  db.update(repos).set({ last_git_analysis_commit: commit }).where(eq(repos.id, id)).run();
@@ -9725,6 +9894,13 @@ var init_dist = __esm({
9725
9894
  setIndexing(db, id) {
9726
9895
  db.update(repos).set({ index_status: "indexing" }).where(eq(repos.id, id)).run();
9727
9896
  },
9897
+ updateProFeatures(db, id, flags) {
9898
+ const set = { updated_at: sql`datetime('now')` };
9899
+ if (flags.enable_embeddings !== void 0) set.enable_embeddings = flags.enable_embeddings;
9900
+ if (flags.enable_summaries !== void 0) set.enable_summaries = flags.enable_summaries;
9901
+ if (flags.enable_vocab_clusters !== void 0) set.enable_vocab_clusters = flags.enable_vocab_clusters;
9902
+ db.update(repos).set(set).where(eq(repos.id, id)).run();
9903
+ },
9728
9904
  remove(db, id) {
9729
9905
  db.delete(fileCochanges).where(eq(fileCochanges.repo_id, id)).run();
9730
9906
  db.delete(fileStats).where(eq(fileStats.repo_id, id)).run();
@@ -9936,6 +10112,9 @@ var init_dist = __esm({
9936
10112
  purpose_count,
9937
10113
  purpose_total
9938
10114
  };
10115
+ },
10116
+ deleteByPath(db, repoId, path) {
10117
+ db.delete(fileMetadata).where(and(eq(fileMetadata.repo_id, repoId), eq(fileMetadata.path, path))).run();
9939
10118
  }
9940
10119
  };
9941
10120
  importQueries = {
@@ -10059,7 +10238,7 @@ var init_dist = __esm({
10059
10238
  }
10060
10239
  };
10061
10240
  logQueries = {
10062
- insert(db, method, path, status, durationMs, source, requestBody, responseSize) {
10241
+ insert(db, method, path, status, durationMs, source, requestBody, responseSize, responseBody, trace) {
10063
10242
  db.insert(requestLogs).values({
10064
10243
  id: randomUUID(),
10065
10244
  method,
@@ -10068,7 +10247,9 @@ var init_dist = __esm({
10068
10247
  duration_ms: durationMs,
10069
10248
  source,
10070
10249
  request_body: requestBody ?? null,
10071
- response_size: responseSize ?? null
10250
+ response_size: responseSize ?? null,
10251
+ response_body: responseBody ?? null,
10252
+ trace: trace ?? null
10072
10253
  }).run();
10073
10254
  },
10074
10255
  list(db, opts = {}) {
@@ -10148,6 +10329,25 @@ var init_dist = __esm({
10148
10329
  return result.changes;
10149
10330
  }
10150
10331
  };
10332
+ settingsQueries = {
10333
+ get(db, key) {
10334
+ const row = db.select({ value: settings.value }).from(settings).where(eq(settings.key, key)).get();
10335
+ return row?.value ?? null;
10336
+ },
10337
+ set(db, key, value) {
10338
+ db.insert(settings).values({ key, value }).onConflictDoUpdate({
10339
+ target: settings.key,
10340
+ set: { value, updated_at: sql`datetime('now')` }
10341
+ }).run();
10342
+ },
10343
+ getAll(db) {
10344
+ const rows = db.select().from(settings).all();
10345
+ return Object.fromEntries(rows.map((r) => [r.key, r.value]));
10346
+ },
10347
+ delete(db, key) {
10348
+ db.delete(settings).where(eq(settings.key, key)).run();
10349
+ }
10350
+ };
10151
10351
  }
10152
10352
  });
10153
10353
  init_schema();
@@ -10155,7 +10355,7 @@ var init_dist = __esm({
10155
10355
  _raw = null;
10156
10356
  init_queries();
10157
10357
  execFileAsync = promisify(execFile);
10158
- MAX_FILE_SIZE = 500 * 1024;
10358
+ MAX_FILE_SIZE = 2 * 1024 * 1024;
10159
10359
  BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
10160
10360
  ".png",
10161
10361
  ".jpg",
@@ -10247,17 +10447,66 @@ var init_dist = __esm({
10247
10447
  PY_EXPORT_RE = /^(?:def|class)\s+(\w+)/gm;
10248
10448
  GO_EXPORT_RE = /^func\s+([A-Z]\w*)/gm;
10249
10449
  RUST_EXPORT_RE = /^pub\s+(?:fn|struct|enum|trait|type|mod)\s+(\w+)/gm;
10250
- CSHARP_EXPORT_RE = /^(?:public|internal)\s+(?:static\s+)?(?:abstract\s+|sealed\s+|partial\s+)?(?:class|interface|enum|struct|record|delegate)\s+(\w+)/gm;
10251
- JAVA_EXPORT_RE = /^(?:public)\s+(?:static\s+)?(?:abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)/gm;
10450
+ CSHARP_EXPORT_RE = /^\s*(?:public|internal)\s+(?:static\s+)?(?:abstract\s+|sealed\s+|partial\s+)?(?:class|interface|enum|struct|record|delegate)\s+(\w+)/gm;
10451
+ CSHARP_EXPORT_METHOD_RE = /^\s*public\s+(?:static\s+)?(?:async\s+)?(?:override\s+)?(?:virtual\s+)?[\w<>\[\]?.]+\s+(\w+)\s*\(/gm;
10452
+ JAVA_EXPORT_RE = /^\s*(?:public)\s+(?:static\s+)?(?:abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)/gm;
10252
10453
  JSDOC_RE = /^\/\*\*\s*([\s\S]*?)\*\//m;
10253
10454
  PY_DOCSTRING_RE = /^(?:["']{3})([\s\S]*?)(?:["']{3})/m;
10254
10455
  CSHARP_DOC_RE = /^(?:\s*\/\/\/\s*(.*))+/m;
10255
10456
  GO_PKG_RE = /^\/\/\s*Package\s+\w+\s+(.*)/m;
10457
+ RUST_DOC_RE = /^(?:\s*\/\/!\s*(.*)(?:\n\s*\/\/!\s*(.*))*)/m;
10256
10458
  SECTION_SINGLE_RE = /^(?:\/\/|#)\s*[-=]{3,}\s*(.+?)\s*[-=]{3,}\s*$/gm;
10257
10459
  SECTION_BLOCK_RE = /^\/\*\s*[-=]{3,}\s*(.+?)\s*[-=]{3,}\s*\*\/$/gm;
10258
10460
  TS_INTERNAL_FN_RE = /^(?:async\s+)?function\s+(\w+)/gm;
10259
10461
  TS_INTERNAL_CONST_RE = /^(?:const|let)\s+(\w+)\s*=/gm;
10462
+ GO_INTERNAL_RES = [
10463
+ /^func\s+([a-z]\w*)/gm,
10464
+ /^var\s+([a-z]\w*)/gm,
10465
+ /^const\s+([a-z]\w*)/gm
10466
+ ];
10467
+ PY_INTERNAL_RES = [/^def\s+(\w+)/gm, /^class\s+(\w+)/gm];
10468
+ RUST_INTERNAL_RES = [
10469
+ /^(?:async\s+)?fn\s+(\w+)/gm,
10470
+ /^struct\s+(\w+)/gm,
10471
+ /^enum\s+(\w+)/gm,
10472
+ /^trait\s+(\w+)/gm,
10473
+ /^type\s+(\w+)/gm,
10474
+ /^const\s+(\w+)/gm
10475
+ ];
10476
+ CSHARP_INTERNAL_RES = [
10477
+ // Types with explicit access modifier
10478
+ /^\s*(?:private|protected|internal)\s+(?:static\s+)?(?:abstract\s+|sealed\s+|partial\s+)?(?:class|interface|enum|struct|record)\s+(\w+)/gm,
10479
+ // Methods with explicit access modifier
10480
+ /^\s*(?:private|protected|internal)\s+(?:static\s+)?(?:async\s+)?[\w<>\[\]?.]+\s+(\w+)\s*\(/gm,
10481
+ // Methods with no access modifier (implicitly private in C#): returnType Name(
10482
+ /^\s+(?:static\s+)?(?:async\s+)?(?:override\s+)?(?:virtual\s+)?[\w<>\[\]?.]+\s+(\w+)\s*\(/gm
10483
+ ];
10484
+ JAVA_INTERNAL_RES = [
10485
+ /^\s*(?:private|protected)\s+(?:static\s+)?(?:final\s+|abstract\s+)?(?:class|interface|enum|record)\s+(\w+)/gm,
10486
+ /^\s*(?:private|protected)\s+(?:static\s+)?(?:final\s+|abstract\s+)?[\w<>\[\].]+\s+(\w+)\s*\(/gm
10487
+ ];
10488
+ KOTLIN_INTERNAL_RES = [
10489
+ /^\s*private\s+(?:suspend\s+)?fun\s+(\w+)/gm,
10490
+ /^\s*private\s+(?:data\s+)?(?:class|interface|object|enum)\s+(\w+)/gm
10491
+ ];
10492
+ init_queries();
10493
+ init_queries();
10494
+ execFileAsync2 = promisify2(execFile2);
10495
+ MAX_COMMITS = 2e3;
10496
+ MAX_FILES_PER_COMMIT = 20;
10497
+ RECENT_DAYS = 90;
10498
+ init_queries();
10499
+ _enabled = true;
10500
+ MAX_CHUNKS_PER_REPO = 1e5;
10501
+ locks = /* @__PURE__ */ new Map();
10502
+ init_queries();
10503
+ MAX_API_CALLS = 2e3;
10504
+ POOL_SIZE = 32;
10505
+ MAX_BATCH_TOKENS = 1e5;
10506
+ CHARS_PER_TOKEN = 3;
10260
10507
  init_queries();
10508
+ PURPOSE_BATCH_LIMIT = 200;
10509
+ PURPOSE_CONCURRENCY = 10;
10261
10510
  init_queries();
10262
10511
  TERM_BATCH_SIZE = 32;
10263
10512
  SIMILARITY_THRESHOLD = 0.75;
@@ -10369,23 +10618,6 @@ var init_dist = __esm({
10369
10618
  "component"
10370
10619
  ]);
10371
10620
  init_queries();
10372
- execFileAsync2 = promisify2(execFile2);
10373
- MAX_COMMITS = 2e3;
10374
- MAX_FILES_PER_COMMIT = 20;
10375
- RECENT_DAYS = 90;
10376
- init_queries();
10377
- _enabled = true;
10378
- MAX_CHUNKS_PER_REPO = 1e5;
10379
- locks = /* @__PURE__ */ new Map();
10380
- init_queries();
10381
- MAX_API_CALLS = 2e3;
10382
- POOL_SIZE = 32;
10383
- MAX_BATCH_TOKENS = 1e5;
10384
- CHARS_PER_TOKEN = 3;
10385
- init_queries();
10386
- PURPOSE_BATCH_LIMIT = 200;
10387
- PURPOSE_CONCURRENCY = 10;
10388
- init_queries();
10389
10621
  watchers = /* @__PURE__ */ new Map();
10390
10622
  debounceTimers = /* @__PURE__ */ new Map();
10391
10623
  IGNORED = [
@@ -10588,6 +10820,36 @@ var init_dist = __esm({
10588
10820
  CACHE_MAX = 20;
10589
10821
  cache = /* @__PURE__ */ new Map();
10590
10822
  init_queries();
10823
+ RequestTrace = class {
10824
+ steps = [];
10825
+ pending = /* @__PURE__ */ new Map();
10826
+ step(label) {
10827
+ this.pending.set(label, performance.now());
10828
+ }
10829
+ end(label, detail) {
10830
+ const start = this.pending.get(label);
10831
+ if (start === void 0) return;
10832
+ this.pending.delete(label);
10833
+ this.steps.push({
10834
+ step: label,
10835
+ duration_ms: Math.round(performance.now() - start),
10836
+ ...detail ? { detail } : {}
10837
+ });
10838
+ }
10839
+ add(label, duration_ms, detail) {
10840
+ this.steps.push({
10841
+ step: label,
10842
+ duration_ms,
10843
+ ...detail ? { detail } : {}
10844
+ });
10845
+ }
10846
+ toJSON() {
10847
+ return this.steps;
10848
+ }
10849
+ serialize() {
10850
+ return JSON.stringify(this.steps);
10851
+ }
10852
+ };
10591
10853
  }
10592
10854
  });
10593
10855
 
@@ -10655,22 +10917,23 @@ function createCloudCapabilities(apiKey, trackUsage, logRequest) {
10655
10917
  return {
10656
10918
  async embedTexts(texts, isQuery) {
10657
10919
  const start = performance.now();
10920
+ const reqBody = JSON.stringify({
10921
+ input: texts,
10922
+ model: "voyage-code-3",
10923
+ input_type: isQuery ? "query" : "document"
10924
+ });
10658
10925
  const res = await fetch(`${CLOUD_API_URL}/api/proxy/embed`, {
10659
10926
  method: "POST",
10660
10927
  headers,
10661
- body: JSON.stringify({
10662
- input: texts,
10663
- model: "voyage-code-3",
10664
- input_type: isQuery ? "query" : "document"
10665
- })
10928
+ body: reqBody
10666
10929
  });
10930
+ const resText = await res.text();
10667
10931
  const duration3 = Math.round(performance.now() - start);
10668
- logRequest?.("POST", "/api/proxy/embed", res.status, duration3, "cloud");
10932
+ logRequest?.("POST", "/api/proxy/embed", res.status, duration3, "cloud", reqBody, resText);
10669
10933
  if (!res.ok) {
10670
- const err = await res.text().catch(() => "");
10671
- throw new Error(`Cloud embed failed (${res.status}): ${err}`);
10934
+ throw new Error(`Cloud embed failed (${res.status}): ${resText}`);
10672
10935
  }
10673
- const data = await res.json();
10936
+ const data = JSON.parse(resText);
10674
10937
  trackUsage?.("embedding_requests");
10675
10938
  trackUsage?.("embedding_chunks", texts.length);
10676
10939
  return (data.data ?? []).map((d) => d.embedding);
@@ -10684,24 +10947,25 @@ function createCloudCapabilities(apiKey, trackUsage, logRequest) {
10684
10947
  content.slice(0, 2e3)
10685
10948
  ].filter(Boolean).join("\n");
10686
10949
  const start = performance.now();
10950
+ const reqBody = JSON.stringify({
10951
+ messages: [
10952
+ { role: "system", content: "You are a code analyst. Output ONLY a 1-sentence purpose summary for the given file. No preamble." },
10953
+ { role: "user", content: prompt }
10954
+ ],
10955
+ max_tokens: 128
10956
+ });
10687
10957
  const res = await fetch(`${CLOUD_API_URL}/api/proxy/chat`, {
10688
10958
  method: "POST",
10689
10959
  headers,
10690
- body: JSON.stringify({
10691
- messages: [
10692
- { role: "system", content: "You are a code analyst. Output ONLY a 1-sentence purpose summary for the given file. No preamble." },
10693
- { role: "user", content: prompt }
10694
- ],
10695
- max_tokens: 128
10696
- })
10960
+ body: reqBody
10697
10961
  });
10962
+ const resText = await res.text();
10698
10963
  const duration3 = Math.round(performance.now() - start);
10699
- logRequest?.("POST", "/api/proxy/chat", res.status, duration3, "cloud");
10964
+ logRequest?.("POST", "/api/proxy/chat", res.status, duration3, "cloud", reqBody, resText);
10700
10965
  if (!res.ok) {
10701
- const err = await res.text().catch(() => "");
10702
- throw new Error(`Cloud purpose failed (${res.status}): ${err}`);
10966
+ throw new Error(`Cloud purpose failed (${res.status}): ${resText}`);
10703
10967
  }
10704
- const data = await res.json();
10968
+ const data = JSON.parse(resText);
10705
10969
  trackUsage?.("purpose_requests");
10706
10970
  return data.choices?.[0]?.message?.content?.trim() ?? "";
10707
10971
  }
@@ -32148,6 +32412,10 @@ var mcp_exports = {};
32148
32412
  __export(mcp_exports, {
32149
32413
  createMcpServer: () => createMcpServer
32150
32414
  });
32415
+ import { randomUUID as randomUUID3 } from "crypto";
32416
+ function logMcp(method, path, status, duration3, reqBody, resSize, resBody, trace) {
32417
+ getRawDb().prepare(LOG_SQL).run(randomUUID3(), method, path, status, duration3, reqBody ?? null, resSize ?? null, resBody ?? null, trace ?? null);
32418
+ }
32151
32419
  function text2(s) {
32152
32420
  return { content: [{ type: "text", text: s }] };
32153
32421
  }
@@ -32169,11 +32437,22 @@ function createMcpServer(db, caps) {
32169
32437
  inputSchema: { repo_path: external_exports.string(), goal: external_exports.string() }
32170
32438
  },
32171
32439
  async ({ repo_path, goal }) => {
32440
+ const trace = new RequestTrace();
32441
+ const start = performance.now();
32172
32442
  try {
32443
+ trace.step("findRepo");
32173
32444
  const repoId = findOrRegisterRepo(db, repo_path);
32174
- const result = await buildContext(db, repoId, goal, caps);
32445
+ trace.end("findRepo");
32446
+ const repo = repoQueries.getById(db, repoId);
32447
+ const useEmbeddings = repo?.enable_embeddings === 1;
32448
+ const result = await buildContext(db, repoId, goal, caps, trace, { useEmbeddings });
32449
+ const duration3 = Math.round(performance.now() - start);
32450
+ const reqBody = JSON.stringify({ repo_path, goal });
32451
+ logMcp("MCP", "/tool/get_context", 200, duration3, reqBody, result.context_pack.length, result.context_pack, trace.serialize());
32175
32452
  return text2(result.context_pack);
32176
32453
  } catch (e) {
32454
+ const duration3 = Math.round(performance.now() - start);
32455
+ logMcp("MCP", "/tool/get_context", 500, duration3, JSON.stringify({ repo_path, goal }), void 0, e.message, trace.serialize());
32177
32456
  return error2(e.message);
32178
32457
  }
32179
32458
  }
@@ -32185,10 +32464,16 @@ function createMcpServer(db, caps) {
32185
32464
  inputSchema: {}
32186
32465
  },
32187
32466
  async () => {
32467
+ const start = performance.now();
32188
32468
  try {
32189
32469
  const repos2 = listRepos(db);
32190
- return text2(JSON.stringify(repos2, null, 2));
32470
+ const body = JSON.stringify(repos2, null, 2);
32471
+ const duration3 = Math.round(performance.now() - start);
32472
+ logMcp("MCP", "/tool/list_repos", 200, duration3, void 0, body.length, body);
32473
+ return text2(body);
32191
32474
  } catch (e) {
32475
+ const duration3 = Math.round(performance.now() - start);
32476
+ logMcp("MCP", "/tool/list_repos", 500, duration3, void 0, void 0, e.message);
32192
32477
  return error2(e.message);
32193
32478
  }
32194
32479
  }
@@ -32200,12 +32485,21 @@ function createMcpServer(db, caps) {
32200
32485
  inputSchema: { repo_path: external_exports.string() }
32201
32486
  },
32202
32487
  async ({ repo_path }) => {
32488
+ const start = performance.now();
32203
32489
  try {
32204
32490
  const repo = repoQueries.getByPath(db, repo_path);
32205
- if (!repo) return error2("repo not found at " + repo_path);
32491
+ if (!repo) {
32492
+ logMcp("MCP", "/tool/get_status", 404, Math.round(performance.now() - start), JSON.stringify({ repo_path }));
32493
+ return error2("repo not found at " + repo_path);
32494
+ }
32206
32495
  const status = await getRepoStatus(db, repo.id);
32207
- return text2(JSON.stringify(status, null, 2));
32496
+ const body = JSON.stringify(status, null, 2);
32497
+ const duration3 = Math.round(performance.now() - start);
32498
+ logMcp("MCP", "/tool/get_status", 200, duration3, JSON.stringify({ repo_path }), body.length, body);
32499
+ return text2(body);
32208
32500
  } catch (e) {
32501
+ const duration3 = Math.round(performance.now() - start);
32502
+ logMcp("MCP", "/tool/get_status", 500, duration3, JSON.stringify({ repo_path }), void 0, e.message);
32209
32503
  return error2(e.message);
32210
32504
  }
32211
32505
  }
@@ -32217,23 +32511,35 @@ function createMcpServer(db, caps) {
32217
32511
  inputSchema: { repo_path: external_exports.string(), force: external_exports.boolean().optional() }
32218
32512
  },
32219
32513
  async ({ repo_path, force }) => {
32514
+ const trace = new RequestTrace();
32515
+ const start = performance.now();
32220
32516
  try {
32517
+ trace.step("findRepo");
32221
32518
  const repoId = findOrRegisterRepo(db, repo_path);
32222
- const result = await runIndex(db, repoId, caps, force ?? false);
32223
- return text2(JSON.stringify(result, null, 2));
32519
+ trace.end("findRepo");
32520
+ const result = await runIndex(db, repoId, caps, force ?? false, void 0, trace);
32521
+ const body = JSON.stringify(result, null, 2);
32522
+ const duration3 = Math.round(performance.now() - start);
32523
+ logMcp("MCP", "/tool/index_repo", 200, duration3, JSON.stringify({ repo_path, force }), body.length, body, trace.serialize());
32524
+ return text2(body);
32224
32525
  } catch (e) {
32526
+ const duration3 = Math.round(performance.now() - start);
32527
+ logMcp("MCP", "/tool/index_repo", 500, duration3, JSON.stringify({ repo_path, force }), void 0, e.message, trace.serialize());
32225
32528
  return error2(e.message);
32226
32529
  }
32227
32530
  }
32228
32531
  );
32229
32532
  return server;
32230
32533
  }
32534
+ var LOG_SQL;
32231
32535
  var init_mcp2 = __esm({
32232
32536
  "apps/daemon/src/mcp.ts"() {
32233
32537
  "use strict";
32234
32538
  init_dist();
32235
32539
  init_mcp();
32236
32540
  init_zod();
32541
+ LOG_SQL = `INSERT INTO request_logs (id, method, path, status, duration_ms, source, request_body, response_size, response_body, trace, created_at)
32542
+ VALUES (?, ?, ?, ?, ?, 'mcp', ?, ?, ?, ?, datetime('now'))`;
32237
32543
  }
32238
32544
  });
32239
32545
 
@@ -34594,14 +34900,32 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34594
34900
  let lastPrune = 0;
34595
34901
  app.use("*", async (c, next) => {
34596
34902
  const start = performance.now();
34903
+ const path = new URL(c.req.url).pathname;
34904
+ if (path.startsWith("/dashboard/") || path.startsWith("/api/dashboard/") || path === "/health" || path.endsWith("/events")) {
34905
+ return next();
34906
+ }
34907
+ const trace = new RequestTrace();
34908
+ c.set("trace", trace);
34909
+ let reqBody;
34910
+ if (c.req.method !== "GET") {
34911
+ try {
34912
+ reqBody = await c.req.raw.clone().text();
34913
+ } catch {
34914
+ }
34915
+ }
34597
34916
  await next();
34598
34917
  const duration3 = Math.round(performance.now() - start);
34599
- const path = new URL(c.req.url).pathname;
34600
34918
  const source = deriveSource(c.req.raw, path);
34601
- if (path.startsWith("/dashboard/") || path.startsWith("/api/dashboard/")) return;
34602
- if (path === "/health") return;
34919
+ const traceData = trace.toJSON().length > 0 ? trace.serialize() : void 0;
34920
+ let resBody;
34921
+ let resSize;
34922
+ try {
34923
+ resBody = await c.res.clone().text();
34924
+ resSize = resBody.length;
34925
+ } catch {
34926
+ }
34603
34927
  try {
34604
- logQueries.insert(db, c.req.method, path, c.res.status, duration3, source);
34928
+ logQueries.insert(db, c.req.method, path, c.res.status, duration3, source, reqBody, resSize, resBody, traceData);
34605
34929
  const now2 = Date.now();
34606
34930
  if (now2 - lastPrune > 36e5) {
34607
34931
  lastPrune = now2;
@@ -34611,14 +34935,17 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34611
34935
  }
34612
34936
  });
34613
34937
  trackRoute("GET", "/health");
34614
- app.get("/health", (c) => c.json({ status: "ok", version: "0.1.0" }));
34938
+ app.get("/health", (c) => c.json({ status: "ok", version: "0.1.0", cloud_url: CLOUD_API_URL }));
34615
34939
  trackRoute("POST", "/telemetry/track");
34616
34940
  app.post("/telemetry/track", async (c) => {
34617
34941
  try {
34618
34942
  if (!isTelemetryEnabled()) return c.json({ ok: true, skipped: true });
34943
+ const trace = c.get("trace");
34619
34944
  const { event_type, event_data } = await c.req.json();
34620
34945
  if (!event_type || typeof event_type !== "string") return c.json({ error: "event_type required" }, 400);
34946
+ trace.step("track");
34621
34947
  track(db, event_type, event_data);
34948
+ trace.end("track", event_type);
34622
34949
  return c.json({ ok: true });
34623
34950
  } catch {
34624
34951
  return c.json({ ok: true });
@@ -34627,10 +34954,13 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34627
34954
  trackRoute("POST", "/repo/register");
34628
34955
  app.post("/repo/register", async (c) => {
34629
34956
  try {
34957
+ const trace = c.get("trace");
34630
34958
  const { root_path, name, remote_url } = await c.req.json();
34631
34959
  if (!root_path) return c.json({ error: "root_path required" }, 400);
34960
+ trace.step("quotaCheck");
34632
34961
  const currentRepos = listRepos(db).length;
34633
34962
  const maxRepos = quotaCache?.quota?.maxRepos ?? 50;
34963
+ trace.end("quotaCheck", `${currentRepos}/${maxRepos}`);
34634
34964
  if (currentRepos >= maxRepos) {
34635
34965
  return c.json({
34636
34966
  error: "Repo limit reached",
@@ -34639,25 +34969,57 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34639
34969
  plan: quotaCache?.plan ?? "unknown"
34640
34970
  }, 429);
34641
34971
  }
34972
+ trace.step("registerRepo");
34642
34973
  const result = registerRepo(db, root_path, name, remote_url);
34974
+ trace.end("registerRepo", result.created ? "created" : "existing");
34643
34975
  if (result.created) {
34644
- runIndex(db, result.repo_id, caps, false, emitRepoEvent).then((r) => {
34976
+ runIndex(db, result.repo_id, caps, false, emitRepoEvent).then(async (r) => {
34977
+ const bg = new RequestTrace();
34978
+ const bgStart = performance.now();
34979
+ const results = { index: r };
34980
+ bg.add("index", r.duration_ms, `${r.files_scanned} files, +${r.chunks_created} chunks`);
34645
34981
  if (r.files_scanned > 0) usageQueries.increment(db, "repos_indexed");
34982
+ const repo = repoQueries.getById(db, result.repo_id);
34983
+ const reqBody = JSON.stringify({ repo_id: result.repo_id, repo_name: repo?.name });
34646
34984
  const tasks = [];
34647
- if (quotaRemaining("embeddingChunks") > 0) {
34648
- tasks.push(ensureEmbedded(db, result.repo_id, caps));
34985
+ if (repo?.enable_vocab_clusters && quotaRemaining("embeddingChunks") > 0) {
34986
+ bg.step("vocabClusters");
34987
+ tasks.push(buildVocabClusters(db, result.repo_id, caps).then(() => {
34988
+ bg.end("vocabClusters");
34989
+ results.vocabClusters = "done";
34990
+ }));
34649
34991
  } else {
34650
- console.log("[LENS] Skipping embeddings: quota exceeded");
34992
+ bg.add("vocabClusters", 0, !repo?.enable_vocab_clusters ? "disabled" : "quota exceeded");
34651
34993
  }
34652
- if (quotaRemaining("purposeRequests") > 0) {
34653
- tasks.push(enrichPurpose(db, result.repo_id, caps));
34994
+ if (repo?.enable_embeddings && quotaRemaining("embeddingChunks") > 0) {
34995
+ bg.step("embeddings");
34996
+ tasks.push(ensureEmbedded(db, result.repo_id, caps).then((er) => {
34997
+ bg.end("embeddings", `${er.embedded_count} embedded`);
34998
+ results.embeddings = er;
34999
+ }));
34654
35000
  } else {
34655
- console.log("[LENS] Skipping purpose enrichment: quota exceeded");
35001
+ bg.add("embeddings", 0, !repo?.enable_embeddings ? "disabled" : "quota exceeded");
34656
35002
  }
34657
- return Promise.all(tasks);
34658
- }).catch((e) => console.error("[LENS] post-register index failed:", e));
35003
+ if (repo?.enable_summaries && quotaRemaining("purposeRequests") > 0) {
35004
+ bg.step("purpose");
35005
+ tasks.push(enrichPurpose(db, result.repo_id, caps).then((pr) => {
35006
+ bg.end("purpose", `${pr.enriched} enriched`);
35007
+ results.purpose = pr;
35008
+ }));
35009
+ } else {
35010
+ bg.add("purpose", 0, !repo?.enable_summaries ? "disabled" : "quota exceeded");
35011
+ }
35012
+ await Promise.all(tasks);
35013
+ const resBody = JSON.stringify(results);
35014
+ const bgDuration = Math.round(performance.now() - bgStart);
35015
+ logQueries.insert(db, "BG", `/enrichment/${result.repo_id}`, 200, bgDuration, "system", reqBody, resBody.length, resBody, bg.serialize());
35016
+ }).catch((e) => {
35017
+ logQueries.insert(db, "BG", `/enrichment/${result.repo_id}`, 500, 0, "system", JSON.stringify({ repo_id: result.repo_id }), void 0, String(e));
35018
+ });
34659
35019
  }
35020
+ trace.step("startWatcher");
34660
35021
  startWatcher(db, result.repo_id, root_path);
35022
+ trace.end("startWatcher");
34661
35023
  emitRepoEvent();
34662
35024
  return c.json(result);
34663
35025
  } catch (e) {
@@ -34667,7 +35029,11 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34667
35029
  trackRoute("GET", "/repo/list");
34668
35030
  app.get("/repo/list", (c) => {
34669
35031
  try {
34670
- return c.json({ repos: listRepos(db) });
35032
+ const trace = c.get("trace");
35033
+ trace.step("listRepos");
35034
+ const repos2 = listRepos(db);
35035
+ trace.end("listRepos", `${repos2.length} repos`);
35036
+ return c.json({ repos: repos2 });
34671
35037
  } catch (e) {
34672
35038
  return c.json({ error: e.message }, 500);
34673
35039
  }
@@ -34675,7 +35041,11 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34675
35041
  trackRoute("GET", "/repo/list/detailed");
34676
35042
  app.get("/repo/list/detailed", (c) => {
34677
35043
  try {
35044
+ const trace = c.get("trace");
35045
+ trace.step("listRepos");
34678
35046
  const repos2 = listRepos(db);
35047
+ trace.end("listRepos", `${repos2.length} repos`);
35048
+ trace.step("loadStats");
34679
35049
  const detailed = repos2.map((r) => {
34680
35050
  const stats = chunkQueries.getStats(db, r.id);
34681
35051
  return {
@@ -34689,6 +35059,7 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34689
35059
  last_indexed_at: r.last_indexed_at
34690
35060
  };
34691
35061
  });
35062
+ trace.end("loadStats");
34692
35063
  return c.json({ repos: detailed });
34693
35064
  } catch (e) {
34694
35065
  return c.json({ error: e.message }, 500);
@@ -34699,7 +35070,10 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34699
35070
  trackRoute("GET", "/repo/:id");
34700
35071
  app.get("/repo/:id", (c) => {
34701
35072
  try {
35073
+ const trace = c.get("trace");
35074
+ trace.step("getRepo");
34702
35075
  const repo = getRepo(db, c.req.param("id"));
35076
+ trace.end("getRepo");
34703
35077
  return c.json(repo);
34704
35078
  } catch (e) {
34705
35079
  if (e.message === "repo not found") return c.json({ error: "repo not found" }, 404);
@@ -34709,10 +35083,15 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34709
35083
  trackRoute("DELETE", "/repo/:id");
34710
35084
  app.delete("/repo/:id", async (c) => {
34711
35085
  try {
35086
+ const trace = c.get("trace");
34712
35087
  const id = c.req.param("id");
35088
+ trace.step("stopWatcher");
34713
35089
  await stopWatcher(id).catch(() => {
34714
35090
  });
35091
+ trace.end("stopWatcher");
35092
+ trace.step("removeRepo");
34715
35093
  const result = removeRepo(db, id);
35094
+ trace.end("removeRepo");
34716
35095
  emitRepoEvent();
34717
35096
  return c.json(result);
34718
35097
  } catch (e) {
@@ -34723,7 +35102,10 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34723
35102
  trackRoute("GET", "/repo/:id/status");
34724
35103
  app.get("/repo/:id/status", async (c) => {
34725
35104
  try {
35105
+ const trace = c.get("trace");
35106
+ trace.step("getRepoStatus");
34726
35107
  const status = await getRepoStatus(db, c.req.param("id"));
35108
+ trace.end("getRepoStatus");
34727
35109
  return c.json({
34728
35110
  ...status,
34729
35111
  has_capabilities: !!caps,
@@ -34740,7 +35122,9 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34740
35122
  try {
34741
35123
  const { repo_id, goal } = await c.req.json();
34742
35124
  if (!repo_id || !goal) return c.json({ error: "repo_id and goal required" }, 400);
34743
- const result = await buildContext(db, repo_id, goal, caps);
35125
+ const repo = repoQueries.getById(db, repo_id);
35126
+ const useEmbeddings = repo?.enable_embeddings === 1;
35127
+ const result = await buildContext(db, repo_id, goal, caps, c.get("trace"), { useEmbeddings });
34744
35128
  usageQueries.increment(db, "context_queries");
34745
35129
  return c.json(result);
34746
35130
  } catch (e) {
@@ -34752,20 +35136,48 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34752
35136
  try {
34753
35137
  const { repo_id, force } = await c.req.json();
34754
35138
  if (!repo_id) return c.json({ error: "repo_id required" }, 400);
34755
- const result = await runIndex(db, repo_id, caps, force ?? false, emitRepoEvent);
35139
+ const result = await runIndex(db, repo_id, caps, force ?? false, emitRepoEvent, c.get("trace"));
34756
35140
  if (result.files_scanned > 0) usageQueries.increment(db, "repos_indexed");
35141
+ const repo = repoQueries.getById(db, repo_id);
35142
+ const bg = new RequestTrace();
35143
+ const bgStart = performance.now();
35144
+ const results = { index: result };
35145
+ const reqBody = JSON.stringify({ repo_id, repo_name: repo?.name, force });
34757
35146
  const tasks = [];
34758
- if (quotaRemaining("embeddingChunks") > 0) {
34759
- tasks.push(ensureEmbedded(db, repo_id, caps));
35147
+ if (repo?.enable_vocab_clusters && quotaRemaining("embeddingChunks") > 0) {
35148
+ bg.step("vocabClusters");
35149
+ tasks.push(buildVocabClusters(db, repo_id, caps).then(() => {
35150
+ bg.end("vocabClusters");
35151
+ results.vocabClusters = "done";
35152
+ }));
34760
35153
  } else {
34761
- console.log("[LENS] Skipping embeddings: quota exceeded");
35154
+ bg.add("vocabClusters", 0, !repo?.enable_vocab_clusters ? "disabled" : "quota exceeded");
34762
35155
  }
34763
- if (quotaRemaining("purposeRequests") > 0) {
34764
- tasks.push(enrichPurpose(db, repo_id, caps));
35156
+ if (repo?.enable_embeddings && quotaRemaining("embeddingChunks") > 0) {
35157
+ bg.step("embeddings");
35158
+ tasks.push(ensureEmbedded(db, repo_id, caps).then((er) => {
35159
+ bg.end("embeddings", `${er.embedded_count} embedded`);
35160
+ results.embeddings = er;
35161
+ }));
34765
35162
  } else {
34766
- console.log("[LENS] Skipping purpose enrichment: quota exceeded");
35163
+ bg.add("embeddings", 0, !repo?.enable_embeddings ? "disabled" : "quota exceeded");
34767
35164
  }
34768
- Promise.all(tasks).catch((e) => console.error("[LENS] post-index embed/enrich failed:", e));
35165
+ if (repo?.enable_summaries && quotaRemaining("purposeRequests") > 0) {
35166
+ bg.step("purpose");
35167
+ tasks.push(enrichPurpose(db, repo_id, caps).then((pr) => {
35168
+ bg.end("purpose", `${pr.enriched} enriched`);
35169
+ results.purpose = pr;
35170
+ }));
35171
+ } else {
35172
+ bg.add("purpose", 0, !repo?.enable_summaries ? "disabled" : "quota exceeded");
35173
+ }
35174
+ Promise.all(tasks).then(() => {
35175
+ const resBody = JSON.stringify(results);
35176
+ const bgDuration = Math.round(performance.now() - bgStart);
35177
+ logQueries.insert(db, "BG", `/enrichment/${repo_id}`, 200, bgDuration, "system", reqBody, resBody.length, resBody, bg.serialize());
35178
+ }).catch((e) => {
35179
+ logQueries.insert(db, "BG", `/enrichment/${repo_id}`, 500, 0, "system", reqBody, void 0, String(e));
35180
+ });
34769
35181
  return c.json(result);
34770
35182
  } catch (e) {
34771
35183
  return c.json({ error: e.message }, 500);
@@ -34774,16 +35186,23 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34774
35186
  trackRoute("GET", "/index/status/:repo_id");
34775
35187
  app.get("/index/status/:repo_id", async (c) => {
34776
35188
  try {
35189
+ const trace = c.get("trace");
34777
35190
  const repoId = c.req.param("repo_id");
35191
+ trace.step("getRepo");
34778
35192
  const repo = repoQueries.getById(db, repoId);
35193
+ trace.end("getRepo");
34779
35194
  if (!repo) return c.json({ error: "repo not found" }, 404);
35195
+ trace.step("getHeadCommit");
34780
35196
  let currentHead = null;
34781
35197
  try {
34782
35198
  currentHead = await getHeadCommit(repo.root_path);
34783
35199
  } catch {
34784
35200
  }
35201
+ trace.end("getHeadCommit");
34785
35202
  const isStale = !!(currentHead && repo.last_indexed_commit !== currentHead);
35203
+ trace.step("getStats");
34786
35204
  const stats = chunkQueries.getStats(db, repoId);
35205
+ trace.end("getStats");
34787
35206
  return c.json({
34788
35207
  index_status: repo.index_status,
34789
35208
  last_indexed_commit: repo.last_indexed_commit,
@@ -34802,10 +35221,15 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34802
35221
  trackRoute("POST", "/index/watch");
34803
35222
  app.post("/index/watch", async (c) => {
34804
35223
  try {
35224
+ const trace = c.get("trace");
34805
35225
  const { repo_id } = await c.req.json();
34806
35226
  if (!repo_id) return c.json({ error: "repo_id required" }, 400);
35227
+ trace.step("getRepo");
34807
35228
  const repo = getRepo(db, repo_id);
35229
+ trace.end("getRepo");
35230
+ trace.step("startWatcher");
34808
35231
  const result = startWatcher(db, repo_id, repo.root_path);
35232
+ trace.end("startWatcher");
34809
35233
  emitRepoEvent();
34810
35234
  return c.json(result);
34811
35235
  } catch (e) {
@@ -34816,9 +35240,12 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34816
35240
  trackRoute("POST", "/index/unwatch");
34817
35241
  app.post("/index/unwatch", async (c) => {
34818
35242
  try {
35243
+ const trace = c.get("trace");
34819
35244
  const { repo_id } = await c.req.json();
34820
35245
  if (!repo_id) return c.json({ error: "repo_id required" }, 400);
35246
+ trace.step("stopWatcher");
34821
35247
  const result = await stopWatcher(repo_id);
35248
+ trace.end("stopWatcher");
34822
35249
  emitRepoEvent();
34823
35250
  return c.json(result);
34824
35251
  } catch (e) {
@@ -34828,7 +35255,11 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34828
35255
  trackRoute("GET", "/index/watch-status/:repo_id");
34829
35256
  app.get("/index/watch-status/:repo_id", (c) => {
34830
35257
  try {
34831
- return c.json(getWatcherStatus(c.req.param("repo_id")));
35258
+ const trace = c.get("trace");
35259
+ trace.step("getWatcherStatus");
35260
+ const status = getWatcherStatus(c.req.param("repo_id"));
35261
+ trace.end("getWatcherStatus");
35262
+ return c.json(status);
34832
35263
  } catch (e) {
34833
35264
  return c.json({ error: e.message }, 500);
34834
35265
  }
@@ -34836,14 +35267,19 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34836
35267
  trackRoute("GET", "/daemon/stats");
34837
35268
  app.get("/daemon/stats", (c) => {
34838
35269
  try {
35270
+ const trace = c.get("trace");
35271
+ trace.step("listRepos");
34839
35272
  const repos2 = listRepos(db);
35273
+ trace.end("listRepos", `${repos2.length} repos`);
34840
35274
  let totalChunks = 0;
34841
35275
  let totalEmbeddings = 0;
35276
+ trace.step("aggregateStats");
34842
35277
  for (const r of repos2) {
34843
35278
  const s = chunkQueries.getStats(db, r.id);
34844
35279
  totalChunks += s.chunk_count;
34845
35280
  totalEmbeddings += s.embedded_count;
34846
35281
  }
35282
+ trace.end("aggregateStats");
34847
35283
  return c.json({
34848
35284
  repos_count: repos2.length,
34849
35285
  total_chunks: totalChunks,
@@ -34901,8 +35337,7 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34901
35337
  start(ctrl) {
34902
35338
  repoClients.add(ctrl);
34903
35339
  },
34904
- cancel(ctrl) {
34905
- repoClients.delete(ctrl);
35340
+ cancel() {
34906
35341
  }
34907
35342
  });
34908
35343
  return new Response(stream, {
@@ -34915,18 +35350,19 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34915
35350
  });
34916
35351
  const authClients = /* @__PURE__ */ new Set();
34917
35352
  const authDir = join5(homedir3(), ".lens");
35353
+ function emitAuthEvent() {
35354
+ for (const ctrl of authClients) {
35355
+ try {
35356
+ ctrl.enqueue(encoder.encode("data: auth-changed\n\n"));
35357
+ } catch {
35358
+ authClients.delete(ctrl);
35359
+ }
35360
+ }
35361
+ }
34918
35362
  try {
34919
35363
  watch2(authDir, (_, filename) => {
34920
- if (filename !== "auth.json") return;
34921
- for (const ctrl of authClients) {
34922
- try {
34923
- ctrl.enqueue(new TextEncoder().encode("data: auth-changed\n\n"));
34924
- } catch {
34925
- authClients.delete(ctrl);
34926
- }
34927
- }
34928
- refreshQuotaCache().catch(() => {
34929
- });
35364
+ if (filename && filename !== "auth.json") return;
35365
+ refreshQuotaCache().then(() => emitAuthEvent()).catch(() => emitAuthEvent());
34930
35366
  });
34931
35367
  } catch {
34932
35368
  }
@@ -34936,8 +35372,7 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34936
35372
  start(ctrl) {
34937
35373
  authClients.add(ctrl);
34938
35374
  },
34939
- cancel(ctrl) {
34940
- authClients.delete(ctrl);
35375
+ cancel() {
34941
35376
  }
34942
35377
  });
34943
35378
  return new Response(stream, {
@@ -34948,6 +35383,12 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34948
35383
  }
34949
35384
  });
34950
35385
  });
35386
+ trackRoute("POST", "/api/auth/notify");
35387
+ app.post("/api/auth/notify", async (c) => {
35388
+ await refreshQuotaCache();
35389
+ emitAuthEvent();
35390
+ return c.json({ ok: true });
35391
+ });
34951
35392
  trackRoute("GET", "/api/auth/status");
34952
35393
  app.get("/api/auth/status", async (c) => {
34953
35394
  try {
@@ -35022,15 +35463,22 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35022
35463
  }
35023
35464
  trackRoute("GET", "/api/cloud/usage");
35024
35465
  app.get("/api/cloud/usage", async (c) => {
35466
+ const trace = c.get("trace");
35025
35467
  const start = c.req.query("start");
35026
35468
  const end = c.req.query("end");
35027
- return cloudProxy("GET", `/api/usage?start=${start}&end=${end}`);
35469
+ trace.step("cloudProxy");
35470
+ const res = await cloudProxy("GET", `/api/usage?start=${start}&end=${end}`);
35471
+ trace.end("cloudProxy", `${res.status}`);
35472
+ return res;
35028
35473
  });
35029
35474
  trackRoute("GET", "/api/cloud/usage/current");
35030
- app.get(
35031
- "/api/cloud/usage/current",
35032
- async () => cloudProxy("GET", "/api/usage/current")
35033
- );
35475
+ app.get("/api/cloud/usage/current", async (c) => {
35476
+ const trace = c.get("trace");
35477
+ trace.step("cloudProxy");
35478
+ const res = await cloudProxy("GET", "/api/usage/current");
35479
+ trace.end("cloudProxy", `${res.status}`);
35480
+ return res;
35481
+ });
35034
35482
  trackRoute("GET", "/api/cloud/subscription");
35035
35483
  app.get("/api/cloud/subscription", (c) => {
35036
35484
  const sub = quotaCache?.subscription ?? { plan: "free", status: "active" };
@@ -35038,14 +35486,22 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35038
35486
  });
35039
35487
  trackRoute("POST", "/api/cloud/billing/checkout");
35040
35488
  app.post("/api/cloud/billing/checkout", async (c) => {
35489
+ const trace = c.get("trace");
35041
35490
  const body = await c.req.json().catch(() => ({}));
35042
- return cloudProxy("POST", "/api/billing/checkout", body);
35491
+ trace.step("cloudProxy");
35492
+ const res = await cloudProxy("POST", "/api/billing/checkout", body);
35493
+ trace.end("cloudProxy", `${res.status}`);
35494
+ return res;
35043
35495
  });
35044
35496
  trackRoute("GET", "/api/cloud/billing/portal");
35045
35497
  app.get("/api/cloud/billing/portal", async (c) => {
35498
+ const trace = c.get("trace");
35046
35499
  const returnUrl = c.req.query("return_url") || "";
35047
35500
  const qs = returnUrl ? `?return_url=${encodeURIComponent(returnUrl)}` : "";
35048
- return cloudProxy("GET", `/api/billing/portal${qs}`);
35501
+ trace.step("cloudProxy");
35502
+ const res = await cloudProxy("GET", `/api/billing/portal${qs}`);
35503
+ trace.end("cloudProxy", `${res.status}`);
35504
+ return res;
35049
35505
  });
35050
35506
  trackRoute("GET", "/api/dashboard/stats");
35051
35507
  app.get("/api/dashboard/stats", (c) => {
@@ -35100,6 +35556,9 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35100
35556
  last_indexed_at: r.last_indexed_at,
35101
35557
  last_indexed_commit: r.last_indexed_commit,
35102
35558
  max_import_depth: r.max_import_depth,
35559
+ enable_embeddings: r.enable_embeddings,
35560
+ enable_summaries: r.enable_summaries,
35561
+ enable_vocab_clusters: r.enable_vocab_clusters,
35103
35562
  has_capabilities: !!caps,
35104
35563
  watcher: {
35105
35564
  active: watcher.watching,
@@ -35146,6 +35605,150 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35146
35605
  return c.json({ error: e.message }, 500);
35147
35606
  }
35148
35607
  });
35608
+ trackRoute("PATCH", "/api/dashboard/repos/:id/settings");
35609
+ app.patch("/api/dashboard/repos/:id/settings", async (c) => {
35610
+ try {
35611
+ const id = c.req.param("id");
35612
+ const repo = repoQueries.getById(db, id);
35613
+ if (!repo) return c.json({ error: "repo not found" }, 404);
35614
+ const body = await c.req.json();
35615
+ const flags = {};
35616
+ if (body.enable_embeddings !== void 0) flags.enable_embeddings = body.enable_embeddings ? 1 : 0;
35617
+ if (body.enable_summaries !== void 0) flags.enable_summaries = body.enable_summaries ? 1 : 0;
35618
+ if (body.enable_vocab_clusters !== void 0) flags.enable_vocab_clusters = body.enable_vocab_clusters ? 1 : 0;
35619
+ repoQueries.updateProFeatures(db, id, flags);
35620
+ emitRepoEvent();
35621
+ return c.json({ ok: true });
35622
+ } catch (e) {
35623
+ return c.json({ error: e.message }, 500);
35624
+ }
35625
+ });
35626
+ trackRoute("GET", "/api/dashboard/repos/:id/files");
35627
+ app.get("/api/dashboard/repos/:id/files", (c) => {
35628
+ try {
35629
+ const id = c.req.param("id");
35630
+ const repo = repoQueries.getById(db, id);
35631
+ if (!repo) return c.json({ error: "repo not found" }, 404);
35632
+ const limit = Number(c.req.query("limit") || 100);
35633
+ const offset = Number(c.req.query("offset") || 0);
35634
+ const search = c.req.query("search") || void 0;
35635
+ const meta = metadataQueries.getByRepo(db, id);
35636
+ const raw2 = getRawDb();
35637
+ const chunkCounts = raw2.prepare("SELECT path, count(*) as chunk_count, SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) as embedded_count FROM chunks WHERE repo_id = ? GROUP BY path").all(id);
35638
+ const countMap = new Map(chunkCounts.map((r) => [r.path, r]));
35639
+ let all = meta.map((m) => {
35640
+ const counts = countMap.get(m.path);
35641
+ return {
35642
+ path: m.path,
35643
+ language: m.language,
35644
+ exports: m.exports,
35645
+ purpose: m.purpose,
35646
+ chunk_count: counts?.chunk_count ?? 0,
35647
+ has_embedding: (counts?.embedded_count ?? 0) > 0
35648
+ };
35649
+ });
35650
+ if (search) {
35651
+ const q = search.toLowerCase();
35652
+ all = all.filter((f) => f.path.toLowerCase().includes(q));
35653
+ }
35654
+ const total = all.length;
35655
+ const files = all.slice(offset, offset + limit);
35656
+ return c.json({ files, total });
35657
+ } catch (e) {
35658
+ return c.json({ error: e.message }, 500);
35659
+ }
35660
+ });
35661
+ trackRoute("GET", "/api/dashboard/repos/:id/files/:path");
35662
+ app.get("/api/dashboard/repos/:id/files/:path", (c) => {
35663
+ try {
35664
+ const id = c.req.param("id");
35665
+ const filePath = decodeURIComponent(c.req.param("path"));
35666
+ const repo = repoQueries.getById(db, id);
35667
+ if (!repo) return c.json({ error: "repo not found" }, 404);
35668
+ const allMeta = metadataQueries.getByRepo(db, id);
35669
+ const meta = allMeta.find((m) => m.path === filePath);
35670
+ if (!meta) return c.json({ error: "file not found" }, 404);
35671
+ const raw2 = getRawDb();
35672
+ const chunkRow = raw2.prepare("SELECT count(*) as chunk_count, SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) as embedded_count FROM chunks WHERE repo_id = ? AND path = ?").get(id, filePath);
35673
+ const allImports = importQueries.getBySources(db, id, [filePath]);
35674
+ const allImporters = importQueries.getByTargets(db, id, [filePath]);
35675
+ const gitStats = statsQueries.getByRepo(db, id);
35676
+ const fileStat = gitStats.get(filePath);
35677
+ const cochangeRows = raw2.prepare("SELECT path_a, path_b, cochange_count FROM file_cochanges WHERE repo_id = ? AND (path_a = ? OR path_b = ?) ORDER BY cochange_count DESC LIMIT 10").all(id, filePath, filePath);
35678
+ const cochanges = cochangeRows.map((r) => ({
35679
+ path: r.path_a === filePath ? r.path_b : r.path_a,
35680
+ count: r.cochange_count
35681
+ }));
35682
+ return c.json({
35683
+ path: meta.path,
35684
+ language: meta.language,
35685
+ exports: meta.exports,
35686
+ docstring: meta.docstring,
35687
+ sections: meta.sections,
35688
+ internals: meta.internals,
35689
+ purpose: meta.purpose,
35690
+ chunk_count: chunkRow.chunk_count,
35691
+ embedded_count: chunkRow.embedded_count,
35692
+ imports: allImports.map((i) => i.target_path),
35693
+ imported_by: allImporters.map((i) => i.source_path),
35694
+ git: fileStat ? {
35695
+ commit_count: fileStat.commit_count,
35696
+ recent_count: fileStat.recent_count,
35697
+ last_modified: fileStat.last_modified?.toISOString() ?? null
35698
+ } : null,
35699
+ cochanges
35700
+ });
35701
+ } catch (e) {
35702
+ return c.json({ error: e.message }, 500);
35703
+ }
35704
+ });
35705
+ trackRoute("GET", "/api/dashboard/repos/:id/chunks");
35706
+ app.get("/api/dashboard/repos/:id/chunks", (c) => {
35707
+ try {
35708
+ const id = c.req.param("id");
35709
+ const repo = repoQueries.getById(db, id);
35710
+ if (!repo) return c.json({ error: "repo not found" }, 404);
35711
+ const limit = Number(c.req.query("limit") || 100);
35712
+ const offset = Number(c.req.query("offset") || 0);
35713
+ const pathFilter = c.req.query("path") || void 0;
35714
+ const raw2 = getRawDb();
35715
+ const where = pathFilter ? "WHERE repo_id = ? AND path = ?" : "WHERE repo_id = ?";
35716
+ const params = pathFilter ? [id, pathFilter] : [id];
35717
+ const rows = raw2.prepare(`SELECT id, path, chunk_index, start_line, end_line, language, CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END as has_embedding FROM chunks ${where} ORDER BY path, chunk_index LIMIT ? OFFSET ?`).all(...params, limit, offset);
35718
+ const totalRow = raw2.prepare(`SELECT count(*) as count FROM chunks ${where}`).get(...params);
35719
+ return c.json({
35720
+ chunks: rows.map((r) => ({ ...r, has_embedding: r.has_embedding === 1 })),
35721
+ total: totalRow.count
35722
+ });
35723
+ } catch (e) {
35724
+ return c.json({ error: e.message }, 500);
35725
+ }
35726
+ });
35727
+ trackRoute("GET", "/api/dashboard/repos/:id/chunks/:chunkId");
35728
+ app.get("/api/dashboard/repos/:id/chunks/:chunkId", (c) => {
35729
+ try {
35730
+ const id = c.req.param("id");
35731
+ const chunkId = c.req.param("chunkId");
35732
+ const raw2 = getRawDb();
35733
+ const row = raw2.prepare("SELECT id, path, chunk_index, start_line, end_line, content, language, chunk_hash, CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END as has_embedding FROM chunks WHERE id = ? AND repo_id = ?").get(chunkId, id);
35734
+ if (!row) return c.json({ error: "chunk not found" }, 404);
35735
+ return c.json({ ...row, has_embedding: row.has_embedding === 1 });
35736
+ } catch (e) {
35737
+ return c.json({ error: e.message }, 500);
35738
+ }
35739
+ });
35740
+ trackRoute("GET", "/api/dashboard/repos/:id/vocab-clusters");
35741
+ app.get("/api/dashboard/repos/:id/vocab-clusters", (c) => {
35742
+ try {
35743
+ const id = c.req.param("id");
35744
+ const repo = repoQueries.getById(db, id);
35745
+ if (!repo) return c.json({ error: "repo not found" }, 404);
35746
+ const clusters = repo.vocab_clusters ? JSON.parse(repo.vocab_clusters) : [];
35747
+ return c.json({ clusters: Array.isArray(clusters) ? clusters : [] });
35748
+ } catch (e) {
35749
+ return c.json({ error: e.message }, 500);
35750
+ }
35751
+ });
35149
35752
  trackRoute("GET", "/api/dashboard/logs");
35150
35753
  app.get("/api/dashboard/logs", (c) => {
35151
35754
  try {
@@ -35204,46 +35807,6 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35204
35807
  return c.json({ error: e.message }, 500);
35205
35808
  }
35206
35809
  });
35207
- trackRoute("GET", "/api/dashboard/jobs");
35208
- app.get("/api/dashboard/jobs", async (c) => {
35209
- try {
35210
- const repos2 = listRepos(db);
35211
- const result = await Promise.all(
35212
- repos2.map(async (r) => {
35213
- const stats = chunkQueries.getStats(db, r.id);
35214
- const structural = metadataQueries.getStructuralStats(db, r.id);
35215
- const watcher = getWatcherStatus(r.id);
35216
- let currentHead = null;
35217
- try {
35218
- currentHead = await getHeadCommit(r.root_path);
35219
- } catch {
35220
- }
35221
- return {
35222
- id: r.id,
35223
- name: r.name,
35224
- index_status: r.index_status,
35225
- last_indexed_commit: r.last_indexed_commit,
35226
- last_indexed_at: r.last_indexed_at,
35227
- is_stale: !!(currentHead && r.last_indexed_commit !== currentHead),
35228
- current_head: currentHead,
35229
- chunk_count: stats.chunk_count,
35230
- embedded_count: stats.embedded_count,
35231
- embeddable_count: stats.embeddable_count,
35232
- purpose_count: structural.purpose_count,
35233
- purpose_total: structural.purpose_total,
35234
- watcher: {
35235
- active: watcher.watching,
35236
- changed_files: watcher.changed_files ?? 0,
35237
- started_at: watcher.started_at ?? null
35238
- }
35239
- };
35240
- })
35241
- );
35242
- return c.json({ repos: result });
35243
- } catch (e) {
35244
- return c.json({ error: e.message }, 500);
35245
- }
35246
- });
35247
35810
  trackRoute("GET", "/api/dashboard/usage");
35248
35811
  app.get("/api/dashboard/usage", (c) => {
35249
35812
  try {
@@ -35299,12 +35862,19 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35299
35862
  if (existsSync2(fullPath) && statSync(fullPath).isFile()) {
35300
35863
  const content = readFileSync2(fullPath);
35301
35864
  const mime = MIME_TYPES[extname3(fullPath)] ?? "application/octet-stream";
35302
- return new Response(content, { headers: { "Content-Type": mime, "Cache-Control": "public, max-age=3600" } });
35865
+ const isHtml = mime === "text/html";
35866
+ return new Response(content, {
35867
+ headers: {
35868
+ "Content-Type": mime,
35869
+ "Cache-Control": isHtml ? "no-cache" : "public, max-age=86400",
35870
+ "Access-Control-Allow-Origin": "*"
35871
+ }
35872
+ });
35303
35873
  }
35304
35874
  const indexPath = join5(dashboardDist, "index.html");
35305
35875
  if (existsSync2(indexPath)) {
35306
35876
  return new Response(readFileSync2(indexPath), {
35307
- headers: { "Content-Type": "text/html" }
35877
+ headers: { "Content-Type": "text/html", "Cache-Control": "no-cache", "Access-Control-Allow-Origin": "*" }
35308
35878
  });
35309
35879
  }
35310
35880
  return c.json({ error: "not found" }, 404);
@@ -35319,19 +35889,20 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35319
35889
  ]);
35320
35890
  if (res.ok) {
35321
35891
  const data = await res.json();
35892
+ const plan = (data.plan ?? "free").trim();
35322
35893
  const subData = subRes.ok ? (await subRes.json()).subscription ?? null : null;
35323
35894
  quotaCache = {
35324
- plan: data.plan ?? "free",
35895
+ plan,
35325
35896
  usage: data.usage ?? {},
35326
35897
  quota: data.quota ?? {},
35327
35898
  subscription: subData,
35328
35899
  fetchedAt: Date.now()
35329
35900
  };
35330
- if (caps && data.plan !== "pro") {
35901
+ if (caps && plan !== "pro") {
35331
35902
  caps = void 0;
35332
35903
  console.error("[LENS] Cloud capabilities disabled (plan changed to free)");
35333
35904
  }
35334
- if (!caps && data.plan === "pro") {
35905
+ if (!caps && plan === "pro") {
35335
35906
  const apiKey = await readApiKey();
35336
35907
  if (apiKey) {
35337
35908
  const { createCloudCapabilities: createCloudCapabilities2 } = await Promise.resolve().then(() => (init_cloud_capabilities(), cloud_capabilities_exports));
@@ -35343,9 +35914,9 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35343
35914
  } catch {
35344
35915
  }
35345
35916
  },
35346
- (method, path, status, duration3, source) => {
35917
+ (method, path, status, duration3, source, reqBody, resBody) => {
35347
35918
  try {
35348
- logQueries.insert(db, method, path, status, duration3, source);
35919
+ logQueries.insert(db, method, path, status, duration3, source, reqBody, resBody?.length, resBody);
35349
35920
  } catch {
35350
35921
  }
35351
35922
  }
@@ -36082,9 +36653,9 @@ async function loadCapabilities(db) {
36082
36653
  } catch {
36083
36654
  }
36084
36655
  },
36085
- (method, path, status, duration3, source) => {
36656
+ (method, path, status, duration3, source, reqBody, resBody) => {
36086
36657
  try {
36087
- logQueries2.insert(db, method, path, status, duration3, source);
36658
+ logQueries2.insert(db, method, path, status, duration3, source, reqBody, resBody?.length, resBody);
36088
36659
  } catch {
36089
36660
  }
36090
36661
  }