lens-engine 0.1.19 → 0.1.21

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
@@ -6627,9 +6627,9 @@ var init_handler = __esm({
6627
6627
  if (this.fsw.closed) {
6628
6628
  return;
6629
6629
  }
6630
- const dirname5 = sysPath.dirname(file);
6630
+ const dirname4 = sysPath.dirname(file);
6631
6631
  const basename4 = sysPath.basename(file);
6632
- const parent = this.fsw._getWatchedDir(dirname5);
6632
+ const parent = this.fsw._getWatchedDir(dirname4);
6633
6633
  let prevStats = stats;
6634
6634
  if (parent.has(basename4))
6635
6635
  return;
@@ -6656,7 +6656,7 @@ var init_handler = __esm({
6656
6656
  prevStats = newStats2;
6657
6657
  }
6658
6658
  } catch (error3) {
6659
- this.fsw._remove(dirname5, basename4);
6659
+ this.fsw._remove(dirname4, basename4);
6660
6660
  }
6661
6661
  } else if (parent.has(basename4)) {
6662
6662
  const at = newStats.atimeMs;
@@ -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
+ GOLD_DATASET: () => GOLD_DATASET,
7608
7609
  RequestTrace: () => RequestTrace,
7609
7610
  agglomerativeCluster: () => agglomerativeCluster,
7610
7611
  analyzeGitHistory: () => analyzeGitHistory,
@@ -7616,10 +7617,12 @@ __export(dist_exports, {
7616
7617
  closeDb: () => closeDb,
7617
7618
  cochangeQueries: () => cochangeQueries,
7618
7619
  computeMaxImportDepth: () => computeMaxImportDepth,
7619
- cosine: () => cosine,
7620
+ computeMetrics: () => computeMetrics,
7621
+ cosine: () => cosine2,
7620
7622
  deriveIdentityKey: () => deriveIdentityKey,
7621
7623
  detectLanguage: () => detectLanguage,
7622
7624
  diffScan: () => diffScan,
7625
+ discoverTestFiles: () => discoverTestFiles,
7623
7626
  enrichPurpose: () => enrichPurpose,
7624
7627
  ensureEmbedded: () => ensureEmbedded,
7625
7628
  ensureIndexed: () => ensureIndexed,
@@ -7654,10 +7657,13 @@ __export(dist_exports, {
7654
7657
  metadataQueries: () => metadataQueries,
7655
7658
  openDb: () => openDb,
7656
7659
  parseGitLog: () => parseGitLog,
7660
+ parseQuery: () => parseQuery,
7657
7661
  registerRepo: () => registerRepo,
7658
7662
  removeRepo: () => removeRepo,
7659
7663
  repoQueries: () => repoQueries,
7660
7664
  resolveImport: () => resolveImport,
7665
+ resolveSnippets: () => resolveSnippets,
7666
+ runEval: () => runEval,
7661
7667
  runIndex: () => runIndex,
7662
7668
  setTelemetryEnabled: () => setTelemetryEnabled,
7663
7669
  settingsQueries: () => settingsQueries,
@@ -7670,21 +7676,21 @@ __export(dist_exports, {
7670
7676
  vectorSearch: () => vectorSearch
7671
7677
  });
7672
7678
  import { randomUUID } from "crypto";
7673
- import Database from "better-sqlite3";
7674
- import { existsSync, mkdirSync } from "fs";
7675
- import { join as join3 } from "path";
7676
- import { homedir } from "os";
7679
+ import { readFile } from "fs/promises";
7677
7680
  import { createHash } from "crypto";
7678
7681
  import { execFile } from "child_process";
7679
7682
  import { stat as stat4 } from "fs/promises";
7680
- import { resolve as resolve3, extname as extname2 } from "path";
7683
+ import { extname as extname2, resolve as resolve3 } from "path";
7681
7684
  import { promisify } from "util";
7682
- import { readFile } from "fs/promises";
7683
- import { createHash as createHash2 } from "crypto";
7684
7685
  import { execFile as execFile2 } from "child_process";
7685
7686
  import { promisify as promisify2 } from "util";
7687
+ import { existsSync, mkdirSync } from "fs";
7688
+ import { homedir } from "os";
7689
+ import { join as join3 } from "path";
7690
+ import Database from "better-sqlite3";
7686
7691
  import { readFile as readFile2, stat as stat22 } from "fs/promises";
7687
7692
  import { relative as relative3 } from "path";
7693
+ import { createHash as createHash2 } from "crypto";
7688
7694
  function jsonParse(val, fallback) {
7689
7695
  if (val == null) return fallback;
7690
7696
  if (typeof val === "string") {
@@ -7704,339 +7710,75 @@ function fromEmbeddingBlob(blob2) {
7704
7710
  const buf = blob2 instanceof Buffer ? blob2 : Buffer.from(blob2);
7705
7711
  return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
7706
7712
  }
7707
- function resolveDbPath(customPath) {
7708
- if (customPath) return customPath;
7709
- const dir = join3(homedir(), ".lens");
7710
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
7711
- return join3(dir, "data.db");
7712
- }
7713
- function openDb(customPath) {
7714
- if (_db) return _db;
7715
- const dbPath = resolveDbPath(customPath);
7716
- const sqlite = new Database(dbPath);
7717
- sqlite.pragma("journal_mode = WAL");
7718
- sqlite.pragma("busy_timeout = 5000");
7719
- sqlite.pragma("foreign_keys = ON");
7720
- const db = drizzle(sqlite, { schema: schema_exports });
7721
- sqlite.exec(createTablesSql());
7722
- const cols = new Set(
7723
- sqlite.pragma("table_info(file_metadata)").map((c) => c.name)
7724
- );
7725
- if (!cols.has("sections")) sqlite.exec("ALTER TABLE file_metadata ADD COLUMN sections TEXT DEFAULT '[]'");
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");
7739
- _db = db;
7740
- _raw = sqlite;
7741
- return db;
7742
- }
7743
- function getDb() {
7744
- if (!_db) throw new Error("Database not initialized. Call openDb() first.");
7745
- return _db;
7746
- }
7747
- function getRawDb() {
7748
- if (!_raw) throw new Error("Database not initialized. Call openDb() first.");
7749
- return _raw;
7750
- }
7751
- function closeDb() {
7752
- if (_raw) _raw.close();
7753
- _db = null;
7754
- _raw = null;
7755
- }
7756
- function createTablesSql() {
7757
- return `
7758
- CREATE TABLE IF NOT EXISTS repos (
7759
- id TEXT PRIMARY KEY,
7760
- identity_key TEXT UNIQUE NOT NULL,
7761
- name TEXT NOT NULL,
7762
- root_path TEXT NOT NULL,
7763
- remote_url TEXT,
7764
- last_indexed_commit TEXT,
7765
- index_status TEXT NOT NULL DEFAULT 'pending',
7766
- last_indexed_at TEXT,
7767
- last_git_analysis_commit TEXT,
7768
- max_import_depth INTEGER DEFAULT 0,
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,
7774
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
7775
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7776
- );
7777
- CREATE INDEX IF NOT EXISTS idx_repos_identity ON repos(identity_key);
7778
-
7779
- CREATE TABLE IF NOT EXISTS chunks (
7780
- id TEXT PRIMARY KEY,
7781
- repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
7782
- path TEXT NOT NULL,
7783
- chunk_index INTEGER NOT NULL,
7784
- start_line INTEGER NOT NULL,
7785
- end_line INTEGER NOT NULL,
7786
- content TEXT NOT NULL,
7787
- chunk_hash TEXT NOT NULL,
7788
- last_seen_commit TEXT NOT NULL,
7789
- language TEXT,
7790
- embedding BLOB,
7791
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
7792
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
7793
- UNIQUE(repo_id, path, chunk_index, chunk_hash)
7794
- );
7795
- CREATE INDEX IF NOT EXISTS idx_chunks_repo_path ON chunks(repo_id, path);
7796
- CREATE INDEX IF NOT EXISTS idx_chunks_repo ON chunks(repo_id);
7797
- CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(repo_id, chunk_hash);
7798
-
7799
- CREATE TABLE IF NOT EXISTS file_metadata (
7800
- id TEXT PRIMARY KEY,
7801
- repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
7802
- path TEXT NOT NULL,
7803
- language TEXT,
7804
- exports TEXT DEFAULT '[]',
7805
- imports TEXT DEFAULT '[]',
7806
- docstring TEXT DEFAULT '',
7807
- sections TEXT DEFAULT '[]',
7808
- internals TEXT DEFAULT '[]',
7809
- purpose TEXT DEFAULT '',
7810
- purpose_hash TEXT DEFAULT '',
7811
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
7812
- UNIQUE(repo_id, path)
7813
- );
7814
- CREATE INDEX IF NOT EXISTS idx_file_metadata_repo ON file_metadata(repo_id);
7815
-
7816
- CREATE TABLE IF NOT EXISTS file_imports (
7817
- id TEXT PRIMARY KEY,
7818
- repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
7819
- source_path TEXT NOT NULL,
7820
- target_path TEXT NOT NULL,
7821
- UNIQUE(repo_id, source_path, target_path)
7822
- );
7823
- CREATE INDEX IF NOT EXISTS idx_file_imports_target ON file_imports(repo_id, target_path);
7824
-
7825
- CREATE TABLE IF NOT EXISTS file_stats (
7826
- id TEXT PRIMARY KEY,
7827
- repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
7828
- path TEXT NOT NULL,
7829
- commit_count INTEGER NOT NULL DEFAULT 0,
7830
- recent_count INTEGER NOT NULL DEFAULT 0,
7831
- last_modified TEXT,
7832
- UNIQUE(repo_id, path)
7833
- );
7834
-
7835
- CREATE TABLE IF NOT EXISTS file_cochanges (
7836
- id TEXT PRIMARY KEY,
7837
- repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
7838
- path_a TEXT NOT NULL,
7839
- path_b TEXT NOT NULL,
7840
- cochange_count INTEGER NOT NULL DEFAULT 1,
7841
- UNIQUE(repo_id, path_a, path_b)
7842
- );
7843
- CREATE INDEX IF NOT EXISTS idx_cochanges_lookup ON file_cochanges(repo_id, path_a);
7844
-
7845
- CREATE TABLE IF NOT EXISTS usage_counters (
7846
- id TEXT PRIMARY KEY,
7847
- date TEXT NOT NULL UNIQUE,
7848
- context_queries INTEGER NOT NULL DEFAULT 0,
7849
- embedding_requests INTEGER NOT NULL DEFAULT 0,
7850
- embedding_chunks INTEGER NOT NULL DEFAULT 0,
7851
- purpose_requests INTEGER NOT NULL DEFAULT 0,
7852
- repos_indexed INTEGER NOT NULL DEFAULT 0,
7853
- synced_at TEXT,
7854
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
7855
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7856
- );
7857
- CREATE INDEX IF NOT EXISTS idx_usage_counters_date ON usage_counters(date);
7858
-
7859
- CREATE TABLE IF NOT EXISTS request_logs (
7860
- id TEXT PRIMARY KEY,
7861
- method TEXT NOT NULL,
7862
- path TEXT NOT NULL,
7863
- status INTEGER NOT NULL,
7864
- duration_ms INTEGER NOT NULL,
7865
- source TEXT NOT NULL DEFAULT 'api',
7866
- request_body TEXT,
7867
- response_size INTEGER,
7868
- response_body TEXT,
7869
- trace TEXT,
7870
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
7871
- );
7872
- CREATE INDEX IF NOT EXISTS idx_request_logs_created ON request_logs(created_at);
7873
- CREATE INDEX IF NOT EXISTS idx_request_logs_source ON request_logs(source);
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
-
7881
- CREATE TABLE IF NOT EXISTS telemetry_events (
7882
- id TEXT PRIMARY KEY,
7883
- event_type TEXT NOT NULL,
7884
- event_data TEXT,
7885
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
7886
- synced_at TEXT
7887
- );
7888
- CREATE INDEX IF NOT EXISTS idx_telemetry_events_type ON telemetry_events(event_type);
7889
- CREATE INDEX IF NOT EXISTS idx_telemetry_events_synced ON telemetry_events(synced_at);
7890
- `;
7713
+ function extractSlice(chunks2, targetLine) {
7714
+ const chunk = chunks2.find((c) => c.start_line <= targetLine && targetLine <= c.end_line);
7715
+ if (!chunk) return null;
7716
+ const lines = chunk.content.split("\n");
7717
+ const offset = targetLine - chunk.start_line;
7718
+ const from = Math.max(0, offset - RADIUS);
7719
+ const to = Math.min(lines.length, offset + RADIUS + 1);
7720
+ const sliced = lines.slice(from, to);
7721
+ if (!sliced.length) return null;
7722
+ return {
7723
+ startLine: chunk.start_line + from,
7724
+ endLine: chunk.start_line + to - 1,
7725
+ code: sliced.join("\n")
7726
+ };
7891
7727
  }
7892
- function normalizeRemoteUrl(url) {
7893
- let normalized = url.trim();
7894
- const sshMatch = normalized.match(/^[\w-]+@([\w.-]+):(.+)$/);
7895
- if (sshMatch) {
7896
- normalized = `${sshMatch[1]}/${sshMatch[2]}`;
7897
- } else {
7898
- normalized = normalized.replace(/^https?:\/\//, "");
7728
+ function sliceContext(db, repoId, snippets, queryKind) {
7729
+ const result = /* @__PURE__ */ new Map();
7730
+ const limit = MAX_SLICES[queryKind] ?? 3;
7731
+ const candidates = [...snippets.values()].filter((s) => s.line !== null).slice(0, limit);
7732
+ if (!candidates.length) return result;
7733
+ const paths = candidates.map((c) => c.path);
7734
+ const allChunks = chunkQueries.getByRepoPaths(db, repoId, paths);
7735
+ const chunksByPath = /* @__PURE__ */ new Map();
7736
+ for (const c of allChunks) {
7737
+ const arr = chunksByPath.get(c.path) ?? [];
7738
+ arr.push({ start_line: c.start_line, end_line: c.end_line, content: c.content });
7739
+ chunksByPath.set(c.path, arr);
7740
+ }
7741
+ for (const snip of candidates) {
7742
+ const chunks2 = chunksByPath.get(snip.path);
7743
+ if (!chunks2) continue;
7744
+ const slice = extractSlice(chunks2, snip.line);
7745
+ if (!slice) continue;
7746
+ result.set(snip.path, {
7747
+ path: snip.path,
7748
+ startLine: slice.startLine,
7749
+ endLine: slice.endLine,
7750
+ code: slice.code,
7751
+ symbol: snip.symbol
7752
+ });
7899
7753
  }
7900
- normalized = normalized.replace(/\.git$/, "");
7901
- normalized = normalized.replace(/\/$/, "");
7902
- return normalized.toLowerCase();
7903
- }
7904
- function deriveIdentityKey(rootPath, remoteUrl) {
7905
- const source = remoteUrl ? normalizeRemoteUrl(remoteUrl) : rootPath;
7906
- return createHash("sha256").update(source).digest("hex");
7754
+ return result;
7907
7755
  }
7908
- function isDocFile(filePath) {
7909
- return DOCS_EXTENSIONS.has(extname2(filePath).toLowerCase());
7756
+ function setTelemetryEnabled(enabled) {
7757
+ _enabled = enabled;
7910
7758
  }
7911
- function detectLanguage(filePath) {
7912
- return LANG_MAP[extname2(filePath).toLowerCase()] ?? null;
7759
+ function track(db, eventType, data) {
7760
+ if (!_enabled) return;
7761
+ try {
7762
+ telemetryQueries.insert(db, eventType, data);
7763
+ } catch {
7764
+ }
7913
7765
  }
7914
- function isBinaryExt(filePath) {
7915
- return BINARY_EXTENSIONS.has(extname2(filePath).toLowerCase());
7766
+ function computeChunkHash(content, params) {
7767
+ const payload = JSON.stringify({ content, params });
7768
+ return createHash("sha256").update(payload).digest("hex");
7916
7769
  }
7917
- async function fullScan(repoRoot) {
7918
- const { stdout } = await execFileAsync("git", ["ls-files", "-z"], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 });
7919
- const paths = stdout.split("\0").filter(Boolean);
7920
- const results = [];
7921
- for (const p of paths) {
7922
- if (isBinaryExt(p)) continue;
7923
- const abs = resolve3(repoRoot, p);
7924
- try {
7925
- const s = await stat4(abs);
7926
- if (s.size > MAX_FILE_SIZE) continue;
7927
- } catch {
7928
- continue;
7929
- }
7930
- results.push({ path: p, absolute_path: abs, language: detectLanguage(p), status: "added" });
7931
- }
7932
- return results;
7933
- }
7934
- async function diffScan(repoRoot, fromCommit, toCommit) {
7935
- const { stdout } = await execFileAsync(
7936
- "git",
7937
- ["diff", "--name-status", "--diff-filter=ACMRD", "-z", `${fromCommit}..${toCommit}`],
7938
- { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 }
7939
- );
7940
- const parts = stdout.split("\0").filter(Boolean);
7941
- const results = [];
7942
- for (let i = 0; i < parts.length; i += 2) {
7943
- const statusChar = parts[i];
7944
- const p = parts[i + 1];
7945
- if (!p) continue;
7946
- if (isBinaryExt(p)) continue;
7947
- const abs = resolve3(repoRoot, p);
7948
- let status;
7949
- if (statusChar === "D") {
7950
- status = "deleted";
7951
- } else if (statusChar === "A") {
7952
- status = "added";
7953
- } else {
7954
- status = "modified";
7955
- }
7956
- if (status !== "deleted") {
7957
- try {
7958
- const s = await stat4(abs);
7959
- if (s.size > MAX_FILE_SIZE) continue;
7960
- } catch {
7961
- continue;
7962
- }
7963
- }
7964
- results.push({ path: p, absolute_path: abs, language: detectLanguage(p), status });
7965
- }
7966
- return results;
7967
- }
7968
- async function getHeadCommit(repoRoot) {
7969
- const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
7970
- return stdout.trim();
7971
- }
7972
- function registerRepo(db, rootPath, name, remoteUrl) {
7973
- const identityKey = deriveIdentityKey(rootPath, remoteUrl);
7974
- const repoName = name ?? rootPath.split("/").pop() ?? "unknown";
7975
- const { id, created } = repoQueries.upsert(db, identityKey, repoName, rootPath, remoteUrl ?? null);
7976
- return { repo_id: id, identity_key: identityKey, name: repoName, created };
7977
- }
7978
- function getRepo(db, id) {
7979
- const repo = repoQueries.getById(db, id);
7980
- if (!repo) throw new Error("repo not found");
7981
- return repo;
7982
- }
7983
- function listRepos(db) {
7984
- return repoQueries.list(db);
7985
- }
7986
- function removeRepo(db, id) {
7987
- const chunkCount = chunkQueries.countByRepo(db, id);
7988
- const deleted = repoQueries.remove(db, id);
7989
- if (!deleted) throw new Error("repo not found");
7990
- return { deleted: true, chunks_removed: chunkCount };
7991
- }
7992
- async function getRepoStatus(db, id) {
7993
- const repo = repoQueries.getById(db, id);
7994
- if (!repo) throw new Error("repo not found");
7995
- let currentHead = null;
7996
- try {
7997
- currentHead = await getHeadCommit(repo.root_path);
7998
- } catch {
7999
- }
8000
- const isStale = !!(currentHead && repo.last_indexed_commit !== currentHead);
8001
- const stats = chunkQueries.getStats(db, id);
8002
- const structural = metadataQueries.getStructuralStats(db, id);
8003
- const vocabRaw = repo.vocab_clusters;
8004
- const vocabClusters = vocabRaw ? JSON.parse(vocabRaw) : [];
8005
- return {
8006
- index_status: repo.index_status,
8007
- indexed_commit: repo.last_indexed_commit,
8008
- current_head: currentHead,
8009
- is_stale: isStale,
8010
- chunk_count: stats.chunk_count,
8011
- files_indexed: stats.files_indexed,
8012
- embedded_count: stats.embedded_count,
8013
- embeddable_count: stats.embeddable_count,
8014
- embedded_pct: stats.embeddable_count > 0 ? Math.round(stats.embedded_count / stats.embeddable_count * 100) : 0,
8015
- metadata_count: structural.metadata_count,
8016
- import_edge_count: structural.import_edge_count,
8017
- git_commits_analyzed: structural.git_file_count,
8018
- cochange_pairs: structural.cochange_pairs,
8019
- purpose_count: structural.purpose_count,
8020
- purpose_total: structural.purpose_total,
8021
- vocab_cluster_count: Array.isArray(vocabClusters) ? vocabClusters.length : 0
8022
- };
8023
- }
8024
- function computeChunkHash(content, params) {
8025
- const payload = JSON.stringify({ content, params });
8026
- return createHash2("sha256").update(payload).digest("hex");
8027
- }
8028
- function findBoundary(lines, targetLine, windowSize = 15) {
8029
- const start = Math.max(0, targetLine - windowSize);
8030
- const end = Math.min(lines.length, targetLine + windowSize);
8031
- let bestBlank = -1;
8032
- let bestBlankDist = Infinity;
8033
- for (let i = start; i < end; i++) {
8034
- if (lines[i].trim() === "") {
8035
- const dist = Math.abs(i - targetLine);
8036
- if (dist < bestBlankDist) {
8037
- bestBlank = i;
8038
- bestBlankDist = dist;
8039
- }
7770
+ function findBoundary(lines, targetLine, windowSize = 15) {
7771
+ const start = Math.max(0, targetLine - windowSize);
7772
+ const end = Math.min(lines.length, targetLine + windowSize);
7773
+ let bestBlank = -1;
7774
+ let bestBlankDist = Infinity;
7775
+ for (let i = start; i < end; i++) {
7776
+ if (lines[i].trim() === "") {
7777
+ const dist = Math.abs(i - targetLine);
7778
+ if (dist < bestBlankDist) {
7779
+ bestBlank = i;
7780
+ bestBlankDist = dist;
7781
+ }
8040
7782
  }
8041
7783
  }
8042
7784
  if (bestBlank >= 0) return bestBlank + 1;
@@ -8093,6 +7835,70 @@ function chunkFile(content, params = DEFAULT_CHUNKING_PARAMS) {
8093
7835
  }
8094
7836
  return chunks2;
8095
7837
  }
7838
+ function isDocFile(filePath) {
7839
+ return DOCS_EXTENSIONS.has(extname2(filePath).toLowerCase());
7840
+ }
7841
+ function detectLanguage(filePath) {
7842
+ return LANG_MAP[extname2(filePath).toLowerCase()] ?? null;
7843
+ }
7844
+ function isBinaryExt(filePath) {
7845
+ return BINARY_EXTENSIONS.has(extname2(filePath).toLowerCase());
7846
+ }
7847
+ async function fullScan(repoRoot) {
7848
+ const { stdout } = await execFileAsync("git", ["ls-files", "-z"], { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 });
7849
+ const paths = stdout.split("\0").filter(Boolean);
7850
+ const results = [];
7851
+ for (const p of paths) {
7852
+ if (isBinaryExt(p)) continue;
7853
+ const abs = resolve3(repoRoot, p);
7854
+ try {
7855
+ const s = await stat4(abs);
7856
+ if (s.size > MAX_FILE_SIZE) continue;
7857
+ } catch {
7858
+ continue;
7859
+ }
7860
+ results.push({ path: p, absolute_path: abs, language: detectLanguage(p), status: "added" });
7861
+ }
7862
+ return results;
7863
+ }
7864
+ async function diffScan(repoRoot, fromCommit, toCommit) {
7865
+ const { stdout } = await execFileAsync(
7866
+ "git",
7867
+ ["diff", "--name-status", "--diff-filter=ACMRD", "-z", `${fromCommit}..${toCommit}`],
7868
+ { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 }
7869
+ );
7870
+ const parts = stdout.split("\0").filter(Boolean);
7871
+ const results = [];
7872
+ for (let i = 0; i < parts.length; i += 2) {
7873
+ const statusChar = parts[i];
7874
+ const p = parts[i + 1];
7875
+ if (!p) continue;
7876
+ if (isBinaryExt(p)) continue;
7877
+ const abs = resolve3(repoRoot, p);
7878
+ let status;
7879
+ if (statusChar === "D") {
7880
+ status = "deleted";
7881
+ } else if (statusChar === "A") {
7882
+ status = "added";
7883
+ } else {
7884
+ status = "modified";
7885
+ }
7886
+ if (status !== "deleted") {
7887
+ try {
7888
+ const s = await stat4(abs);
7889
+ if (s.size > MAX_FILE_SIZE) continue;
7890
+ } catch {
7891
+ continue;
7892
+ }
7893
+ }
7894
+ results.push({ path: p, absolute_path: abs, language: detectLanguage(p), status });
7895
+ }
7896
+ return results;
7897
+ }
7898
+ async function getHeadCommit(repoRoot) {
7899
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
7900
+ return stdout.trim();
7901
+ }
8096
7902
  function extractImportSpecifiers(content, language) {
8097
7903
  switch (language) {
8098
7904
  case "typescript":
@@ -8195,13 +8001,13 @@ function resolveRust(dir, spec, known) {
8195
8001
  base = spec.replace("crate::", "src/").replace(/::/g, "/");
8196
8002
  } else if (spec.startsWith("super::")) {
8197
8003
  const parent = dir.substring(0, dir.lastIndexOf("/"));
8198
- base = parent + "/" + spec.replace("super::", "").replace(/::/g, "/");
8004
+ base = `${parent}/${spec.replace("super::", "").replace(/::/g, "/")}`;
8199
8005
  } else {
8200
- base = dir + "/" + spec.replace("self::", "").replace(/::/g, "/");
8006
+ base = `${dir}/${spec.replace("self::", "").replace(/::/g, "/")}`;
8201
8007
  }
8202
- const candidate = normalizePath2(base) + ".rs";
8008
+ const candidate = `${normalizePath2(base)}.rs`;
8203
8009
  if (known.has(candidate)) return candidate;
8204
- const modCandidate = normalizePath2(base) + "/mod.rs";
8010
+ const modCandidate = `${normalizePath2(base)}/mod.rs`;
8205
8011
  if (known.has(modCandidate)) return modCandidate;
8206
8012
  return null;
8207
8013
  }
@@ -8349,7 +8155,8 @@ function extractAndPersistMetadata(db, repoId) {
8349
8155
  for (const row of rows) {
8350
8156
  const existing = files.get(row.path);
8351
8157
  if (existing) {
8352
- existing.content += "\n" + row.content;
8158
+ existing.content += `
8159
+ ${row.content}`;
8353
8160
  } else {
8354
8161
  files.set(row.path, {
8355
8162
  content: row.content,
@@ -8375,35 +8182,6 @@ function extractAndPersistMetadata(db, repoId) {
8375
8182
  }
8376
8183
  return count;
8377
8184
  }
8378
- function buildAndPersistImportGraph(db, repoId) {
8379
- const rows = chunkQueries.getAllByRepo(db, repoId);
8380
- const files = /* @__PURE__ */ new Map();
8381
- for (const row of rows) {
8382
- const existing = files.get(row.path);
8383
- if (existing) {
8384
- existing.content += "\n" + row.content;
8385
- } else {
8386
- files.set(row.path, {
8387
- content: row.content,
8388
- language: row.language ?? detectLanguage(row.path) ?? "text"
8389
- });
8390
- }
8391
- }
8392
- const knownPaths = new Set(files.keys());
8393
- importQueries.deleteByRepo(db, repoId);
8394
- let edgeCount = 0;
8395
- for (const [sourcePath, { content, language }] of files) {
8396
- const specs = extractImportSpecifiers(content, language);
8397
- for (const spec of specs) {
8398
- const targetPath = resolveImport(sourcePath, spec, language, knownPaths);
8399
- if (targetPath && targetPath !== sourcePath) {
8400
- importQueries.insert(db, repoId, sourcePath, targetPath);
8401
- edgeCount++;
8402
- }
8403
- }
8404
- }
8405
- return edgeCount;
8406
- }
8407
8185
  async function analyzeGitHistory(db, repoId, rootPath, lastAnalyzedCommit) {
8408
8186
  const args = ["log", "--name-only", "--format=%H %aI", "--no-merges", `-n`, `${MAX_COMMITS}`];
8409
8187
  if (lastAnalyzedCommit) args.push(`${lastAnalyzedCommit}..HEAD`);
@@ -8498,15 +8276,35 @@ function parseGitLog(stdout) {
8498
8276
  if (current) commits.push(current);
8499
8277
  return commits;
8500
8278
  }
8501
- function setTelemetryEnabled(enabled) {
8502
- _enabled = enabled;
8503
- }
8504
- function track(db, eventType, data) {
8505
- if (!_enabled) return;
8506
- try {
8507
- telemetryQueries.insert(db, eventType, data);
8508
- } catch {
8279
+ function buildAndPersistImportGraph(db, repoId) {
8280
+ const rows = chunkQueries.getAllByRepo(db, repoId);
8281
+ const files = /* @__PURE__ */ new Map();
8282
+ for (const row of rows) {
8283
+ const existing = files.get(row.path);
8284
+ if (existing) {
8285
+ existing.content += `
8286
+ ${row.content}`;
8287
+ } else {
8288
+ files.set(row.path, {
8289
+ content: row.content,
8290
+ language: row.language ?? detectLanguage(row.path) ?? "text"
8291
+ });
8292
+ }
8293
+ }
8294
+ const knownPaths = new Set(files.keys());
8295
+ importQueries.deleteByRepo(db, repoId);
8296
+ let edgeCount = 0;
8297
+ for (const [sourcePath, { content, language }] of files) {
8298
+ const specs = extractImportSpecifiers(content, language);
8299
+ for (const spec of specs) {
8300
+ const targetPath = resolveImport(sourcePath, spec, language, knownPaths);
8301
+ if (targetPath && targetPath !== sourcePath) {
8302
+ importQueries.insert(db, repoId, sourcePath, targetPath);
8303
+ edgeCount++;
8304
+ }
8305
+ }
8509
8306
  }
8307
+ return edgeCount;
8510
8308
  }
8511
8309
  async function withLock(key, fn) {
8512
8310
  while (locks.has(key)) {
@@ -8524,7 +8322,7 @@ async function withLock(key, fn) {
8524
8322
  resolve22();
8525
8323
  }
8526
8324
  }
8527
- async function runIndex(db, repoId, caps, force = false, onProgress, trace) {
8325
+ async function runIndex(db, repoId, _caps, force = false, onProgress, trace) {
8528
8326
  return withLock(repoId, async () => {
8529
8327
  const start = Date.now();
8530
8328
  const repo = repoQueries.getById(db, repoId);
@@ -8696,458 +8494,372 @@ async function ensureIndexed(db, repoId, caps) {
8696
8494
  if (repo.last_indexed_commit === head) return null;
8697
8495
  return runIndex(db, repoId, caps);
8698
8496
  }
8699
- function estimateTokens(text22) {
8700
- return Math.ceil(text22.length / CHARS_PER_TOKEN);
8497
+ function estimateTokens(s) {
8498
+ return Math.ceil(s.length / 4);
8701
8499
  }
8702
- async function ensureEmbedded(db, repoId, caps) {
8703
- const start = Date.now();
8704
- if (!caps?.embedTexts) {
8705
- return { embedded_count: 0, skipped_count: 0, duration_ms: 0 };
8500
+ function basename3(p) {
8501
+ const i = p.lastIndexOf("/");
8502
+ return i >= 0 ? p.substring(i + 1) : p;
8503
+ }
8504
+ function guessLang(path) {
8505
+ const dot = path.lastIndexOf(".");
8506
+ if (dot < 0) return "";
8507
+ return LANG_EXT[path.substring(dot + 1)] ?? "";
8508
+ }
8509
+ function getPurpose(path, data) {
8510
+ for (const m of data.metadata) {
8511
+ if (m.path === path && m.purpose) return m.purpose;
8706
8512
  }
8707
- let embedded = 0;
8708
- let apiCalls = 0;
8709
- while (apiCalls < MAX_API_CALLS) {
8710
- const remaining = chunkQueries.countUnembedded(db, repoId);
8711
- if (remaining === 0) break;
8712
- const rows = chunkQueries.getUnembedded(db, repoId, POOL_SIZE);
8713
- if (rows.length === 0) break;
8714
- const validRows = rows.filter((r) => r.content.trim().length > 0);
8715
- if (validRows.length === 0) {
8716
- apiCalls++;
8717
- continue;
8718
- }
8719
- let offset = 0;
8720
- let poolFailed = false;
8721
- while (offset < validRows.length && apiCalls < MAX_API_CALLS) {
8722
- const batch = [];
8723
- let batchTokens = 0;
8724
- while (offset < validRows.length) {
8725
- const est = estimateTokens(validRows[offset].content);
8726
- if (batch.length > 0 && batchTokens + est > MAX_BATCH_TOKENS) break;
8727
- batch.push(validRows[offset]);
8728
- batchTokens += est;
8729
- offset++;
8730
- }
8731
- try {
8732
- const texts = batch.map((r) => r.content);
8733
- const vectors = await caps.embedTexts(texts);
8734
- for (let i = 0; i < batch.length; i++) {
8735
- chunkQueries.updateEmbedding(db, batch[i].id, repoId, vectors[i]);
8736
- }
8737
- embedded += batch.length;
8738
- } catch (err) {
8739
- console.error(`[LENS] Embed batch failed (${batch.length} chunks, ~${batchTokens} tokens):`, err.message);
8740
- poolFailed = true;
8741
- break;
8742
- }
8743
- apiCalls++;
8744
- }
8745
- if (poolFailed) break;
8746
- }
8747
- return { embedded_count: embedded, skipped_count: 0, duration_ms: Date.now() - start };
8513
+ return "";
8748
8514
  }
8749
- async function enrichPurpose(db, repoId, caps, fullRun = false) {
8750
- const start = Date.now();
8751
- if (!caps?.generatePurpose) {
8752
- return { enriched: 0, skipped: 0, duration_ms: 0 };
8515
+ function renderImports(path, data) {
8516
+ const fwd = data.forwardImports.get(path)?.slice(0, 3) ?? [];
8517
+ const rev = data.reverseImports.get(path)?.slice(0, 3) ?? [];
8518
+ const parts = [];
8519
+ if (rev.length) parts.push(`\u2190 ${rev.join(", ")}`);
8520
+ if (fwd.length) parts.push(`\u2192 ${fwd.join(", ")}`);
8521
+ return parts.join(" | ");
8522
+ }
8523
+ function renderCochanges(path, cochanges, limit = 2) {
8524
+ const partners = [];
8525
+ for (const cc of cochanges) {
8526
+ if (cc.path === path) partners.push({ name: basename3(cc.partner), count: cc.count });
8527
+ else if (cc.partner === path) partners.push({ name: basename3(cc.path), count: cc.count });
8753
8528
  }
8754
- let totalEnriched = 0;
8755
- let totalSkipped = 0;
8756
- while (true) {
8757
- const candidates = metadataQueries.getCandidatesForPurpose(db, repoId, PURPOSE_BATCH_LIMIT);
8758
- if (candidates.length === 0) break;
8759
- for (let i = 0; i < candidates.length; i += PURPOSE_CONCURRENCY) {
8760
- const batch = candidates.slice(i, i + PURPOSE_CONCURRENCY);
8761
- const results = await Promise.allSettled(
8762
- batch.map(async (row) => {
8763
- const exports = Array.isArray(row.exports) ? row.exports : jsonParse(row.exports, []);
8764
- const purpose = await caps.generatePurpose(row.path, row.first_chunk, exports, row.docstring ?? "");
8765
- return { row, purpose };
8766
- })
8767
- );
8768
- for (let j = 0; j < results.length; j++) {
8769
- const result = results[j];
8770
- const row = batch[j];
8771
- if (result.status === "fulfilled" && result.value.purpose) {
8772
- metadataQueries.updatePurpose(db, repoId, row.path, result.value.purpose, row.chunk_hash);
8773
- totalEnriched++;
8774
- } else {
8775
- metadataQueries.setPurposeHash(db, repoId, row.path, row.chunk_hash);
8776
- totalSkipped++;
8777
- }
8778
- }
8779
- }
8780
- if (!fullRun) break;
8529
+ partners.sort((a, b) => b.count - a.count);
8530
+ return partners.slice(0, limit).map((p) => `${p.name} (${p.count}x)`).join(", ");
8531
+ }
8532
+ function getExports(path, data) {
8533
+ for (const m of data.metadata) {
8534
+ if (m.path === path) return m.exports ?? [];
8781
8535
  }
8782
- return { enriched: totalEnriched, skipped: totalSkipped, duration_ms: Date.now() - start };
8536
+ return [];
8783
8537
  }
8784
- function splitIdentifier(name) {
8785
- 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));
8538
+ function fileLabel(path, snippet) {
8539
+ const line = snippet?.line ? `:${snippet.line}` : "";
8540
+ return `${path}${line}`;
8786
8541
  }
8787
- function extractVocab(files) {
8788
- const termToFiles = /* @__PURE__ */ new Map();
8789
- for (const f of files) {
8790
- const pathParts = f.path.split("/");
8791
- for (const seg of pathParts) {
8792
- const base = seg.replace(/\.[^.]+$/, "");
8793
- for (const word of splitIdentifier(base)) {
8794
- const s = termToFiles.get(word) ?? /* @__PURE__ */ new Set();
8795
- s.add(f.path);
8796
- termToFiles.set(word, s);
8797
- }
8798
- }
8799
- for (const exp of f.exports) {
8800
- for (const word of splitIdentifier(exp)) {
8801
- const s = termToFiles.get(word) ?? /* @__PURE__ */ new Set();
8802
- s.add(f.path);
8803
- termToFiles.set(word, s);
8804
- }
8542
+ function renderSlice(path, data) {
8543
+ const slice = data.slices?.get(path);
8544
+ if (!slice) return null;
8545
+ const lang = guessLang(path);
8546
+ return ` \`\`\`${lang}
8547
+ ${slice.code}
8548
+ \`\`\``;
8549
+ }
8550
+ function formatNatural(data) {
8551
+ const L = [];
8552
+ const files = data.files.slice(0, 7);
8553
+ const snippets = data.snippets ?? /* @__PURE__ */ new Map();
8554
+ L.push(`# ${data.goal}`);
8555
+ L.push("");
8556
+ L.push("## Key Files");
8557
+ for (let i = 0; i < files.length; i++) {
8558
+ const f = files[i];
8559
+ const snip = snippets.get(f.path);
8560
+ const purpose = getPurpose(f.path, data);
8561
+ const label = fileLabel(f.path, snip);
8562
+ L.push(`${i + 1}. **${label}**${purpose ? ` \u2014 ${purpose}` : ""}`);
8563
+ const exports = getExports(f.path, data);
8564
+ if (exports.length) L.push(` Exports: ${exports.slice(0, 5).join(", ")}`);
8565
+ const imports = renderImports(f.path, data);
8566
+ if (imports) L.push(` ${imports}`);
8567
+ const cochangeStr = renderCochanges(f.path, data.cochanges);
8568
+ if (cochangeStr) L.push(` Co-changes: ${cochangeStr}`);
8569
+ const sliceStr = renderSlice(f.path, data);
8570
+ if (sliceStr) L.push(sliceStr);
8571
+ }
8572
+ const testLines = [];
8573
+ const testFiles = data.testFiles;
8574
+ if (testFiles) {
8575
+ const seen = /* @__PURE__ */ new Set();
8576
+ for (const f of files.slice(0, 5)) {
8577
+ const tests = testFiles.get(f.path);
8578
+ if (tests) {
8579
+ for (const t of tests) {
8580
+ if (!seen.has(t)) {
8581
+ seen.add(t);
8582
+ testLines.push(`- ${t}`);
8583
+ }
8584
+ }
8585
+ }
8586
+ }
8587
+ }
8588
+ if (testLines.length) {
8589
+ L.push("");
8590
+ L.push("## Tests");
8591
+ for (const t of testLines) L.push(t);
8592
+ }
8593
+ return L.join("\n");
8594
+ }
8595
+ function formatSymbol(data) {
8596
+ const L = [];
8597
+ const files = data.files;
8598
+ const snippets = data.snippets ?? /* @__PURE__ */ new Map();
8599
+ const top = files[0];
8600
+ if (!top) return `# ${data.goal}
8601
+
8602
+ No files found.`;
8603
+ const snip = snippets.get(top.path);
8604
+ const purpose = getPurpose(top.path, data);
8605
+ L.push(`# Symbol: ${data.goal}`);
8606
+ L.push("");
8607
+ L.push("## Definition");
8608
+ L.push(`**${fileLabel(top.path, snip)}**${purpose ? ` \u2014 ${purpose}` : ""}`);
8609
+ if (snip?.symbol) {
8610
+ L.push(` \`${snip.symbol}\``);
8611
+ } else {
8612
+ const exports = getExports(top.path, data);
8613
+ if (exports.length) L.push(` Exports: ${exports.slice(0, 5).join(", ")}`);
8614
+ }
8615
+ const sliceStr = renderSlice(top.path, data);
8616
+ if (sliceStr) L.push(sliceStr);
8617
+ const rev = data.reverseImports.get(top.path);
8618
+ if (rev?.length) {
8619
+ L.push("");
8620
+ L.push("## Dependents");
8621
+ L.push(`\u2190 ${rev.slice(0, 5).join(", ")}`);
8622
+ }
8623
+ const cochangeStr = renderCochanges(top.path, data.cochanges, 3);
8624
+ if (cochangeStr) {
8625
+ L.push("");
8626
+ L.push("## Co-changes");
8627
+ L.push(cochangeStr);
8628
+ }
8629
+ if (files.length > 1) {
8630
+ L.push("");
8631
+ L.push("## Also relevant");
8632
+ for (let i = 1; i < Math.min(files.length, 5); i++) {
8633
+ const f = files[i];
8634
+ const p = getPurpose(f.path, data);
8635
+ L.push(`${i + 1}. ${f.path}${p ? ` \u2014 ${p}` : ""}`);
8805
8636
  }
8806
8637
  }
8807
- const totalFiles = files.length;
8808
- const maxDf = Math.max(10, Math.floor(totalFiles * 0.3));
8809
- const filtered = [];
8810
- for (const [term, fileSet] of termToFiles) {
8811
- if (fileSet.size >= 2 && fileSet.size <= maxDf) {
8812
- filtered.push(term);
8638
+ return L.join("\n");
8639
+ }
8640
+ function formatError(data) {
8641
+ const L = [];
8642
+ const files = data.files;
8643
+ const snippets = data.snippets ?? /* @__PURE__ */ new Map();
8644
+ L.push(`# Error: ${data.goal}`);
8645
+ L.push("");
8646
+ if (files.length === 0) {
8647
+ L.push("No matching files found.");
8648
+ return L.join("\n");
8649
+ }
8650
+ const top = files[0];
8651
+ const snip = snippets.get(top.path);
8652
+ const purpose = getPurpose(top.path, data);
8653
+ L.push("## Error Source");
8654
+ L.push(`1. **${fileLabel(top.path, snip)}**${purpose ? ` \u2014 ${purpose}` : ""}`);
8655
+ const exports = getExports(top.path, data);
8656
+ if (exports.length) L.push(` Exports: ${exports.slice(0, 5).join(", ")}`);
8657
+ const imports = renderImports(top.path, data);
8658
+ if (imports) L.push(` ${imports}`);
8659
+ const sliceStr = renderSlice(top.path, data);
8660
+ if (sliceStr) L.push(sliceStr);
8661
+ if (files.length > 1) {
8662
+ L.push("");
8663
+ L.push("## Also References");
8664
+ for (let i = 1; i < Math.min(files.length, 5); i++) {
8665
+ const f = files[i];
8666
+ const s = snippets.get(f.path);
8667
+ const p = getPurpose(f.path, data);
8668
+ L.push(`${i + 1}. **${fileLabel(f.path, s)}**${p ? ` \u2014 ${p}` : ""}`);
8813
8669
  }
8814
8670
  }
8815
- filtered.sort((a, b) => (termToFiles.get(b)?.size ?? 0) - (termToFiles.get(a)?.size ?? 0));
8816
- const terms = filtered.slice(0, MAX_TERMS);
8817
- return { terms, termToFiles };
8671
+ return L.join("\n");
8818
8672
  }
8819
- function cosine(a, b) {
8820
- let dot = 0, na = 0, nb = 0;
8821
- for (let i = 0; i < a.length; i++) {
8822
- dot += a[i] * b[i];
8823
- na += a[i] * a[i];
8824
- nb += b[i] * b[i];
8673
+ function formatStackTrace(data) {
8674
+ const L = [];
8675
+ const files = data.files;
8676
+ const snippets = data.snippets ?? /* @__PURE__ */ new Map();
8677
+ L.push("# Stack Trace");
8678
+ L.push("");
8679
+ if (files.length === 0) {
8680
+ L.push("No matching files found.");
8681
+ return L.join("\n");
8682
+ }
8683
+ const top = files[0];
8684
+ const snip = snippets.get(top.path);
8685
+ const purpose = getPurpose(top.path, data);
8686
+ L.push("## Crash Point");
8687
+ L.push(`**${fileLabel(top.path, snip)}**${purpose ? ` \u2014 ${purpose}` : ""}`);
8688
+ if (snip?.symbol) {
8689
+ L.push(` \`${snip.symbol}\``);
8690
+ }
8691
+ const sliceStr = renderSlice(top.path, data);
8692
+ if (sliceStr) L.push(sliceStr);
8693
+ const chain = [];
8694
+ const fwd = data.forwardImports.get(top.path);
8695
+ const rev = data.reverseImports.get(top.path);
8696
+ if (rev?.length || fwd?.length) {
8697
+ const callers = rev?.slice(0, 2) ?? [];
8698
+ const deps = fwd?.slice(0, 2) ?? [];
8699
+ if (callers.length || deps.length) {
8700
+ const parts = [...callers.map((c) => `${c} \u2192`), top.path, ...deps.map((d) => `\u2192 ${d}`)];
8701
+ chain.push(parts.join(" "));
8702
+ }
8703
+ }
8704
+ if (chain.length) {
8705
+ L.push("");
8706
+ L.push("## Call Chain");
8707
+ for (const c of chain) L.push(c);
8825
8708
  }
8826
- const denom = Math.sqrt(na) * Math.sqrt(nb);
8827
- return denom > 0 ? dot / denom : 0;
8709
+ if (files.length > 1) {
8710
+ L.push("");
8711
+ L.push("## Related");
8712
+ for (let i = 1; i < Math.min(files.length, 5); i++) {
8713
+ const f = files[i];
8714
+ const p = getPurpose(f.path, data);
8715
+ const cochangeStr = renderCochanges(f.path, data.cochanges, 1);
8716
+ L.push(`- ${f.path}${p ? ` \u2014 ${p}` : ""}${cochangeStr ? ` (${cochangeStr})` : ""}`);
8717
+ const relSlice = renderSlice(f.path, data);
8718
+ if (relSlice) L.push(relSlice);
8719
+ }
8720
+ }
8721
+ return L.join("\n");
8828
8722
  }
8829
- function agglomerativeCluster(terms, embeddings) {
8830
- const n = terms.length;
8831
- const pairs = [];
8832
- for (let i = 0; i < n; i++) {
8833
- for (let j = i + 1; j < n; j++) {
8834
- const sim = cosine(embeddings[i], embeddings[j]);
8835
- if (sim >= SIMILARITY_THRESHOLD) pairs.push({ i, j, sim });
8723
+ function stripCodeSlices(lines) {
8724
+ const result = [];
8725
+ let inFence = false;
8726
+ for (const line of lines) {
8727
+ if (line.trimStart().startsWith("```") && !inFence) {
8728
+ inFence = true;
8729
+ continue;
8730
+ }
8731
+ if (inFence && line.trimStart().startsWith("```")) {
8732
+ inFence = false;
8733
+ continue;
8836
8734
  }
8735
+ if (!inFence) result.push(line);
8837
8736
  }
8838
- pairs.sort((a, b) => b.sim - a.sim);
8839
- const parent = new Int32Array(n);
8840
- const size = new Uint16Array(n);
8841
- for (let i = 0; i < n; i++) {
8842
- parent[i] = i;
8843
- size[i] = 1;
8737
+ return result;
8738
+ }
8739
+ function progressiveStrip(output, _data) {
8740
+ let lines = output.split("\n");
8741
+ if (estimateTokens(lines.join("\n")) > TOKEN_CAP) {
8742
+ lines = stripCodeSlices(lines);
8844
8743
  }
8845
- function find(x) {
8846
- while (parent[x] !== x) {
8847
- parent[x] = parent[parent[x]];
8848
- x = parent[x];
8849
- }
8850
- return x;
8744
+ if (estimateTokens(lines.join("\n")) > TOKEN_CAP) {
8745
+ lines = lines.map((l) => {
8746
+ if (l.trimStart().startsWith("Co-changes:")) {
8747
+ const match2 = l.match(/Co-changes:\s*([^,]+)/);
8748
+ if (match2) return `${l.substring(0, l.indexOf("Co-changes:"))}Co-changes: ${match2[1].trim()}`;
8749
+ }
8750
+ return l;
8751
+ });
8851
8752
  }
8852
- for (const { i, j } of pairs) {
8853
- const ri = find(i), rj = find(j);
8854
- if (ri === rj) continue;
8855
- if (size[ri] + size[rj] > MAX_CLUSTER_SIZE) continue;
8856
- if (size[ri] >= size[rj]) {
8857
- parent[rj] = ri;
8858
- size[ri] += size[rj];
8859
- } else {
8860
- parent[ri] = rj;
8861
- size[rj] += size[ri];
8753
+ if (estimateTokens(lines.join("\n")) > TOKEN_CAP) {
8754
+ const testIdx = lines.findIndex((l) => l.startsWith("## Tests"));
8755
+ if (testIdx >= 0) {
8756
+ let endIdx = lines.findIndex((l, i) => i > testIdx && l.startsWith("## "));
8757
+ if (endIdx < 0) endIdx = lines.length;
8758
+ lines.splice(testIdx, endIdx - testIdx);
8862
8759
  }
8863
8760
  }
8864
- const clusterMap = /* @__PURE__ */ new Map();
8865
- for (let i = 0; i < n; i++) {
8866
- const root = find(i);
8867
- const arr = clusterMap.get(root) ?? [];
8868
- arr.push(i);
8869
- clusterMap.set(root, arr);
8761
+ if (estimateTokens(lines.join("\n")) > TOKEN_CAP) {
8762
+ lines = lines.filter((l) => {
8763
+ const trimmed = l.trimStart();
8764
+ return !(trimmed.startsWith("\u2190") || trimmed.startsWith("\u2192")) || !trimmed.includes("|");
8765
+ });
8870
8766
  }
8871
- return [...clusterMap.values()].filter((c) => c.length >= 2);
8872
- }
8873
- async function buildVocabClusters(db, repoId, caps, force = false) {
8874
- if (!caps?.embedTexts) {
8875
- console.error("[LENS] Vocab clusters: skipped (no embedTexts capability)");
8876
- return;
8767
+ if (estimateTokens(lines.join("\n")) > TOKEN_CAP) {
8768
+ const reduced = [];
8769
+ let fileCount = 0;
8770
+ for (const line of lines) {
8771
+ if (/^\d+\./.test(line.trimStart())) {
8772
+ fileCount++;
8773
+ if (fileCount > 5) continue;
8774
+ }
8775
+ reduced.push(line);
8776
+ }
8777
+ lines = reduced;
8877
8778
  }
8878
- const repo = repoQueries.getById(db, repoId);
8879
- if (!repo) return;
8880
- if (!force && repo.last_vocab_cluster_commit && repo.last_vocab_cluster_commit === repo.last_indexed_commit) {
8881
- console.error("[LENS] Vocab clusters: skipped (unchanged since last build)");
8882
- return;
8779
+ if (estimateTokens(lines.join("\n")) > TOKEN_CAP) {
8780
+ lines = lines.map((l) => {
8781
+ const dashIdx = l.indexOf(" \u2014 ");
8782
+ if (dashIdx > 0 && (l.includes("**") || /^\d+\./.test(l.trimStart()))) {
8783
+ return l.substring(0, dashIdx);
8784
+ }
8785
+ return l;
8786
+ });
8883
8787
  }
8884
- const rows = metadataQueries.getByRepo(db, repoId).map((r) => ({
8885
- path: r.path,
8886
- exports: Array.isArray(r.exports) ? r.exports : jsonParse(r.exports, [])
8887
- }));
8888
- if (rows.length === 0) {
8889
- console.error("[LENS] Vocab clusters: skipped (no metadata rows)");
8890
- return;
8788
+ return lines.join("\n");
8789
+ }
8790
+ function filterByScoreRelevance(data) {
8791
+ if (!data.scores || data.scores.size === 0) return data;
8792
+ const topScore = Math.max(...data.scores.values());
8793
+ if (topScore <= 0) return data;
8794
+ const threshold = topScore * 0.15;
8795
+ const filtered = data.files.filter((f) => {
8796
+ const score = data.scores?.get(f.path);
8797
+ if (score === void 0) return data.files.indexOf(f) < 5;
8798
+ return score >= threshold;
8799
+ });
8800
+ return { ...data, files: filtered };
8801
+ }
8802
+ function formatContextPack(data) {
8803
+ const filtered = filterByScoreRelevance(data);
8804
+ let output;
8805
+ switch (filtered.queryKind) {
8806
+ case "symbol":
8807
+ output = formatSymbol(filtered);
8808
+ break;
8809
+ case "error_message":
8810
+ output = formatError(filtered);
8811
+ break;
8812
+ case "stack_trace":
8813
+ output = formatStackTrace(filtered);
8814
+ break;
8815
+ default:
8816
+ output = formatNatural(filtered);
8817
+ break;
8891
8818
  }
8892
- const { terms, termToFiles } = extractVocab(rows);
8893
- if (terms.length < 4) {
8894
- console.error(`[LENS] Vocab clusters: skipped (only ${terms.length} terms, need \u22654)`);
8895
- return;
8819
+ if (estimateTokens(output) > TOKEN_CAP) {
8820
+ output = progressiveStrip(output, filtered);
8896
8821
  }
8897
- console.error(`[LENS] Vocab clusters: embedding ${terms.length} terms in ${Math.ceil(terms.length / TERM_BATCH_SIZE)} batches...`);
8898
- let embeddings;
8899
- try {
8900
- const results = [];
8901
- for (let i = 0; i < terms.length; i += TERM_BATCH_SIZE) {
8902
- const batch = terms.slice(i, i + TERM_BATCH_SIZE);
8903
- const vecs = await caps.embedTexts(batch, false);
8904
- results.push(...vecs);
8822
+ return output;
8823
+ }
8824
+ function extractFrames(query) {
8825
+ const frames = [];
8826
+ const seen = /* @__PURE__ */ new Set();
8827
+ for (const pattern of FRAME_PATTERNS) {
8828
+ pattern.lastIndex = 0;
8829
+ let m;
8830
+ while (m = pattern.exec(query)) {
8831
+ const raw2 = m[1].replace(/^\.\//, "").replace(/^\/+/, "");
8832
+ const line = Number.parseInt(m[2], 10);
8833
+ const key = `${raw2}:${line}`;
8834
+ if (!seen.has(key)) {
8835
+ seen.add(key);
8836
+ frames.push({ path: raw2, line });
8837
+ }
8905
8838
  }
8906
- embeddings = results;
8907
- } catch (err) {
8908
- console.error(`[LENS] Vocab clusters: embed failed (${terms.length} terms):`, err.message);
8909
- return;
8910
8839
  }
8911
- const rawClusters = agglomerativeCluster(terms, embeddings);
8912
- console.error(`[LENS] Vocab clusters: ${rawClusters.length} clusters from ${terms.length} terms`);
8913
- const clusters = rawClusters.map((indices) => {
8914
- const clusterTerms = indices.map((i) => terms[i]);
8915
- const fileSet = /* @__PURE__ */ new Set();
8916
- for (const t of clusterTerms) {
8917
- const files = termToFiles.get(t);
8918
- if (files) for (const f of files) fileSet.add(f);
8919
- }
8920
- return { terms: clusterTerms, files: [...fileSet].sort() };
8921
- }).filter((c) => c.files.length > 0).sort((a, b) => b.files.length - a.files.length).slice(0, MAX_CLUSTERS);
8922
- repoQueries.updateVocabClusters(db, repoId, clusters, repo.last_indexed_commit ?? void 0);
8840
+ return frames;
8923
8841
  }
8924
- async function handleFileChange(db, repoId, repoRoot, absPath) {
8925
- const relPath = relative3(repoRoot, absPath);
8926
- if (isBinaryExt(relPath)) return;
8927
- try {
8928
- const s = await stat22(absPath);
8929
- if (s.size > MAX_FILE_SIZE) return;
8930
- } catch {
8931
- return;
8842
+ function tokenize(query) {
8843
+ return query.toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
8844
+ }
8845
+ function parseQuery(query) {
8846
+ const naturalTokens = tokenize(query);
8847
+ const frames = extractFrames(query);
8848
+ if (frames.length >= 2) {
8849
+ return { kind: "stack_trace", raw: query, frames, symbol: null, errorToken: null, naturalTokens };
8932
8850
  }
8933
- let content;
8934
- try {
8935
- content = await readFile2(absPath, "utf-8");
8936
- } catch {
8937
- return;
8851
+ const trimmed = query.trim();
8852
+ if (SYMBOL_RE.test(trimmed) && trimmed.length >= 4 && CASING_BOUNDARY.test(trimmed)) {
8853
+ return { kind: "symbol", raw: query, frames: [], symbol: trimmed, errorToken: null, naturalTokens };
8938
8854
  }
8939
- const language = detectLanguage(relPath);
8940
- const newChunks = chunkFile(content, DEFAULT_CHUNKING_PARAMS);
8941
- const existing = /* @__PURE__ */ new Map();
8942
- for (const row of chunkQueries.getByRepoPath(db, repoId, relPath)) {
8943
- existing.set(`${row.chunk_index}:${row.chunk_hash}`, row.id);
8855
+ const errorCode = query.match(ERROR_CODE_RE)?.[0];
8856
+ const errorSuffix = query.match(ERROR_SUFFIX_RE)?.[0];
8857
+ const errorPrefix = query.match(ERROR_PREFIX_RE)?.[1];
8858
+ const errorToken = errorCode || errorSuffix || errorPrefix || null;
8859
+ if (errorToken) {
8860
+ return { kind: "error_message", raw: query, frames, symbol: null, errorToken, naturalTokens };
8944
8861
  }
8945
- const newKeys = /* @__PURE__ */ new Set();
8946
- for (const chunk of newChunks) {
8947
- const key = `${chunk.chunk_index}:${chunk.chunk_hash}`;
8948
- newKeys.add(key);
8949
- if (existing.has(key)) {
8950
- chunkQueries.updateLastSeen(db, existing.get(key), repoId, "watcher");
8951
- } else {
8952
- chunkQueries.upsert(
8953
- db,
8954
- repoId,
8955
- relPath,
8956
- chunk.chunk_index,
8957
- chunk.start_line,
8958
- chunk.end_line,
8959
- chunk.content,
8960
- chunk.chunk_hash,
8961
- "watcher",
8962
- language
8963
- );
8964
- }
8965
- }
8966
- for (const [key, id] of existing) {
8967
- if (!newKeys.has(key)) chunkQueries.deleteById(db, id, repoId);
8968
- }
8969
- const entry = watchers.get(repoId);
8970
- if (entry) entry.changedFiles++;
8971
- }
8972
- async function handleFileDelete(db, repoId, repoRoot, absPath) {
8973
- const relPath = relative3(repoRoot, absPath);
8974
- if (isBinaryExt(relPath)) return;
8975
- chunkQueries.deleteByRepoPath(db, repoId, relPath);
8976
- const entry = watchers.get(repoId);
8977
- if (entry) entry.deletedFiles++;
8978
- }
8979
- function debouncedHandler(key, fn) {
8980
- const existing = debounceTimers.get(key);
8981
- if (existing) clearTimeout(existing);
8982
- debounceTimers.set(
8983
- key,
8984
- setTimeout(() => {
8985
- debounceTimers.delete(key);
8986
- fn().catch(() => {
8987
- });
8988
- }, DEBOUNCE_MS)
8989
- );
8990
- }
8991
- function startWatcher(db, repoId, rootPath) {
8992
- if (watchers.has(repoId)) return { started: false, already_watching: true };
8993
- try {
8994
- const fsWatcher = watch(rootPath, {
8995
- ignored: IGNORED,
8996
- ignoreInitial: true,
8997
- awaitWriteFinish: { stabilityThreshold: 200 },
8998
- persistent: false,
8999
- usePolling: false,
9000
- depth: 5
9001
- });
9002
- const entry = {
9003
- watcher: fsWatcher,
9004
- repoRoot: rootPath,
9005
- repoId,
9006
- changedFiles: 0,
9007
- deletedFiles: 0,
9008
- startedAt: /* @__PURE__ */ new Date()
9009
- };
9010
- watchers.set(repoId, entry);
9011
- fsWatcher.on(
9012
- "add",
9013
- (path) => debouncedHandler(`${repoId}:${path}`, () => handleFileChange(db, repoId, rootPath, path))
9014
- );
9015
- fsWatcher.on(
9016
- "change",
9017
- (path) => debouncedHandler(`${repoId}:${path}`, () => handleFileChange(db, repoId, rootPath, path))
9018
- );
9019
- fsWatcher.on(
9020
- "unlink",
9021
- (path) => debouncedHandler(`${repoId}:${path}`, () => handleFileDelete(db, repoId, rootPath, path))
9022
- );
9023
- fsWatcher.on("error", () => {
9024
- stopWatcher(repoId).catch(() => {
9025
- });
9026
- });
9027
- return { started: true, already_watching: false };
9028
- } catch {
9029
- return { started: false, already_watching: false };
9030
- }
9031
- }
9032
- async function stopWatcher(repoId) {
9033
- const entry = watchers.get(repoId);
9034
- if (!entry) return { stopped: false };
9035
- await entry.watcher.close();
9036
- watchers.delete(repoId);
9037
- for (const [key, timer] of debounceTimers) {
9038
- if (key.startsWith(`${repoId}:`)) {
9039
- clearTimeout(timer);
9040
- debounceTimers.delete(key);
9041
- }
9042
- }
9043
- return { stopped: true };
9044
- }
9045
- function getWatcherStatus(repoId) {
9046
- const entry = watchers.get(repoId);
9047
- if (!entry) return { watching: false, repo_root: null, changed_files: 0, deleted_files: 0, started_at: null };
9048
- return {
9049
- watching: true,
9050
- repo_root: entry.repoRoot,
9051
- changed_files: entry.changedFiles,
9052
- deleted_files: entry.deletedFiles,
9053
- started_at: entry.startedAt.toISOString()
9054
- };
9055
- }
9056
- function basename3(p) {
9057
- const i = p.lastIndexOf("/");
9058
- return i >= 0 ? p.substring(i + 1) : p;
9059
- }
9060
- function daysAgo(d) {
9061
- if (!d) return "unknown";
9062
- const days = Math.round((Date.now() - d.getTime()) / 864e5);
9063
- if (days === 0) return "today";
9064
- if (days === 1) return "1d ago";
9065
- return `${days}d ago`;
9066
- }
9067
- function formatContextPack(data) {
9068
- const L = [];
9069
- const files = data.files.slice(0, 15);
9070
- const exportsMap = /* @__PURE__ */ new Map();
9071
- for (const m of data.metadata) {
9072
- if (m.exports?.length) exportsMap.set(m.path, m.exports);
9073
- }
9074
- L.push(`# ${data.goal}`);
9075
- L.push("");
9076
- if (files.length > 0) {
9077
- L.push("## Files");
9078
- for (let i = 0; i < files.length; i++) {
9079
- const f = files[i];
9080
- const exports = exportsMap.get(f.path);
9081
- const exportStr = exports?.length ? ` \u2014 exports: ${exports.slice(0, 5).join(", ")}` : "";
9082
- L.push(`${i + 1}. ${f.path}${exportStr}`);
9083
- }
9084
- L.push("");
9085
- }
9086
- const depLines = [];
9087
- for (const f of files.slice(0, 5)) {
9088
- const fwd = data.forwardImports.get(f.path);
9089
- const rev = data.reverseImports.get(f.path);
9090
- const hop2 = data.hop2Deps.get(f.path);
9091
- if (!fwd?.length && !rev?.length) continue;
9092
- depLines.push(f.path);
9093
- if (fwd?.length) {
9094
- depLines.push(` imports: ${fwd.slice(0, 5).map(basename3).join(", ")}`);
9095
- }
9096
- if (rev?.length) {
9097
- let revStr = rev.slice(0, 4).map(basename3).join(", ");
9098
- if (hop2?.length) {
9099
- revStr += ` \u2192 ${hop2.slice(0, 3).map(basename3).join(", ")}`;
9100
- }
9101
- depLines.push(` imported by: ${revStr}`);
9102
- }
9103
- }
9104
- if (depLines.length > 0) {
9105
- L.push("## Dependency Graph");
9106
- for (const line of depLines) L.push(line);
9107
- L.push("");
9108
- }
9109
- if (data.cochanges.length > 0) {
9110
- const clusters = buildClusters(data.cochanges);
9111
- if (clusters.length > 0) {
9112
- L.push("## Co-change Clusters");
9113
- for (const c of clusters) {
9114
- L.push(`[${c.members.map(basename3).join(", ")}] \u2014 ${c.count} co-commits`);
9115
- }
9116
- L.push("");
9117
- }
9118
- }
9119
- const activityLines = [];
9120
- for (const f of files.slice(0, 5)) {
9121
- const stat32 = data.fileStats.get(f.path);
9122
- if (!stat32 || stat32.commit_count === 0) continue;
9123
- activityLines.push(
9124
- `${basename3(f.path)}: ${stat32.commit_count} commits, ${stat32.recent_count}/90d, last: ${daysAgo(stat32.last_modified)}`
9125
- );
9126
- }
9127
- if (activityLines.length > 0) {
9128
- L.push("## Activity");
9129
- for (const line of activityLines) L.push(line);
9130
- }
9131
- return L.join("\n");
9132
- }
9133
- function buildClusters(cochanges) {
9134
- const clusters = [];
9135
- for (const cc of cochanges) {
9136
- let merged = false;
9137
- for (const c of clusters) {
9138
- if (c.members.includes(cc.path) || c.members.includes(cc.partner)) {
9139
- if (!c.members.includes(cc.path)) c.members.push(cc.path);
9140
- if (!c.members.includes(cc.partner)) c.members.push(cc.partner);
9141
- c.count = Math.max(c.count, cc.count);
9142
- merged = true;
9143
- break;
9144
- }
9145
- }
9146
- if (!merged) {
9147
- clusters.push({ members: [cc.path, cc.partner], count: cc.count });
9148
- }
9149
- }
9150
- return clusters.filter((c) => c.count >= 2).sort((a, b) => b.count - a.count).slice(0, 5);
8862
+ return { kind: "natural", raw: query, frames, symbol: null, errorToken: null, naturalTokens };
9151
8863
  }
9152
8864
  function stem(word) {
9153
8865
  if (word.length <= 4) return word;
@@ -9155,6 +8867,7 @@ function stem(word) {
9155
8867
  }
9156
8868
  function expandKeywords(words, clusters) {
9157
8869
  const exact = /* @__PURE__ */ new Set();
8870
+ const expanded = /* @__PURE__ */ new Set();
9158
8871
  const stemmed = /* @__PURE__ */ new Set();
9159
8872
  const clusterFiles = /* @__PURE__ */ new Set();
9160
8873
  for (const w of words) {
@@ -9174,21 +8887,26 @@ function expandKeywords(words, clusters) {
9174
8887
  for (const w of words) {
9175
8888
  const synonyms = CONCEPT_SYNONYMS[w];
9176
8889
  if (synonyms) {
9177
- for (const syn of synonyms) exact.add(syn);
8890
+ for (const syn of synonyms) {
8891
+ if (!exact.has(syn)) expanded.add(syn);
8892
+ }
9178
8893
  }
9179
8894
  }
9180
8895
  if (clusters) {
9181
8896
  for (const w of words) {
9182
8897
  for (const cluster of clusters) {
9183
8898
  if (cluster.terms.includes(w) || cluster.terms.some((t) => stem(t) === stem(w))) {
9184
- for (const t of cluster.terms) exact.add(t);
8899
+ for (const t of cluster.terms) {
8900
+ if (!exact.has(t)) expanded.add(t);
8901
+ }
9185
8902
  for (const f of cluster.files) clusterFiles.add(f);
9186
8903
  }
9187
8904
  }
9188
8905
  }
9189
8906
  }
9190
8907
  for (const e of exact) stemmed.delete(e);
9191
- return { exact: [...exact], stemmed: [...stemmed], clusterFiles };
8908
+ for (const e of expanded) stemmed.delete(e);
8909
+ return { exact: [...exact], expanded, stemmed: [...stemmed], clusterFiles };
9192
8910
  }
9193
8911
  function isNoisePath(path) {
9194
8912
  const lower = path.toLowerCase();
@@ -9212,10 +8930,10 @@ function buildTermWeights(words, metadata) {
9212
8930
  }
9213
8931
  return weights;
9214
8932
  }
9215
- function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, maxImportDepth) {
8933
+ function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, maxImportDepth, parsed) {
9216
8934
  const rawWords = query.toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS2.has(w));
9217
- const { exact, stemmed, clusterFiles } = expandKeywords(rawWords, vocabClusters ?? null);
9218
- const allTerms = [...exact, ...stemmed];
8935
+ const { exact, expanded, stemmed, clusterFiles } = expandKeywords(rawWords, vocabClusters ?? null);
8936
+ const allTerms = [...exact, ...expanded, ...stemmed];
9219
8937
  const termWeights = buildTermWeights(allTerms, metadata);
9220
8938
  const exportTokensMap = /* @__PURE__ */ new Map();
9221
8939
  for (const f of metadata) {
@@ -9242,6 +8960,32 @@ function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, m
9242
8960
  const dirTokens = pathSegments.slice(-4, -1).flatMap((s) => s.replace(/\./g, " ").split(/\s+/)).filter((t) => t.length >= 2);
9243
8961
  const pathTokenSet = /* @__PURE__ */ new Set([...fileTokens, ...dirTokens]);
9244
8962
  const expTokens = exportTokensMap.get(f.path);
8963
+ if (parsed) {
8964
+ if (parsed.kind === "stack_trace") {
8965
+ for (const frame of parsed.frames) {
8966
+ if (f.path.endsWith(frame.path) || frame.path.endsWith(f.path)) {
8967
+ score += 50;
8968
+ }
8969
+ }
8970
+ } else if (parsed.kind === "symbol" && parsed.symbol) {
8971
+ const sym = parsed.symbol;
8972
+ if ((f.exports ?? []).includes(sym)) score += 100;
8973
+ else if ((f.internals ?? []).includes(sym)) score += 80;
8974
+ } else if (parsed.kind === "error_message") {
8975
+ if (parsed.errorToken) {
8976
+ const token = parsed.errorToken.toLowerCase();
8977
+ if (exportsLower.includes(token) || internalsLower.includes(token) || pathLower.includes(token)) {
8978
+ score += 30;
8979
+ }
8980
+ }
8981
+ const rawError = parsed.raw.replace(/^(Error|TypeError|ReferenceError|SyntaxError|RangeError|FATAL|WARN|ERR):\s*/i, "").toLowerCase().trim();
8982
+ if (rawError.length >= 5) {
8983
+ if (docLower.includes(rawError) || sectionsLower.includes(rawError) || internalsLower.includes(rawError) || purposeLower.includes(rawError)) {
8984
+ score += 40;
8985
+ }
8986
+ }
8987
+ }
8988
+ }
9245
8989
  for (const w of exact) {
9246
8990
  const weight = termWeights.get(w) ?? 1;
9247
8991
  let termScore = 0;
@@ -9255,6 +8999,16 @@ function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, m
9255
8999
  if (termScore > 0) matchedTerms++;
9256
9000
  score += termScore;
9257
9001
  }
9002
+ for (const w of expanded) {
9003
+ const weight = (termWeights.get(w) ?? 1) * 0.5;
9004
+ let termScore = 0;
9005
+ if (expTokens?.has(w)) termScore += 2 * weight;
9006
+ else if (exportsLower.includes(w)) termScore += 1.5 * weight;
9007
+ if (docLower.includes(w) || purposeLower.includes(w)) termScore += 1 * weight;
9008
+ if (internalsLower.includes(w)) termScore += 1 * weight;
9009
+ if (termScore > 0) matchedTerms++;
9010
+ score += termScore;
9011
+ }
9258
9012
  for (const w of stemmed) {
9259
9013
  const weight = termWeights.get(w) ?? 1;
9260
9014
  let termScore = 0;
@@ -9270,7 +9024,7 @@ function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, m
9270
9024
  score *= 1 + coverage * coverage;
9271
9025
  }
9272
9026
  if (score > 0 && isNoisePath(f.path)) {
9273
- score *= 0.3;
9027
+ score = 0;
9274
9028
  }
9275
9029
  const exportCount = (f.exports ?? []).length;
9276
9030
  if (exportCount > 5 && score > 0) {
@@ -9304,14 +9058,27 @@ function interpretQuery(query, metadata, fileStats2, vocabClusters, indegrees, m
9304
9058
  deduped.push(f);
9305
9059
  if (deduped.length >= fileCap) break;
9306
9060
  }
9061
+ const scores = /* @__PURE__ */ new Map();
9062
+ for (const f of deduped) scores.set(f.path, f.score);
9307
9063
  return {
9308
9064
  fileCap,
9065
+ scores,
9309
9066
  files: deduped.map((f) => {
9310
- const reason = f.docstring?.slice(0, 80) || (f.exports?.length ? `exports: ${f.exports.slice(0, 4).join(", ")}` : "path match");
9067
+ const reason = truncateAtWord(f.docstring, 80) || formatExports(f.exports) || f.path.split("/").pop() || "";
9311
9068
  return { path: f.path, reason };
9312
9069
  })
9313
9070
  };
9314
9071
  }
9072
+ function truncateAtWord(s, max) {
9073
+ if (!s) return "";
9074
+ if (s.length <= max) return s;
9075
+ const cut = s.lastIndexOf(" ", max);
9076
+ return cut > max * 0.5 ? `${s.slice(0, cut)}...` : `${s.slice(0, max)}...`;
9077
+ }
9078
+ function formatExports(exports) {
9079
+ if (!exports?.length) return "";
9080
+ return `exports: ${exports.slice(0, 4).join(", ")}`;
9081
+ }
9315
9082
  function siblingKey(path) {
9316
9083
  const lastSlash = path.lastIndexOf("/");
9317
9084
  const dir = lastSlash >= 0 ? path.substring(0, lastSlash) : "";
@@ -9320,6 +9087,99 @@ function siblingKey(path) {
9320
9087
  const tokens = stem2.split(/[-.]/).slice(0, 3).join("-");
9321
9088
  return `${dir}/${tokens}`;
9322
9089
  }
9090
+ function findSymbolLine(symbol, chunkRows) {
9091
+ const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9092
+ const re = new RegExp(`\\b${escaped}\\b`);
9093
+ for (const chunk of chunkRows) {
9094
+ const lines = chunk.content.split("\n");
9095
+ for (let i = 0; i < lines.length; i++) {
9096
+ if (re.test(lines[i]) && SYMBOL_DEF_RE.test(lines[i])) {
9097
+ return chunk.start_line + i;
9098
+ }
9099
+ }
9100
+ }
9101
+ for (const chunk of chunkRows) {
9102
+ const lines = chunk.content.split("\n");
9103
+ for (let i = 0; i < lines.length; i++) {
9104
+ if (re.test(lines[i])) {
9105
+ return chunk.start_line + i;
9106
+ }
9107
+ }
9108
+ }
9109
+ return null;
9110
+ }
9111
+ function pickBestExport(exports, queryTokens) {
9112
+ if (exports.length <= 1 || !queryTokens.length) return exports[0];
9113
+ let best = exports[0];
9114
+ let bestScore = 0;
9115
+ for (const exp of exports) {
9116
+ const lower = exp.toLowerCase();
9117
+ const expTokens = lower.replace(/([a-z])([A-Z])/g, "$1 $2").split(/[\s_-]+/);
9118
+ let score = 0;
9119
+ for (const qt of queryTokens) {
9120
+ if (lower.includes(qt)) score += 2;
9121
+ else if (expTokens.some((t) => t.includes(qt) || qt.includes(t))) score++;
9122
+ }
9123
+ if (score > bestScore) {
9124
+ bestScore = score;
9125
+ best = exp;
9126
+ }
9127
+ }
9128
+ return best;
9129
+ }
9130
+ function resolveSnippets(db, repoId, files, metadata, parsed, limit = 5) {
9131
+ const result = /* @__PURE__ */ new Map();
9132
+ const topFiles = files.slice(0, limit);
9133
+ const topPaths = topFiles.map((f) => f.path);
9134
+ const allChunks = chunkQueries.getByRepoPaths(db, repoId, topPaths);
9135
+ const chunksByPath = /* @__PURE__ */ new Map();
9136
+ for (const c of allChunks) {
9137
+ const arr = chunksByPath.get(c.path) ?? [];
9138
+ arr.push({ start_line: c.start_line, content: c.content });
9139
+ chunksByPath.set(c.path, arr);
9140
+ }
9141
+ const metaMap = /* @__PURE__ */ new Map();
9142
+ for (const m of metadata) metaMap.set(m.path, m);
9143
+ const frameByPath = /* @__PURE__ */ new Map();
9144
+ for (const frame of parsed.frames) {
9145
+ for (const p of topPaths) {
9146
+ if (p.endsWith(frame.path) || frame.path.endsWith(p)) {
9147
+ frameByPath.set(p, frame.line);
9148
+ }
9149
+ }
9150
+ }
9151
+ for (const f of topFiles) {
9152
+ const chunks2 = chunksByPath.get(f.path) ?? [];
9153
+ const meta = metaMap.get(f.path);
9154
+ const frameLine = frameByPath.get(f.path);
9155
+ if (frameLine !== void 0) {
9156
+ result.set(f.path, { path: f.path, symbol: null, line: frameLine, matchKind: "frame" });
9157
+ continue;
9158
+ }
9159
+ if (parsed.symbol) {
9160
+ const exports = meta?.exports ?? [];
9161
+ const internals = meta?.internals ?? [];
9162
+ if (exports.includes(parsed.symbol)) {
9163
+ const line = findSymbolLine(parsed.symbol, chunks2);
9164
+ result.set(f.path, { path: f.path, symbol: parsed.symbol, line, matchKind: "export" });
9165
+ continue;
9166
+ }
9167
+ if (internals.includes(parsed.symbol)) {
9168
+ const line = findSymbolLine(parsed.symbol, chunks2);
9169
+ result.set(f.path, { path: f.path, symbol: parsed.symbol, line, matchKind: "internal" });
9170
+ continue;
9171
+ }
9172
+ }
9173
+ if (meta?.exports?.length) {
9174
+ const bestExport = pickBestExport(meta.exports, parsed.naturalTokens);
9175
+ const line = findSymbolLine(bestExport, chunks2);
9176
+ result.set(f.path, { path: f.path, symbol: bestExport, line, matchKind: "export" });
9177
+ continue;
9178
+ }
9179
+ result.set(f.path, { path: f.path, symbol: null, line: null, matchKind: null });
9180
+ }
9181
+ return result;
9182
+ }
9323
9183
  function loadFileMetadata(db, repoId) {
9324
9184
  return metadataQueries.getByRepo(db, repoId);
9325
9185
  }
@@ -9382,12 +9242,43 @@ function getIndegrees(db, repoId) {
9382
9242
  function getCochangePartners(db, repoId, paths, minCount = 5, limit = 10) {
9383
9243
  return cochangeQueries.getPartners(db, repoId, paths, minCount, limit);
9384
9244
  }
9245
+ function discoverTestFiles(reverseImports, cochanges, metadataPaths, sourcePaths) {
9246
+ const result = /* @__PURE__ */ new Map();
9247
+ for (const src of sourcePaths) {
9248
+ const tests = /* @__PURE__ */ new Set();
9249
+ const importers = reverseImports.get(src) ?? [];
9250
+ for (const imp of importers) {
9251
+ if (TEST_PATTERN.test(imp)) tests.add(imp);
9252
+ }
9253
+ for (const cc of cochanges) {
9254
+ const partner = cc.path === src ? cc.partner : cc.partner === src ? cc.path : null;
9255
+ if (partner && TEST_PATTERN.test(partner)) tests.add(partner);
9256
+ }
9257
+ const ext = src.slice(src.lastIndexOf("."));
9258
+ const base = src.slice(0, src.lastIndexOf("."));
9259
+ const dir = src.slice(0, src.lastIndexOf("/"));
9260
+ const name = src.slice(src.lastIndexOf("/") + 1, src.lastIndexOf("."));
9261
+ const candidates = [
9262
+ `${base}.test${ext}`,
9263
+ `${base}.spec${ext}`,
9264
+ `${dir}/__tests__/${name}${ext}`,
9265
+ `${dir}/__tests__/${name}.test${ext}`
9266
+ ];
9267
+ for (const c of candidates) {
9268
+ if (metadataPaths.has(c)) tests.add(c);
9269
+ }
9270
+ if (tests.size > 0) {
9271
+ result.set(src, [...tests].slice(0, 3));
9272
+ }
9273
+ }
9274
+ return result;
9275
+ }
9385
9276
  function loadVocabClusters(db, repoId) {
9386
9277
  const repo = repoQueries.getById(db, repoId);
9387
9278
  if (!repo?.vocab_clusters) return null;
9388
9279
  return jsonParse(repo.vocab_clusters, null);
9389
9280
  }
9390
- function cosine2(a, b) {
9281
+ function cosine(a, b) {
9391
9282
  let dot = 0, na = 0, nb = 0;
9392
9283
  for (let i = 0; i < a.length; i++) {
9393
9284
  dot += a[i] * b[i];
@@ -9410,7 +9301,7 @@ async function vectorSearch(db, repoId, query, limit, caps, codeOnly = true) {
9410
9301
  end_line: c.end_line,
9411
9302
  content: c.content,
9412
9303
  language: c.language,
9413
- score: cosine2(queryArr, c.embedding)
9304
+ score: cosine(queryArr, c.embedding)
9414
9305
  })).filter((c) => !codeOnly || !isDocFile(c.path)).sort((a, b) => b.score - a.score).slice(0, limit);
9415
9306
  return scored;
9416
9307
  }
@@ -9442,14 +9333,21 @@ async function buildContext(db, repoId, goal, caps, trace, options) {
9442
9333
  const repo = repoQueries.getById(db, repoId);
9443
9334
  const commit = repo?.last_indexed_commit ?? null;
9444
9335
  const useEmb = options?.useEmbeddings !== false;
9445
- const key = cacheKey(repoId, goal, commit, useEmb);
9446
- const cached2 = cacheGet(key);
9447
- if (cached2) {
9448
- trace?.add("cache", 0, "HIT");
9449
- track(db, "context", { duration_ms: Date.now() - start, result_count: cached2.stats.files_in_context, cache_hit: true });
9450
- return { ...cached2, stats: { ...cached2.stats, cached: true, duration_ms: Date.now() - start } };
9336
+ if (!options?.skipCache) {
9337
+ const key = cacheKey(repoId, goal, commit, useEmb);
9338
+ const cached2 = cacheGet(key);
9339
+ if (cached2) {
9340
+ trace?.add("cache", 0, "HIT");
9341
+ track(db, "context", {
9342
+ duration_ms: Date.now() - start,
9343
+ result_count: cached2.stats.files_in_context,
9344
+ cache_hit: true
9345
+ });
9346
+ return { ...cached2, stats: { ...cached2.stats, cached: true, duration_ms: Date.now() - start } };
9347
+ }
9451
9348
  }
9452
9349
  const embAvailable = useEmb && (await Promise.resolve().then(() => (init_queries(), queries_exports))).chunkQueries.hasEmbeddings(db, repoId);
9350
+ const parsed = parseQuery(goal);
9453
9351
  trace?.step("loadStructural");
9454
9352
  const metadata = loadFileMetadata(db, repoId);
9455
9353
  const allStats = getAllFileStats(db, repoId);
@@ -9465,11 +9363,44 @@ async function buildContext(db, repoId, goal, caps, trace, options) {
9465
9363
  statsForInterpreter.set(path, { commit_count: stat32.commit_count, recent_count: stat32.recent_count });
9466
9364
  }
9467
9365
  trace?.step("interpretQuery");
9468
- const interpreted = interpretQuery(goal, metadata, statsForInterpreter, vocabClusters, indegreeMap, maxImportDepth);
9469
- trace?.end("interpretQuery", `${interpreted.files.length} files`);
9366
+ const interpreted = interpretQuery(
9367
+ goal,
9368
+ metadata,
9369
+ statsForInterpreter,
9370
+ vocabClusters,
9371
+ indegreeMap,
9372
+ maxImportDepth,
9373
+ parsed
9374
+ );
9375
+ trace?.end("interpretQuery", `${interpreted.files.length} files (${parsed.kind})`);
9376
+ if (parsed.kind === "error_message") {
9377
+ trace?.step("errorContentSearch");
9378
+ const rawError = parsed.raw.replace(/^(Error|TypeError|ReferenceError|SyntaxError|RangeError|FATAL|WARN|ERR):\s*/i, "").trim();
9379
+ if (rawError.length >= 5) {
9380
+ const contentHits = chunkQueries.searchContent(db, repoId, rawError).filter((p) => !isNoisePath(p));
9381
+ const scored = contentHits.filter((p) => (interpreted.scores.get(p) ?? 0) > 0);
9382
+ const unscored = contentHits.filter((p) => (interpreted.scores.get(p) ?? 0) === 0);
9383
+ const capped = [...scored.slice(0, 3), ...unscored.slice(0, Math.max(0, 3 - scored.length))];
9384
+ const existingSet = new Set(interpreted.files.map((f) => f.path));
9385
+ for (const p of capped) {
9386
+ const currentScore = interpreted.scores.get(p) ?? 0;
9387
+ interpreted.scores.set(p, currentScore + 40);
9388
+ if (!existingSet.has(p)) {
9389
+ const meta = metadata.find((m) => m.path === p);
9390
+ interpreted.files.push({
9391
+ path: p,
9392
+ reason: meta?.docstring?.slice(0, 80) || `contains "${rawError.slice(0, 30)}"`
9393
+ });
9394
+ existingSet.add(p);
9395
+ }
9396
+ }
9397
+ interpreted.files.sort((a, b) => (interpreted.scores.get(b.path) ?? 0) - (interpreted.scores.get(a.path) ?? 0));
9398
+ }
9399
+ trace?.end("errorContentSearch");
9400
+ }
9470
9401
  trace?.step("cochangePromotion");
9471
9402
  const topForCochange = interpreted.files.slice(0, 5).map((f) => f.path);
9472
- const cochangePartners = getCochangePartners(db, repoId, topForCochange, 3, 20);
9403
+ const cochangePartners = getCochangePartners(db, repoId, topForCochange, 5, 15);
9473
9404
  const existingPathSet = new Set(interpreted.files.map((f) => f.path));
9474
9405
  let promoted = 0;
9475
9406
  for (const cp of cochangePartners) {
@@ -9549,6 +9480,20 @@ async function buildContext(db, repoId, goal, caps, trace, options) {
9549
9480
  }
9550
9481
  }
9551
9482
  trace?.end("structuralEnrichment");
9483
+ trace?.step("resolveSnippets");
9484
+ const snippets = resolveSnippets(db, repoId, interpreted.files, metadata, parsed, 5);
9485
+ const metadataPaths = new Set(metadata.map((m) => m.path));
9486
+ const testFiles = discoverTestFiles(
9487
+ reverseImports,
9488
+ cochanges,
9489
+ metadataPaths,
9490
+ interpreted.files.slice(0, 5).map((f) => f.path)
9491
+ );
9492
+ trace?.end("resolveSnippets", `${snippets.size} snippets, ${testFiles.size} test maps`);
9493
+ trace?.step("sliceContext");
9494
+ const { sliceContext: sliceCtx } = await Promise.resolve().then(() => (init_slicer(), slicer_exports));
9495
+ const slices = sliceCtx(db, repoId, snippets, parsed.kind);
9496
+ trace?.end("sliceContext", `${slices.size} slices`);
9552
9497
  trace?.step("formatContextPack");
9553
9498
  const data = {
9554
9499
  goal,
@@ -9558,7 +9503,12 @@ async function buildContext(db, repoId, goal, caps, trace, options) {
9558
9503
  forwardImports,
9559
9504
  hop2Deps,
9560
9505
  cochanges,
9561
- fileStats: allStats
9506
+ fileStats: allStats,
9507
+ scores: interpreted.scores,
9508
+ snippets,
9509
+ slices,
9510
+ testFiles,
9511
+ queryKind: parsed.kind
9562
9512
  };
9563
9513
  const contextPack = formatContextPack(data);
9564
9514
  trace?.end("formatContextPack");
@@ -9571,19 +9521,742 @@ async function buildContext(db, repoId, goal, caps, trace, options) {
9571
9521
  cached: false
9572
9522
  }
9573
9523
  };
9574
- cacheSet(key, response);
9575
- track(db, "context", { duration_ms: response.stats.duration_ms, result_count: response.stats.files_in_context, cache_hit: false });
9576
- return response;
9524
+ if (options?.includeRankedFiles) {
9525
+ response.ranked_files = interpreted.files.map((f) => ({
9526
+ path: f.path,
9527
+ reason: f.reason,
9528
+ score: interpreted.scores.get(f.path) ?? 0
9529
+ }));
9530
+ response.query_kind = parsed.kind;
9531
+ }
9532
+ if (!options?.skipCache) {
9533
+ const key = cacheKey(repoId, goal, commit, useEmb);
9534
+ cacheSet(key, response);
9535
+ }
9536
+ track(db, "context", {
9537
+ duration_ms: response.stats.duration_ms,
9538
+ result_count: response.stats.files_in_context,
9539
+ cache_hit: false
9540
+ });
9541
+ return response;
9542
+ } catch {
9543
+ return {
9544
+ context_pack: `# ${goal}
9545
+
9546
+ Context generation failed.`,
9547
+ stats: { files_in_context: 0, index_fresh: false, duration_ms: Date.now() - start, cached: false }
9548
+ };
9549
+ }
9550
+ }
9551
+ function resolveDbPath(customPath) {
9552
+ if (customPath) return customPath;
9553
+ const dir = join3(homedir(), ".lens");
9554
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
9555
+ return join3(dir, "data.db");
9556
+ }
9557
+ function openDb(customPath) {
9558
+ if (_db) return _db;
9559
+ const dbPath = resolveDbPath(customPath);
9560
+ const sqlite = new Database(dbPath);
9561
+ sqlite.pragma("journal_mode = WAL");
9562
+ sqlite.pragma("busy_timeout = 5000");
9563
+ sqlite.pragma("foreign_keys = ON");
9564
+ const db = drizzle(sqlite, { schema: schema_exports });
9565
+ sqlite.exec(createTablesSql());
9566
+ const cols = new Set(sqlite.pragma("table_info(file_metadata)").map((c) => c.name));
9567
+ if (!cols.has("sections")) sqlite.exec("ALTER TABLE file_metadata ADD COLUMN sections TEXT DEFAULT '[]'");
9568
+ if (!cols.has("internals")) sqlite.exec("ALTER TABLE file_metadata ADD COLUMN internals TEXT DEFAULT '[]'");
9569
+ const repoCols = new Set(sqlite.pragma("table_info(repos)").map((c) => c.name));
9570
+ if (!repoCols.has("enable_embeddings"))
9571
+ sqlite.exec("ALTER TABLE repos ADD COLUMN enable_embeddings INTEGER NOT NULL DEFAULT 1");
9572
+ if (!repoCols.has("enable_summaries"))
9573
+ sqlite.exec("ALTER TABLE repos ADD COLUMN enable_summaries INTEGER NOT NULL DEFAULT 1");
9574
+ if (!repoCols.has("enable_vocab_clusters"))
9575
+ sqlite.exec("ALTER TABLE repos ADD COLUMN enable_vocab_clusters INTEGER NOT NULL DEFAULT 1");
9576
+ if (!repoCols.has("last_vocab_cluster_commit"))
9577
+ sqlite.exec("ALTER TABLE repos ADD COLUMN last_vocab_cluster_commit TEXT");
9578
+ const logCols = new Set(sqlite.pragma("table_info(request_logs)").map((c) => c.name));
9579
+ if (!logCols.has("response_body")) sqlite.exec("ALTER TABLE request_logs ADD COLUMN response_body TEXT");
9580
+ if (!logCols.has("trace")) sqlite.exec("ALTER TABLE request_logs ADD COLUMN trace TEXT");
9581
+ _db = db;
9582
+ _raw = sqlite;
9583
+ return db;
9584
+ }
9585
+ function getDb() {
9586
+ if (!_db) throw new Error("Database not initialized. Call openDb() first.");
9587
+ return _db;
9588
+ }
9589
+ function getRawDb() {
9590
+ if (!_raw) throw new Error("Database not initialized. Call openDb() first.");
9591
+ return _raw;
9592
+ }
9593
+ function closeDb() {
9594
+ if (_raw) _raw.close();
9595
+ _db = null;
9596
+ _raw = null;
9597
+ }
9598
+ function createTablesSql() {
9599
+ return `
9600
+ CREATE TABLE IF NOT EXISTS repos (
9601
+ id TEXT PRIMARY KEY,
9602
+ identity_key TEXT UNIQUE NOT NULL,
9603
+ name TEXT NOT NULL,
9604
+ root_path TEXT NOT NULL,
9605
+ remote_url TEXT,
9606
+ last_indexed_commit TEXT,
9607
+ index_status TEXT NOT NULL DEFAULT 'pending',
9608
+ last_indexed_at TEXT,
9609
+ last_git_analysis_commit TEXT,
9610
+ max_import_depth INTEGER DEFAULT 0,
9611
+ vocab_clusters TEXT,
9612
+ last_vocab_cluster_commit TEXT,
9613
+ enable_embeddings INTEGER NOT NULL DEFAULT 1,
9614
+ enable_summaries INTEGER NOT NULL DEFAULT 1,
9615
+ enable_vocab_clusters INTEGER NOT NULL DEFAULT 1,
9616
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
9617
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
9618
+ );
9619
+ CREATE INDEX IF NOT EXISTS idx_repos_identity ON repos(identity_key);
9620
+
9621
+ CREATE TABLE IF NOT EXISTS chunks (
9622
+ id TEXT PRIMARY KEY,
9623
+ repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
9624
+ path TEXT NOT NULL,
9625
+ chunk_index INTEGER NOT NULL,
9626
+ start_line INTEGER NOT NULL,
9627
+ end_line INTEGER NOT NULL,
9628
+ content TEXT NOT NULL,
9629
+ chunk_hash TEXT NOT NULL,
9630
+ last_seen_commit TEXT NOT NULL,
9631
+ language TEXT,
9632
+ embedding BLOB,
9633
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
9634
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
9635
+ UNIQUE(repo_id, path, chunk_index, chunk_hash)
9636
+ );
9637
+ CREATE INDEX IF NOT EXISTS idx_chunks_repo_path ON chunks(repo_id, path);
9638
+ CREATE INDEX IF NOT EXISTS idx_chunks_repo ON chunks(repo_id);
9639
+ CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(repo_id, chunk_hash);
9640
+
9641
+ CREATE TABLE IF NOT EXISTS file_metadata (
9642
+ id TEXT PRIMARY KEY,
9643
+ repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
9644
+ path TEXT NOT NULL,
9645
+ language TEXT,
9646
+ exports TEXT DEFAULT '[]',
9647
+ imports TEXT DEFAULT '[]',
9648
+ docstring TEXT DEFAULT '',
9649
+ sections TEXT DEFAULT '[]',
9650
+ internals TEXT DEFAULT '[]',
9651
+ purpose TEXT DEFAULT '',
9652
+ purpose_hash TEXT DEFAULT '',
9653
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
9654
+ UNIQUE(repo_id, path)
9655
+ );
9656
+ CREATE INDEX IF NOT EXISTS idx_file_metadata_repo ON file_metadata(repo_id);
9657
+
9658
+ CREATE TABLE IF NOT EXISTS file_imports (
9659
+ id TEXT PRIMARY KEY,
9660
+ repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
9661
+ source_path TEXT NOT NULL,
9662
+ target_path TEXT NOT NULL,
9663
+ UNIQUE(repo_id, source_path, target_path)
9664
+ );
9665
+ CREATE INDEX IF NOT EXISTS idx_file_imports_target ON file_imports(repo_id, target_path);
9666
+
9667
+ CREATE TABLE IF NOT EXISTS file_stats (
9668
+ id TEXT PRIMARY KEY,
9669
+ repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
9670
+ path TEXT NOT NULL,
9671
+ commit_count INTEGER NOT NULL DEFAULT 0,
9672
+ recent_count INTEGER NOT NULL DEFAULT 0,
9673
+ last_modified TEXT,
9674
+ UNIQUE(repo_id, path)
9675
+ );
9676
+
9677
+ CREATE TABLE IF NOT EXISTS file_cochanges (
9678
+ id TEXT PRIMARY KEY,
9679
+ repo_id TEXT NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
9680
+ path_a TEXT NOT NULL,
9681
+ path_b TEXT NOT NULL,
9682
+ cochange_count INTEGER NOT NULL DEFAULT 1,
9683
+ UNIQUE(repo_id, path_a, path_b)
9684
+ );
9685
+ CREATE INDEX IF NOT EXISTS idx_cochanges_lookup ON file_cochanges(repo_id, path_a);
9686
+
9687
+ CREATE TABLE IF NOT EXISTS usage_counters (
9688
+ id TEXT PRIMARY KEY,
9689
+ date TEXT NOT NULL UNIQUE,
9690
+ context_queries INTEGER NOT NULL DEFAULT 0,
9691
+ embedding_requests INTEGER NOT NULL DEFAULT 0,
9692
+ embedding_chunks INTEGER NOT NULL DEFAULT 0,
9693
+ purpose_requests INTEGER NOT NULL DEFAULT 0,
9694
+ repos_indexed INTEGER NOT NULL DEFAULT 0,
9695
+ synced_at TEXT,
9696
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
9697
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
9698
+ );
9699
+ CREATE INDEX IF NOT EXISTS idx_usage_counters_date ON usage_counters(date);
9700
+
9701
+ CREATE TABLE IF NOT EXISTS request_logs (
9702
+ id TEXT PRIMARY KEY,
9703
+ method TEXT NOT NULL,
9704
+ path TEXT NOT NULL,
9705
+ status INTEGER NOT NULL,
9706
+ duration_ms INTEGER NOT NULL,
9707
+ source TEXT NOT NULL DEFAULT 'api',
9708
+ request_body TEXT,
9709
+ response_size INTEGER,
9710
+ response_body TEXT,
9711
+ trace TEXT,
9712
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
9713
+ );
9714
+ CREATE INDEX IF NOT EXISTS idx_request_logs_created ON request_logs(created_at);
9715
+ CREATE INDEX IF NOT EXISTS idx_request_logs_source ON request_logs(source);
9716
+
9717
+ CREATE TABLE IF NOT EXISTS settings (
9718
+ key TEXT PRIMARY KEY,
9719
+ value TEXT NOT NULL,
9720
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
9721
+ );
9722
+
9723
+ CREATE TABLE IF NOT EXISTS telemetry_events (
9724
+ id TEXT PRIMARY KEY,
9725
+ event_type TEXT NOT NULL,
9726
+ event_data TEXT,
9727
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
9728
+ synced_at TEXT
9729
+ );
9730
+ CREATE INDEX IF NOT EXISTS idx_telemetry_events_type ON telemetry_events(event_type);
9731
+ CREATE INDEX IF NOT EXISTS idx_telemetry_events_synced ON telemetry_events(synced_at);
9732
+ `;
9733
+ }
9734
+ function hitAtN(expected, returned, n) {
9735
+ const top = returned.slice(0, n);
9736
+ return expected.some((e) => top.includes(e));
9737
+ }
9738
+ function entryHitAtN(entry, returned, n) {
9739
+ if (!entry) return false;
9740
+ return returned.slice(0, n).includes(entry);
9741
+ }
9742
+ function recallAtK(expected, returned, k) {
9743
+ if (expected.length === 0) return 1;
9744
+ const top = new Set(returned.slice(0, k));
9745
+ const hits = expected.filter((e) => top.has(e)).length;
9746
+ return hits / expected.length;
9747
+ }
9748
+ function computeMetrics(results) {
9749
+ const total = results.length;
9750
+ if (total === 0) {
9751
+ return {
9752
+ total: 0,
9753
+ hit_at_1: 0,
9754
+ hit_at_3: 0,
9755
+ entry_hit_at_1: 0,
9756
+ entry_hit_at_3: 0,
9757
+ avg_recall_at_5: 0,
9758
+ avg_duration_ms: 0,
9759
+ by_kind: [],
9760
+ results
9761
+ };
9762
+ }
9763
+ const hit1 = results.filter((r) => r.hit_at_1).length;
9764
+ const hit3 = results.filter((r) => r.hit_at_3).length;
9765
+ const entryHit1 = results.filter((r) => r.entry_hit_at_1).length;
9766
+ const entryHit3 = results.filter((r) => r.entry_hit_at_3).length;
9767
+ const sumRecall = results.reduce((s, r) => s + r.recall_at_5, 0);
9768
+ const sumDuration = results.reduce((s, r) => s + r.duration_ms, 0);
9769
+ const kinds = [...new Set(results.map((r) => r.kind))];
9770
+ const by_kind = kinds.map((kind) => {
9771
+ const group = results.filter((r) => r.kind === kind);
9772
+ const n = group.length;
9773
+ return {
9774
+ kind,
9775
+ count: n,
9776
+ hit_at_1: group.filter((r) => r.hit_at_1).length / n,
9777
+ hit_at_3: group.filter((r) => r.hit_at_3).length / n,
9778
+ entry_hit_at_1: group.filter((r) => r.entry_hit_at_1).length / n,
9779
+ entry_hit_at_3: group.filter((r) => r.entry_hit_at_3).length / n,
9780
+ avg_recall_at_5: group.reduce((s, r) => s + r.recall_at_5, 0) / n,
9781
+ avg_duration_ms: Math.round(group.reduce((s, r) => s + r.duration_ms, 0) / n)
9782
+ };
9783
+ });
9784
+ return {
9785
+ total,
9786
+ hit_at_1: hit1 / total,
9787
+ hit_at_3: hit3 / total,
9788
+ entry_hit_at_1: entryHit1 / total,
9789
+ entry_hit_at_3: entryHit3 / total,
9790
+ avg_recall_at_5: sumRecall / total,
9791
+ avg_duration_ms: Math.round(sumDuration / total),
9792
+ by_kind,
9793
+ results
9794
+ };
9795
+ }
9796
+ async function runEval(db, repoId, options) {
9797
+ let queries = GOLD_DATASET;
9798
+ if (options?.filterKind) {
9799
+ queries = queries.filter((q) => q.kind === options.filterKind);
9800
+ }
9801
+ const results = [];
9802
+ for (const gold of queries) {
9803
+ const start = Date.now();
9804
+ const response = await buildContext(db, repoId, gold.query, void 0, void 0, {
9805
+ useEmbeddings: false,
9806
+ includeRankedFiles: true,
9807
+ skipCache: true
9808
+ });
9809
+ const duration3 = Date.now() - start;
9810
+ const returned = (response.ranked_files ?? []).map((f) => f.path);
9811
+ results.push({
9812
+ id: gold.id,
9813
+ query: gold.query,
9814
+ kind: gold.kind,
9815
+ expected_files: gold.expected_files,
9816
+ expected_entry: gold.expected_entry,
9817
+ returned_files: returned,
9818
+ hit_at_1: hitAtN(gold.expected_files, returned, 1),
9819
+ hit_at_3: hitAtN(gold.expected_files, returned, 3),
9820
+ entry_hit_at_1: entryHitAtN(gold.expected_entry, returned, 1),
9821
+ entry_hit_at_3: entryHitAtN(gold.expected_entry, returned, 3),
9822
+ recall_at_5: recallAtK(gold.expected_files, returned, 5),
9823
+ duration_ms: duration3,
9824
+ file_count: returned.length
9825
+ });
9826
+ }
9827
+ return computeMetrics(results);
9828
+ }
9829
+ function estimateTokens2(text22) {
9830
+ return Math.ceil(text22.length / CHARS_PER_TOKEN);
9831
+ }
9832
+ async function ensureEmbedded(db, repoId, caps) {
9833
+ const start = Date.now();
9834
+ if (!caps?.embedTexts) {
9835
+ return { embedded_count: 0, skipped_count: 0, duration_ms: 0 };
9836
+ }
9837
+ let embedded = 0;
9838
+ let apiCalls = 0;
9839
+ while (apiCalls < MAX_API_CALLS) {
9840
+ const remaining = chunkQueries.countUnembedded(db, repoId);
9841
+ if (remaining === 0) break;
9842
+ const rows = chunkQueries.getUnembedded(db, repoId, POOL_SIZE);
9843
+ if (rows.length === 0) break;
9844
+ const validRows = rows.filter((r) => r.content.trim().length > 0);
9845
+ if (validRows.length === 0) {
9846
+ apiCalls++;
9847
+ continue;
9848
+ }
9849
+ let offset = 0;
9850
+ let poolFailed = false;
9851
+ while (offset < validRows.length && apiCalls < MAX_API_CALLS) {
9852
+ const batch = [];
9853
+ let batchTokens = 0;
9854
+ while (offset < validRows.length) {
9855
+ const est = estimateTokens2(validRows[offset].content);
9856
+ if (batch.length > 0 && batchTokens + est > MAX_BATCH_TOKENS) break;
9857
+ batch.push(validRows[offset]);
9858
+ batchTokens += est;
9859
+ offset++;
9860
+ }
9861
+ try {
9862
+ const texts = batch.map((r) => r.content);
9863
+ const vectors = await caps.embedTexts(texts);
9864
+ for (let i = 0; i < batch.length; i++) {
9865
+ chunkQueries.updateEmbedding(db, batch[i].id, repoId, vectors[i]);
9866
+ }
9867
+ embedded += batch.length;
9868
+ } catch (err) {
9869
+ console.error(
9870
+ `[LENS] Embed batch failed (${batch.length} chunks, ~${batchTokens} tokens):`,
9871
+ err.message
9872
+ );
9873
+ poolFailed = true;
9874
+ break;
9875
+ }
9876
+ apiCalls++;
9877
+ }
9878
+ if (poolFailed) break;
9879
+ }
9880
+ return { embedded_count: embedded, skipped_count: 0, duration_ms: Date.now() - start };
9881
+ }
9882
+ async function enrichPurpose(db, repoId, caps, fullRun = false) {
9883
+ const start = Date.now();
9884
+ if (!caps?.generatePurpose) {
9885
+ return { enriched: 0, skipped: 0, duration_ms: 0 };
9886
+ }
9887
+ let totalEnriched = 0;
9888
+ let totalSkipped = 0;
9889
+ while (true) {
9890
+ const candidates = metadataQueries.getCandidatesForPurpose(db, repoId, PURPOSE_BATCH_LIMIT);
9891
+ if (candidates.length === 0) break;
9892
+ for (let i = 0; i < candidates.length; i += PURPOSE_CONCURRENCY) {
9893
+ const batch = candidates.slice(i, i + PURPOSE_CONCURRENCY);
9894
+ const results = await Promise.allSettled(
9895
+ batch.map(async (row) => {
9896
+ const exports = Array.isArray(row.exports) ? row.exports : jsonParse(row.exports, []);
9897
+ const purpose = await caps.generatePurpose(row.path, row.first_chunk, exports, row.docstring ?? "");
9898
+ return { row, purpose };
9899
+ })
9900
+ );
9901
+ for (let j = 0; j < results.length; j++) {
9902
+ const result = results[j];
9903
+ const row = batch[j];
9904
+ if (result.status === "fulfilled" && result.value.purpose) {
9905
+ metadataQueries.updatePurpose(db, repoId, row.path, result.value.purpose, row.chunk_hash);
9906
+ totalEnriched++;
9907
+ } else {
9908
+ metadataQueries.setPurposeHash(db, repoId, row.path, row.chunk_hash);
9909
+ totalSkipped++;
9910
+ }
9911
+ }
9912
+ }
9913
+ if (!fullRun) break;
9914
+ }
9915
+ return { enriched: totalEnriched, skipped: totalSkipped, duration_ms: Date.now() - start };
9916
+ }
9917
+ function splitIdentifier(name) {
9918
+ 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 && !STOPWORDS3.has(w));
9919
+ }
9920
+ function extractVocab(files) {
9921
+ const termToFiles = /* @__PURE__ */ new Map();
9922
+ for (const f of files) {
9923
+ const pathParts = f.path.split("/");
9924
+ for (const seg of pathParts) {
9925
+ const base = seg.replace(/\.[^.]+$/, "");
9926
+ for (const word of splitIdentifier(base)) {
9927
+ const s = termToFiles.get(word) ?? /* @__PURE__ */ new Set();
9928
+ s.add(f.path);
9929
+ termToFiles.set(word, s);
9930
+ }
9931
+ }
9932
+ for (const exp of f.exports) {
9933
+ for (const word of splitIdentifier(exp)) {
9934
+ const s = termToFiles.get(word) ?? /* @__PURE__ */ new Set();
9935
+ s.add(f.path);
9936
+ termToFiles.set(word, s);
9937
+ }
9938
+ }
9939
+ }
9940
+ const totalFiles = files.length;
9941
+ const maxDf = Math.max(10, Math.floor(totalFiles * 0.3));
9942
+ const filtered = [];
9943
+ for (const [term, fileSet] of termToFiles) {
9944
+ if (fileSet.size >= 2 && fileSet.size <= maxDf) {
9945
+ filtered.push(term);
9946
+ }
9947
+ }
9948
+ filtered.sort((a, b) => (termToFiles.get(b)?.size ?? 0) - (termToFiles.get(a)?.size ?? 0));
9949
+ const terms = filtered.slice(0, MAX_TERMS);
9950
+ return { terms, termToFiles };
9951
+ }
9952
+ function cosine2(a, b) {
9953
+ let dot = 0, na = 0, nb = 0;
9954
+ for (let i = 0; i < a.length; i++) {
9955
+ dot += a[i] * b[i];
9956
+ na += a[i] * a[i];
9957
+ nb += b[i] * b[i];
9958
+ }
9959
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
9960
+ return denom > 0 ? dot / denom : 0;
9961
+ }
9962
+ function agglomerativeCluster(terms, embeddings) {
9963
+ const n = terms.length;
9964
+ const pairs = [];
9965
+ for (let i = 0; i < n; i++) {
9966
+ for (let j = i + 1; j < n; j++) {
9967
+ const sim = cosine2(embeddings[i], embeddings[j]);
9968
+ if (sim >= SIMILARITY_THRESHOLD) pairs.push({ i, j, sim });
9969
+ }
9970
+ }
9971
+ pairs.sort((a, b) => b.sim - a.sim);
9972
+ const parent = new Int32Array(n);
9973
+ const size = new Uint16Array(n);
9974
+ for (let i = 0; i < n; i++) {
9975
+ parent[i] = i;
9976
+ size[i] = 1;
9977
+ }
9978
+ function find(x) {
9979
+ while (parent[x] !== x) {
9980
+ parent[x] = parent[parent[x]];
9981
+ x = parent[x];
9982
+ }
9983
+ return x;
9984
+ }
9985
+ for (const { i, j } of pairs) {
9986
+ const ri = find(i), rj = find(j);
9987
+ if (ri === rj) continue;
9988
+ if (size[ri] + size[rj] > MAX_CLUSTER_SIZE) continue;
9989
+ if (size[ri] >= size[rj]) {
9990
+ parent[rj] = ri;
9991
+ size[ri] += size[rj];
9992
+ } else {
9993
+ parent[ri] = rj;
9994
+ size[rj] += size[ri];
9995
+ }
9996
+ }
9997
+ const clusterMap = /* @__PURE__ */ new Map();
9998
+ for (let i = 0; i < n; i++) {
9999
+ const root = find(i);
10000
+ const arr = clusterMap.get(root) ?? [];
10001
+ arr.push(i);
10002
+ clusterMap.set(root, arr);
10003
+ }
10004
+ return [...clusterMap.values()].filter((c) => c.length >= 2);
10005
+ }
10006
+ async function buildVocabClusters(db, repoId, caps, force = false) {
10007
+ if (!caps?.embedTexts) {
10008
+ console.error("[LENS] Vocab clusters: skipped (no embedTexts capability)");
10009
+ return;
10010
+ }
10011
+ const repo = repoQueries.getById(db, repoId);
10012
+ if (!repo) return;
10013
+ if (!force && repo.last_vocab_cluster_commit && repo.last_vocab_cluster_commit === repo.last_indexed_commit) {
10014
+ console.error("[LENS] Vocab clusters: skipped (unchanged since last build)");
10015
+ return;
10016
+ }
10017
+ const rows = metadataQueries.getByRepo(db, repoId).map((r) => ({
10018
+ path: r.path,
10019
+ exports: Array.isArray(r.exports) ? r.exports : jsonParse(r.exports, [])
10020
+ }));
10021
+ if (rows.length === 0) {
10022
+ console.error("[LENS] Vocab clusters: skipped (no metadata rows)");
10023
+ return;
10024
+ }
10025
+ const { terms, termToFiles } = extractVocab(rows);
10026
+ if (terms.length < 4) {
10027
+ console.error(`[LENS] Vocab clusters: skipped (only ${terms.length} terms, need \u22654)`);
10028
+ return;
10029
+ }
10030
+ console.error(
10031
+ `[LENS] Vocab clusters: embedding ${terms.length} terms in ${Math.ceil(terms.length / TERM_BATCH_SIZE)} batches...`
10032
+ );
10033
+ let embeddings;
10034
+ try {
10035
+ const results = [];
10036
+ for (let i = 0; i < terms.length; i += TERM_BATCH_SIZE) {
10037
+ const batch = terms.slice(i, i + TERM_BATCH_SIZE);
10038
+ const vecs = await caps.embedTexts(batch, false);
10039
+ results.push(...vecs);
10040
+ }
10041
+ embeddings = results;
10042
+ } catch (err) {
10043
+ console.error(`[LENS] Vocab clusters: embed failed (${terms.length} terms):`, err.message);
10044
+ return;
10045
+ }
10046
+ const rawClusters = agglomerativeCluster(terms, embeddings);
10047
+ console.error(`[LENS] Vocab clusters: ${rawClusters.length} clusters from ${terms.length} terms`);
10048
+ const clusters = rawClusters.map((indices) => {
10049
+ const clusterTerms = indices.map((i) => terms[i]);
10050
+ const fileSet = /* @__PURE__ */ new Set();
10051
+ for (const t of clusterTerms) {
10052
+ const files = termToFiles.get(t);
10053
+ if (files) for (const f of files) fileSet.add(f);
10054
+ }
10055
+ return { terms: clusterTerms, files: [...fileSet].sort() };
10056
+ }).filter((c) => c.files.length > 0).sort((a, b) => b.files.length - a.files.length).slice(0, MAX_CLUSTERS);
10057
+ repoQueries.updateVocabClusters(db, repoId, clusters, repo.last_indexed_commit ?? void 0);
10058
+ }
10059
+ async function handleFileChange(db, repoId, repoRoot, absPath) {
10060
+ const relPath = relative3(repoRoot, absPath);
10061
+ if (isBinaryExt(relPath)) return;
10062
+ try {
10063
+ const s = await stat22(absPath);
10064
+ if (s.size > MAX_FILE_SIZE) return;
10065
+ } catch {
10066
+ return;
10067
+ }
10068
+ let content;
10069
+ try {
10070
+ content = await readFile2(absPath, "utf-8");
10071
+ } catch {
10072
+ return;
10073
+ }
10074
+ const language = detectLanguage(relPath);
10075
+ const newChunks = chunkFile(content, DEFAULT_CHUNKING_PARAMS);
10076
+ const existing = /* @__PURE__ */ new Map();
10077
+ for (const row of chunkQueries.getByRepoPath(db, repoId, relPath)) {
10078
+ existing.set(`${row.chunk_index}:${row.chunk_hash}`, row.id);
10079
+ }
10080
+ const newKeys = /* @__PURE__ */ new Set();
10081
+ for (const chunk of newChunks) {
10082
+ const key = `${chunk.chunk_index}:${chunk.chunk_hash}`;
10083
+ newKeys.add(key);
10084
+ if (existing.has(key)) {
10085
+ chunkQueries.updateLastSeen(db, existing.get(key), repoId, "watcher");
10086
+ } else {
10087
+ chunkQueries.upsert(
10088
+ db,
10089
+ repoId,
10090
+ relPath,
10091
+ chunk.chunk_index,
10092
+ chunk.start_line,
10093
+ chunk.end_line,
10094
+ chunk.content,
10095
+ chunk.chunk_hash,
10096
+ "watcher",
10097
+ language
10098
+ );
10099
+ }
10100
+ }
10101
+ for (const [key, id] of existing) {
10102
+ if (!newKeys.has(key)) chunkQueries.deleteById(db, id, repoId);
10103
+ }
10104
+ const entry = watchers.get(repoId);
10105
+ if (entry) entry.changedFiles++;
10106
+ }
10107
+ async function handleFileDelete(db, repoId, repoRoot, absPath) {
10108
+ const relPath = relative3(repoRoot, absPath);
10109
+ if (isBinaryExt(relPath)) return;
10110
+ chunkQueries.deleteByRepoPath(db, repoId, relPath);
10111
+ const entry = watchers.get(repoId);
10112
+ if (entry) entry.deletedFiles++;
10113
+ }
10114
+ function debouncedHandler(key, fn) {
10115
+ const existing = debounceTimers.get(key);
10116
+ if (existing) clearTimeout(existing);
10117
+ debounceTimers.set(
10118
+ key,
10119
+ setTimeout(() => {
10120
+ debounceTimers.delete(key);
10121
+ fn().catch(() => {
10122
+ });
10123
+ }, DEBOUNCE_MS)
10124
+ );
10125
+ }
10126
+ function startWatcher(db, repoId, rootPath) {
10127
+ if (watchers.has(repoId)) return { started: false, already_watching: true };
10128
+ try {
10129
+ const fsWatcher = watch(rootPath, {
10130
+ ignored: IGNORED,
10131
+ ignoreInitial: true,
10132
+ awaitWriteFinish: { stabilityThreshold: 200 },
10133
+ persistent: false,
10134
+ usePolling: false,
10135
+ depth: 5
10136
+ });
10137
+ const entry = {
10138
+ watcher: fsWatcher,
10139
+ repoRoot: rootPath,
10140
+ repoId,
10141
+ changedFiles: 0,
10142
+ deletedFiles: 0,
10143
+ startedAt: /* @__PURE__ */ new Date()
10144
+ };
10145
+ watchers.set(repoId, entry);
10146
+ fsWatcher.on(
10147
+ "add",
10148
+ (path) => debouncedHandler(`${repoId}:${path}`, () => handleFileChange(db, repoId, rootPath, path))
10149
+ );
10150
+ fsWatcher.on(
10151
+ "change",
10152
+ (path) => debouncedHandler(`${repoId}:${path}`, () => handleFileChange(db, repoId, rootPath, path))
10153
+ );
10154
+ fsWatcher.on(
10155
+ "unlink",
10156
+ (path) => debouncedHandler(`${repoId}:${path}`, () => handleFileDelete(db, repoId, rootPath, path))
10157
+ );
10158
+ fsWatcher.on("error", () => {
10159
+ stopWatcher(repoId).catch(() => {
10160
+ });
10161
+ });
10162
+ return { started: true, already_watching: false };
10163
+ } catch {
10164
+ return { started: false, already_watching: false };
10165
+ }
10166
+ }
10167
+ async function stopWatcher(repoId) {
10168
+ const entry = watchers.get(repoId);
10169
+ if (!entry) return { stopped: false };
10170
+ await entry.watcher.close();
10171
+ watchers.delete(repoId);
10172
+ for (const [key, timer] of debounceTimers) {
10173
+ if (key.startsWith(`${repoId}:`)) {
10174
+ clearTimeout(timer);
10175
+ debounceTimers.delete(key);
10176
+ }
10177
+ }
10178
+ return { stopped: true };
10179
+ }
10180
+ function getWatcherStatus(repoId) {
10181
+ const entry = watchers.get(repoId);
10182
+ if (!entry) return { watching: false, repo_root: null, changed_files: 0, deleted_files: 0, started_at: null };
10183
+ return {
10184
+ watching: true,
10185
+ repo_root: entry.repoRoot,
10186
+ changed_files: entry.changedFiles,
10187
+ deleted_files: entry.deletedFiles,
10188
+ started_at: entry.startedAt.toISOString()
10189
+ };
10190
+ }
10191
+ function normalizeRemoteUrl(url) {
10192
+ let normalized = url.trim();
10193
+ const sshMatch = normalized.match(/^[\w-]+@([\w.-]+):(.+)$/);
10194
+ if (sshMatch) {
10195
+ normalized = `${sshMatch[1]}/${sshMatch[2]}`;
10196
+ } else {
10197
+ normalized = normalized.replace(/^https?:\/\//, "");
10198
+ }
10199
+ normalized = normalized.replace(/\.git$/, "");
10200
+ normalized = normalized.replace(/\/$/, "");
10201
+ return normalized.toLowerCase();
10202
+ }
10203
+ function deriveIdentityKey(rootPath, remoteUrl) {
10204
+ const source = remoteUrl ? normalizeRemoteUrl(remoteUrl) : rootPath;
10205
+ return createHash2("sha256").update(source).digest("hex");
10206
+ }
10207
+ function registerRepo(db, rootPath, name, remoteUrl) {
10208
+ const identityKey = deriveIdentityKey(rootPath, remoteUrl);
10209
+ const repoName = name ?? rootPath.split("/").pop() ?? "unknown";
10210
+ const { id, created } = repoQueries.upsert(db, identityKey, repoName, rootPath, remoteUrl ?? null);
10211
+ return { repo_id: id, identity_key: identityKey, name: repoName, created };
10212
+ }
10213
+ function getRepo(db, id) {
10214
+ const repo = repoQueries.getById(db, id);
10215
+ if (!repo) throw new Error("repo not found");
10216
+ return repo;
10217
+ }
10218
+ function listRepos(db) {
10219
+ return repoQueries.list(db);
10220
+ }
10221
+ function removeRepo(db, id) {
10222
+ const chunkCount = chunkQueries.countByRepo(db, id);
10223
+ const deleted = repoQueries.remove(db, id);
10224
+ if (!deleted) throw new Error("repo not found");
10225
+ return { deleted: true, chunks_removed: chunkCount };
10226
+ }
10227
+ async function getRepoStatus(db, id) {
10228
+ const repo = repoQueries.getById(db, id);
10229
+ if (!repo) throw new Error("repo not found");
10230
+ let currentHead = null;
10231
+ try {
10232
+ currentHead = await getHeadCommit(repo.root_path);
9577
10233
  } catch {
9578
- return {
9579
- context_pack: `# ${goal}
9580
-
9581
- Context generation failed.`,
9582
- stats: { files_in_context: 0, index_fresh: false, duration_ms: Date.now() - start, cached: false }
9583
- };
9584
10234
  }
10235
+ const isStale = !!(currentHead && repo.last_indexed_commit !== currentHead);
10236
+ const stats = chunkQueries.getStats(db, id);
10237
+ const structural = metadataQueries.getStructuralStats(db, id);
10238
+ const vocabRaw = repo.vocab_clusters;
10239
+ const vocabClusters = vocabRaw ? JSON.parse(vocabRaw) : [];
10240
+ return {
10241
+ index_status: repo.index_status,
10242
+ indexed_commit: repo.last_indexed_commit,
10243
+ current_head: currentHead,
10244
+ is_stale: isStale,
10245
+ chunk_count: stats.chunk_count,
10246
+ files_indexed: stats.files_indexed,
10247
+ embedded_count: stats.embedded_count,
10248
+ embeddable_count: stats.embeddable_count,
10249
+ embedded_pct: stats.embeddable_count > 0 ? Math.round(stats.embedded_count / stats.embeddable_count * 100) : 0,
10250
+ metadata_count: structural.metadata_count,
10251
+ import_edge_count: structural.import_edge_count,
10252
+ git_commits_analyzed: structural.git_file_count,
10253
+ cochange_pairs: structural.cochange_pairs,
10254
+ purpose_count: structural.purpose_count,
10255
+ purpose_total: structural.purpose_total,
10256
+ vocab_cluster_count: Array.isArray(vocabClusters) ? vocabClusters.length : 0
10257
+ };
9585
10258
  }
9586
- 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, UNIVERSAL_DECL_RES, EXPORT_LINE_RE, SKIP_NAMES, 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;
10259
+ 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, slicer_exports, RADIUS, MAX_SLICES, init_slicer, _enabled, DEFAULT_CHUNKING_PARAMS, execFileAsync, MAX_FILE_SIZE, BINARY_EXTENSIONS, DOCS_EXTENSIONS, LANG_MAP, 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, UNIVERSAL_DECL_RES, EXPORT_LINE_RE, SKIP_NAMES, execFileAsync2, MAX_COMMITS, MAX_FILES_PER_COMMIT, RECENT_DAYS, MAX_CHUNKS_PER_REPO, locks, TOKEN_CAP, LANG_EXT, FRAME_PATTERNS, CASING_BOUNDARY, SYMBOL_RE, ERROR_CODE_RE, ERROR_SUFFIX_RE, ERROR_PREFIX_RE, STOPWORDS, STOPWORDS2, NOISE_EXTENSIONS, NOISE_PATHS, CONCEPT_SYNONYMS, SYMBOL_DEF_RE, TEST_PATTERN, CACHE_TTL, CACHE_MAX, cache, _db, _raw, GOLD_DATASET, 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, STOPWORDS3, watchers, debounceTimers, IGNORED, DEBOUNCE_MS, RequestTrace;
9587
10260
  var init_dist = __esm({
9588
10261
  "packages/engine/dist/index.js"() {
9589
10262
  "use strict";
@@ -9771,10 +10444,7 @@ var init_dist = __esm({
9771
10444
  created_at: now(),
9772
10445
  synced_at: text("synced_at")
9773
10446
  },
9774
- (t) => [
9775
- index("idx_telemetry_events_type").on(t.event_type),
9776
- index("idx_telemetry_events_synced").on(t.synced_at)
9777
- ]
10447
+ (t) => [index("idx_telemetry_events_type").on(t.event_type), index("idx_telemetry_events_synced").on(t.synced_at)]
9778
10448
  );
9779
10449
  }
9780
10450
  });
@@ -9972,6 +10642,21 @@ var init_dist = __esm({
9972
10642
  const row = db.select({ id: chunks.id }).from(chunks).where(and(eq(chunks.repo_id, repoId), sql`embedding IS NOT NULL`)).limit(1).get();
9973
10643
  return !!row;
9974
10644
  },
10645
+ getByRepoPaths(db, repoId, paths) {
10646
+ if (!paths.length) return [];
10647
+ return db.select({
10648
+ path: chunks.path,
10649
+ chunk_index: chunks.chunk_index,
10650
+ start_line: chunks.start_line,
10651
+ end_line: chunks.end_line,
10652
+ content: chunks.content
10653
+ }).from(chunks).where(and(eq(chunks.repo_id, repoId), inArray(chunks.path, paths))).orderBy(chunks.path, chunks.chunk_index).all();
10654
+ },
10655
+ searchContent(db, repoId, searchString) {
10656
+ if (!searchString || searchString.length < 4) return [];
10657
+ const rows = db.select({ path: chunks.path }).from(chunks).where(and(eq(chunks.repo_id, repoId), sql`INSTR(LOWER(${chunks.content}), ${searchString.toLowerCase()}) > 0`)).groupBy(chunks.path).all();
10658
+ return rows.map((r) => r.path);
10659
+ },
9975
10660
  getStats(db, repoId) {
9976
10661
  const row = db.select({
9977
10662
  chunk_count: sql`count(*)`,
@@ -10222,7 +10907,7 @@ var init_dist = __esm({
10222
10907
  const { limit = 50, offset = 0, method, path, status, source } = opts;
10223
10908
  const conditions = [];
10224
10909
  if (method) conditions.push(eq(requestLogs.method, method));
10225
- if (path) conditions.push(sql`${requestLogs.path} LIKE ${"%" + path + "%"}`);
10910
+ if (path) conditions.push(sql`${requestLogs.path} LIKE ${`%${path}%`}`);
10226
10911
  if (status) conditions.push(eq(requestLogs.status, status));
10227
10912
  if (source) conditions.push(eq(requestLogs.source, source));
10228
10913
  const where = conditions.length ? and(...conditions) : void 0;
@@ -10264,7 +10949,9 @@ var init_dist = __esm({
10264
10949
  return db.select().from(usageCounters).where(eq(usageCounters.date, date3)).get() ?? null;
10265
10950
  },
10266
10951
  getUnsynced(db) {
10267
- return db.select().from(usageCounters).where(sql`synced_at IS NULL AND (context_queries > 0 OR embedding_requests > 0 OR purpose_requests > 0 OR repos_indexed > 0)`).all();
10952
+ return db.select().from(usageCounters).where(
10953
+ sql`synced_at IS NULL AND (context_queries > 0 OR embedding_requests > 0 OR purpose_requests > 0 OR repos_indexed > 0)`
10954
+ ).all();
10268
10955
  },
10269
10956
  markSynced(db, date3) {
10270
10957
  db.update(usageCounters).set({ synced_at: sql`datetime('now')` }).where(eq(usageCounters.date, date3)).run();
@@ -10316,10 +11003,32 @@ var init_dist = __esm({
10316
11003
  };
10317
11004
  }
10318
11005
  });
10319
- init_schema();
10320
- _db = null;
10321
- _raw = null;
11006
+ slicer_exports = {};
11007
+ __export2(slicer_exports, {
11008
+ sliceContext: () => sliceContext
11009
+ });
11010
+ init_slicer = __esm2({
11011
+ "src/context/slicer.ts"() {
11012
+ "use strict";
11013
+ init_queries();
11014
+ RADIUS = 10;
11015
+ MAX_SLICES = {
11016
+ symbol: 1,
11017
+ error_message: 1,
11018
+ stack_trace: 2,
11019
+ natural: 3
11020
+ };
11021
+ }
11022
+ });
11023
+ init_queries();
10322
11024
  init_queries();
11025
+ init_queries();
11026
+ _enabled = true;
11027
+ DEFAULT_CHUNKING_PARAMS = {
11028
+ target_lines: 150,
11029
+ overlap_lines: 10,
11030
+ version: 1
11031
+ };
10323
11032
  execFileAsync = promisify(execFile);
10324
11033
  MAX_FILE_SIZE = 2 * 1024 * 1024;
10325
11034
  BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -10397,12 +11106,6 @@ var init_dist = __esm({
10397
11106
  ".scss": "scss"
10398
11107
  };
10399
11108
  init_queries();
10400
- DEFAULT_CHUNKING_PARAMS = {
10401
- target_lines: 150,
10402
- overlap_lines: 10,
10403
- version: 1
10404
- };
10405
- init_queries();
10406
11109
  TS_IMPORT_RE = /(?:import\s+.*?from\s+|import\s+|export\s+.*?from\s+|require\s*\()['"]([^'"]+)['"]/g;
10407
11110
  PY_IMPORT_RE = /^(?:from\s+(\.[\w.]*)\s+import|import\s+([\w.]+))/gm;
10408
11111
  GO_IMPORT_RE = /import\s+(?:\(\s*)?(?:[\w.]*\s+)?"([^"]+)"/g;
@@ -10414,7 +11117,7 @@ var init_dist = __esm({
10414
11117
  GO_EXPORT_RE = /^func\s+([A-Z]\w*)/gm;
10415
11118
  RUST_EXPORT_RE = /^pub\s+(?:fn|struct|enum|trait|type|mod)\s+(\w+)/gm;
10416
11119
  CSHARP_EXPORT_RE = /^\s*(?:public|internal)\s+(?:static\s+)?(?:abstract\s+|sealed\s+|partial\s+)?(?:class|interface|enum|struct|record|delegate)\s+(\w+)/gm;
10417
- CSHARP_EXPORT_METHOD_RE = /^\s*public\s+(?:static\s+)?(?:async\s+)?(?:override\s+)?(?:virtual\s+)?[\w<>\[\]?.]+\s+(\w+)\s*\(/gm;
11120
+ CSHARP_EXPORT_METHOD_RE = /^\s*public\s+(?:static\s+)?(?:async\s+)?(?:override\s+)?(?:virtual\s+)?[\w<>[\]?.]+\s+(\w+)\s*\(/gm;
10418
11121
  JAVA_EXPORT_RE = /^\s*(?:public)\s+(?:static\s+)?(?:abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)/gm;
10419
11122
  JSDOC_RE = /^\/\*\*\s*([\s\S]*?)\*\//m;
10420
11123
  PY_DOCSTRING_RE = /^(?:["']{3})([\s\S]*?)(?:["']{3})/m;
@@ -10428,60 +11131,179 @@ var init_dist = __esm({
10428
11131
  /^\s*(?:const|let|var|val)\s+(\w+)\s*[=:]/gm,
10429
11132
  /^\s*(?:abstract\s+|sealed\s+|partial\s+|data\s+)?(?:class|struct|enum|trait|interface|record|object|mod)\s+(\w+)/gm,
10430
11133
  /^\s*type\s+(\w+)\s*[=<{]/gm,
10431
- /^\s*(?:private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:suspend\s+)?(?:override\s+)?[\w<>\[\]?.]+\s+(\w+)\s*\(/gm
11134
+ /^\s*(?:private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:suspend\s+)?(?:override\s+)?[\w<>[\]?.]+\s+(\w+)\s*\(/gm
10432
11135
  ];
10433
11136
  EXPORT_LINE_RE = /^(?:export|pub\s|public\s)/;
10434
11137
  SKIP_NAMES = /* @__PURE__ */ new Set([
10435
11138
  "if",
10436
- "for",
10437
- "foreach",
10438
- "while",
10439
- "switch",
10440
- "using",
10441
- "catch",
10442
- "lock",
10443
- "return",
10444
- "throw",
10445
- "yield",
10446
- "try",
10447
- "do",
10448
- "else",
10449
- "new",
10450
- "await",
10451
- "base",
11139
+ "for",
11140
+ "foreach",
11141
+ "while",
11142
+ "switch",
11143
+ "using",
11144
+ "catch",
11145
+ "lock",
11146
+ "return",
11147
+ "throw",
11148
+ "yield",
11149
+ "try",
11150
+ "do",
11151
+ "else",
11152
+ "new",
11153
+ "await",
11154
+ "base",
11155
+ "this",
11156
+ "super",
11157
+ "self",
11158
+ "import",
11159
+ "require",
11160
+ "from",
11161
+ "package"
11162
+ ]);
11163
+ init_queries();
11164
+ execFileAsync2 = promisify2(execFile2);
11165
+ MAX_COMMITS = 2e3;
11166
+ MAX_FILES_PER_COMMIT = 20;
11167
+ RECENT_DAYS = 90;
11168
+ init_queries();
11169
+ MAX_CHUNKS_PER_REPO = 1e5;
11170
+ locks = /* @__PURE__ */ new Map();
11171
+ TOKEN_CAP = 2e3;
11172
+ LANG_EXT = {
11173
+ ts: "typescript",
11174
+ tsx: "typescript",
11175
+ js: "javascript",
11176
+ jsx: "javascript",
11177
+ py: "python",
11178
+ rb: "ruby",
11179
+ go: "go",
11180
+ rs: "rust",
11181
+ java: "java",
11182
+ kt: "kotlin",
11183
+ cs: "csharp",
11184
+ cpp: "cpp",
11185
+ c: "c",
11186
+ h: "c",
11187
+ swift: "swift",
11188
+ php: "php",
11189
+ sql: "sql",
11190
+ sh: "shell"
11191
+ };
11192
+ FRAME_PATTERNS = [
11193
+ // JS/TS: at fn (path:line:col) or at path:line:col
11194
+ /at\s+(?:\S+\s+\()?([^():]+):(\d+):\d+\)?/g,
11195
+ // Python: File "path", line N
11196
+ /File\s+"([^"]+)",\s+line\s+(\d+)/g,
11197
+ // C#/Java: at namespace(path:line)
11198
+ /at\s+\S+\(([^():]+):(\d+)\)/g
11199
+ ];
11200
+ CASING_BOUNDARY = /[a-z][A-Z]|_/;
11201
+ SYMBOL_RE = /^[a-zA-Z_]\w+$/;
11202
+ ERROR_CODE_RE = /\b[A-Z][A-Z0-9_]{2,}\b/;
11203
+ ERROR_SUFFIX_RE = /\w+(Exception|Error)\b/;
11204
+ ERROR_PREFIX_RE = /\b(TypeError|ReferenceError|SyntaxError|RangeError|Error|panic|FATAL|WARN|ERR):/;
11205
+ STOPWORDS = /* @__PURE__ */ new Set([
11206
+ "the",
11207
+ "a",
11208
+ "an",
11209
+ "is",
11210
+ "are",
11211
+ "was",
11212
+ "were",
11213
+ "be",
11214
+ "been",
11215
+ "being",
11216
+ "have",
11217
+ "has",
11218
+ "had",
11219
+ "do",
11220
+ "does",
11221
+ "did",
11222
+ "will",
11223
+ "would",
11224
+ "could",
11225
+ "should",
11226
+ "may",
11227
+ "might",
11228
+ "can",
11229
+ "need",
11230
+ "must",
11231
+ "to",
11232
+ "of",
11233
+ "in",
11234
+ "for",
11235
+ "on",
11236
+ "with",
11237
+ "at",
11238
+ "by",
11239
+ "from",
11240
+ "as",
11241
+ "into",
11242
+ "through",
11243
+ "and",
11244
+ "but",
11245
+ "or",
11246
+ "not",
11247
+ "no",
11248
+ "so",
11249
+ "if",
11250
+ "then",
11251
+ "than",
11252
+ "that",
10452
11253
  "this",
10453
- "super",
10454
- "self",
11254
+ "it",
11255
+ "its",
11256
+ "all",
11257
+ "each",
11258
+ "every",
11259
+ "any",
11260
+ "some",
11261
+ "how",
11262
+ "what",
11263
+ "which",
11264
+ "who",
11265
+ "when",
11266
+ "where",
11267
+ "why",
11268
+ "get",
11269
+ "set",
11270
+ "new",
11271
+ "null",
11272
+ "true",
11273
+ "false",
11274
+ "void",
11275
+ "type",
11276
+ "var",
11277
+ "let",
11278
+ "const",
11279
+ "return",
10455
11280
  "import",
10456
- "require",
10457
- "from",
10458
- "package"
11281
+ "export",
11282
+ "default",
11283
+ "class",
11284
+ "function",
11285
+ "string",
11286
+ "number",
11287
+ "boolean",
11288
+ "object",
11289
+ "array",
11290
+ "index",
11291
+ "data",
11292
+ "value",
11293
+ "result",
11294
+ "item",
11295
+ "list",
11296
+ "name",
11297
+ "id",
11298
+ "key",
11299
+ "src",
11300
+ "lib",
11301
+ "app",
11302
+ "spec",
11303
+ "mock",
11304
+ "module"
10459
11305
  ]);
10460
- init_queries();
10461
- init_queries();
10462
- execFileAsync2 = promisify2(execFile2);
10463
- MAX_COMMITS = 2e3;
10464
- MAX_FILES_PER_COMMIT = 20;
10465
- RECENT_DAYS = 90;
10466
- init_queries();
10467
- _enabled = true;
10468
- MAX_CHUNKS_PER_REPO = 1e5;
10469
- locks = /* @__PURE__ */ new Map();
10470
- init_queries();
10471
- MAX_API_CALLS = 2e3;
10472
- POOL_SIZE = 32;
10473
- MAX_BATCH_TOKENS = 1e5;
10474
- CHARS_PER_TOKEN = 3;
10475
- init_queries();
10476
- PURPOSE_BATCH_LIMIT = 200;
10477
- PURPOSE_CONCURRENCY = 10;
10478
- init_queries();
10479
- TERM_BATCH_SIZE = 32;
10480
- SIMILARITY_THRESHOLD = 0.75;
10481
- MAX_TERMS = 1500;
10482
- MAX_CLUSTERS = 200;
10483
- MAX_CLUSTER_SIZE = 12;
10484
- STOPWORDS = /* @__PURE__ */ new Set([
11306
+ STOPWORDS2 = /* @__PURE__ */ new Set([
10485
11307
  "the",
10486
11308
  "a",
10487
11309
  "an",
@@ -10578,39 +11400,294 @@ var init_dist = __esm({
10578
11400
  "src",
10579
11401
  "lib",
10580
11402
  "app",
10581
- "test",
10582
11403
  "spec",
10583
11404
  "mock",
10584
- "model",
10585
11405
  "module",
10586
- "component"
11406
+ "work",
11407
+ "works",
11408
+ "working",
11409
+ "make",
11410
+ "makes",
11411
+ "use",
11412
+ "uses",
11413
+ "used",
11414
+ "using",
11415
+ "find",
11416
+ "run",
11417
+ "runs",
11418
+ "call",
11419
+ "calls",
11420
+ "look",
11421
+ "like",
11422
+ "just",
11423
+ "also",
11424
+ "about",
11425
+ "there",
11426
+ "here",
11427
+ "define",
11428
+ "defined",
11429
+ "handle",
11430
+ "handles",
11431
+ "add",
11432
+ "adds",
11433
+ "create",
11434
+ "serve",
11435
+ "does",
11436
+ "file",
11437
+ "files"
11438
+ ]);
11439
+ NOISE_EXTENSIONS = /* @__PURE__ */ new Set([
11440
+ ".md",
11441
+ ".json",
11442
+ ".yaml",
11443
+ ".yml",
11444
+ ".toml",
11445
+ ".lock",
11446
+ ".sql",
11447
+ ".txt",
11448
+ ".csv",
11449
+ ".psd",
11450
+ ".png",
11451
+ ".jpg",
11452
+ ".jpeg",
11453
+ ".gif",
11454
+ ".svg",
11455
+ ".ico",
11456
+ ".woff",
11457
+ ".woff2",
11458
+ ".ttf",
11459
+ ".eot",
11460
+ ".xml",
11461
+ ".axml",
11462
+ ".resx",
11463
+ ".config"
10587
11464
  ]);
11465
+ NOISE_PATHS = [
11466
+ ".gitignore",
11467
+ ".github/",
11468
+ ".vscode/",
11469
+ ".idea/",
11470
+ "node_modules/",
11471
+ "dist/",
11472
+ "build/",
11473
+ "publish/",
11474
+ "vendor/",
11475
+ "vendors/",
11476
+ "/scripts/",
11477
+ "areas/helppage/",
11478
+ "packages.config",
11479
+ "resources/drawable/",
11480
+ "resources/layout/",
11481
+ "resources/values/",
11482
+ "wwwroot/lib/",
11483
+ "wwwroot/css/",
11484
+ "wwwroot/js/lib/",
11485
+ ".min.js",
11486
+ ".min.css",
11487
+ ".designer.cs",
11488
+ "assemblyinfo.cs"
11489
+ ];
11490
+ CONCEPT_SYNONYMS = {
11491
+ error: ["interceptor", "exception", "catch", "middleware", "handler"],
11492
+ errors: ["interceptor", "exception", "catch", "middleware", "handler"],
11493
+ retry: ["interceptor", "backoff", "retry", "queue"],
11494
+ retries: ["interceptor", "backoff", "retry", "queue"],
11495
+ auth: ["interceptor", "token", "login", "guard", "middleware", "session"],
11496
+ authentication: ["interceptor", "token", "login", "guard", "middleware"],
11497
+ authorization: ["guard", "role", "permission", "policy"],
11498
+ cache: ["invalidat", "ttl", "expire", "store", "memo"],
11499
+ caching: ["invalidat", "ttl", "expire", "store", "memo"],
11500
+ logging: ["logger", "trace", "monitor", "telemetry"],
11501
+ validation: ["validator", "schema", "sanitiz", "constraint"],
11502
+ notification: ["email", "alert", "signal", "push", "webhook"],
11503
+ upload: ["multipart", "stream", "blob", "attachment", "photo"],
11504
+ performance: ["optimize", "cache", "lazy", "batch", "throttle"],
11505
+ security: ["auth", "token", "encrypt", "cors", "csrf", "sanitiz"],
11506
+ testing: ["spec", "mock", "fixture", "assert", "stub"],
11507
+ deploy: ["pipeline", "docker", "build", "release", "ci-cd"],
11508
+ database: ["repository", "migration", "query", "schema", "orm"],
11509
+ api: ["controller", "endpoint", "route", "middleware", "interceptor"]
11510
+ };
10588
11511
  init_queries();
10589
- watchers = /* @__PURE__ */ new Map();
10590
- debounceTimers = /* @__PURE__ */ new Map();
10591
- IGNORED = [
10592
- /(^|[/\\])\../,
10593
- "**/node_modules/**",
10594
- "**/.git/**",
10595
- "**/dist/**",
10596
- "**/build/**",
10597
- "**/__pycache__/**",
10598
- "**/target/**",
10599
- "**/.next/**",
10600
- "**/.nuxt/**",
10601
- "**/coverage/**",
10602
- "**/.turbo/**",
10603
- "**/out/**",
10604
- "**/.output/**",
10605
- "**/*.log",
10606
- "**/*.lock",
10607
- "**/pnpm-lock.yaml",
10608
- "**/package-lock.json",
10609
- "**/yarn.lock"
11512
+ SYMBOL_DEF_RE = /^.*?\b(?:function|def|fn|class|const|let|var|type|interface|struct|enum|export\s+(?:function|class|const|type|interface|default))\s+/;
11513
+ init_queries();
11514
+ TEST_PATTERN = /\.(?:test|spec)\.|__tests__\/|_test\./;
11515
+ init_queries();
11516
+ CACHE_TTL = 12e4;
11517
+ CACHE_MAX = 20;
11518
+ cache = /* @__PURE__ */ new Map();
11519
+ init_schema();
11520
+ _db = null;
11521
+ _raw = null;
11522
+ init_queries();
11523
+ GOLD_DATASET = [
11524
+ // --- natural (12) ---
11525
+ {
11526
+ id: "nat-01",
11527
+ query: "How does the context pack pipeline work?",
11528
+ kind: "natural",
11529
+ expected_files: [
11530
+ "packages/engine/src/context/context.ts",
11531
+ "packages/engine/src/context/formatter.ts",
11532
+ "packages/engine/src/context/query-interpreter.ts"
11533
+ ],
11534
+ expected_entry: "packages/engine/src/context/context.ts"
11535
+ },
11536
+ {
11537
+ id: "nat-02",
11538
+ query: "How does file indexing work?",
11539
+ kind: "natural",
11540
+ expected_files: [
11541
+ "packages/engine/src/index/engine.ts",
11542
+ "packages/engine/src/index/chunker.ts",
11543
+ "packages/engine/src/index/discovery.ts"
11544
+ ],
11545
+ expected_entry: "packages/engine/src/index/engine.ts"
11546
+ },
11547
+ {
11548
+ id: "nat-03",
11549
+ query: "How are imports resolved and the import graph built?",
11550
+ kind: "natural",
11551
+ expected_files: ["packages/engine/src/index/import-graph.ts", "packages/engine/src/index/imports.ts"],
11552
+ expected_entry: "packages/engine/src/index/import-graph.ts"
11553
+ },
11554
+ {
11555
+ id: "nat-04",
11556
+ query: "How does the daemon HTTP server handle requests?",
11557
+ kind: "natural",
11558
+ expected_files: ["apps/daemon/src/server.ts", "apps/daemon/src/index.ts"],
11559
+ expected_entry: "apps/daemon/src/server.ts"
11560
+ },
11561
+ {
11562
+ id: "nat-05",
11563
+ query: "How does the CLI register a repo?",
11564
+ kind: "natural",
11565
+ expected_files: ["packages/cli/src/commands/register.ts", "packages/engine/src/repo/repo.ts"],
11566
+ expected_entry: "packages/cli/src/commands/register.ts"
11567
+ },
11568
+ {
11569
+ id: "nat-06",
11570
+ query: "How does git history analysis and co-change detection work?",
11571
+ kind: "natural",
11572
+ expected_files: ["packages/engine/src/index/git-analysis.ts"],
11573
+ expected_entry: "packages/engine/src/index/git-analysis.ts"
11574
+ },
11575
+ {
11576
+ id: "nat-07",
11577
+ query: "How does the MCP stdio server work?",
11578
+ kind: "natural",
11579
+ expected_files: ["apps/daemon/src/mcp.ts"],
11580
+ expected_entry: "apps/daemon/src/mcp.ts"
11581
+ },
11582
+ {
11583
+ id: "nat-08",
11584
+ query: "How does TF-IDF scoring work in the query interpreter?",
11585
+ kind: "natural",
11586
+ expected_files: ["packages/engine/src/context/query-interpreter.ts"],
11587
+ expected_entry: "packages/engine/src/context/query-interpreter.ts"
11588
+ },
11589
+ {
11590
+ id: "nat-09",
11591
+ query: "How are file metadata (exports, docstrings, sections) extracted?",
11592
+ kind: "natural",
11593
+ expected_files: ["packages/engine/src/index/extract-metadata.ts"],
11594
+ expected_entry: "packages/engine/src/index/extract-metadata.ts"
11595
+ },
11596
+ {
11597
+ id: "nat-10",
11598
+ query: "How does vector/semantic search work?",
11599
+ kind: "natural",
11600
+ expected_files: ["packages/engine/src/context/vector.ts", "packages/engine/src/index/embed.ts"],
11601
+ expected_entry: "packages/engine/src/context/vector.ts"
11602
+ },
11603
+ {
11604
+ id: "nat-11",
11605
+ query: "How does the file watcher detect changes?",
11606
+ kind: "natural",
11607
+ expected_files: ["packages/engine/src/index/watcher.ts"],
11608
+ expected_entry: "packages/engine/src/index/watcher.ts"
11609
+ },
11610
+ {
11611
+ id: "nat-12",
11612
+ query: "How is the database schema defined?",
11613
+ kind: "natural",
11614
+ expected_files: ["packages/engine/src/db/schema.ts", "packages/engine/src/db/connection.ts"],
11615
+ expected_entry: "packages/engine/src/db/schema.ts"
11616
+ },
11617
+ // --- symbol (5) ---
11618
+ {
11619
+ id: "sym-01",
11620
+ query: "buildContext",
11621
+ kind: "symbol",
11622
+ expected_files: ["packages/engine/src/context/context.ts"],
11623
+ expected_entry: "packages/engine/src/context/context.ts"
11624
+ },
11625
+ {
11626
+ id: "sym-02",
11627
+ query: "interpretQuery",
11628
+ kind: "symbol",
11629
+ expected_files: ["packages/engine/src/context/query-interpreter.ts"],
11630
+ expected_entry: "packages/engine/src/context/query-interpreter.ts"
11631
+ },
11632
+ {
11633
+ id: "sym-03",
11634
+ query: "runIndex",
11635
+ kind: "symbol",
11636
+ expected_files: ["packages/engine/src/index/engine.ts"],
11637
+ expected_entry: "packages/engine/src/index/engine.ts"
11638
+ },
11639
+ {
11640
+ id: "sym-04",
11641
+ query: "extractFileMetadata",
11642
+ kind: "symbol",
11643
+ expected_files: ["packages/engine/src/index/extract-metadata.ts"],
11644
+ expected_entry: "packages/engine/src/index/extract-metadata.ts"
11645
+ },
11646
+ {
11647
+ id: "sym-05",
11648
+ query: "formatContextPack",
11649
+ kind: "symbol",
11650
+ expected_files: ["packages/engine/src/context/formatter.ts"],
11651
+ expected_entry: "packages/engine/src/context/formatter.ts"
11652
+ },
11653
+ // --- error_message (3) ---
11654
+ {
11655
+ id: "err-01",
11656
+ query: "Error: repo not found",
11657
+ kind: "error_message",
11658
+ expected_files: ["packages/engine/src/repo/repo.ts", "apps/daemon/src/server.ts"],
11659
+ expected_entry: "packages/engine/src/repo/repo.ts"
11660
+ },
11661
+ {
11662
+ id: "err-02",
11663
+ query: "LENS daemon is not running",
11664
+ kind: "error_message",
11665
+ expected_files: ["packages/cli/src/util/client.ts"],
11666
+ expected_entry: "packages/cli/src/util/client.ts"
11667
+ },
11668
+ {
11669
+ id: "err-03",
11670
+ query: "Context generation failed",
11671
+ kind: "error_message",
11672
+ expected_files: ["packages/engine/src/context/context.ts"],
11673
+ expected_entry: "packages/engine/src/context/context.ts"
11674
+ }
10610
11675
  ];
10611
- DEBOUNCE_MS = 500;
10612
11676
  init_queries();
10613
- STOPWORDS2 = /* @__PURE__ */ new Set([
11677
+ MAX_API_CALLS = 2e3;
11678
+ POOL_SIZE = 32;
11679
+ MAX_BATCH_TOKENS = 1e5;
11680
+ CHARS_PER_TOKEN = 3;
11681
+ init_queries();
11682
+ PURPOSE_BATCH_LIMIT = 200;
11683
+ PURPOSE_CONCURRENCY = 10;
11684
+ init_queries();
11685
+ TERM_BATCH_SIZE = 32;
11686
+ SIMILARITY_THRESHOLD = 0.75;
11687
+ MAX_TERMS = 1500;
11688
+ MAX_CLUSTERS = 200;
11689
+ MAX_CLUSTER_SIZE = 12;
11690
+ STOPWORDS3 = /* @__PURE__ */ new Set([
10614
11691
  "the",
10615
11692
  "a",
10616
11693
  "an",
@@ -10707,86 +11784,37 @@ var init_dist = __esm({
10707
11784
  "src",
10708
11785
  "lib",
10709
11786
  "app",
11787
+ "test",
10710
11788
  "spec",
10711
11789
  "mock",
10712
- "module"
10713
- ]);
10714
- NOISE_EXTENSIONS = /* @__PURE__ */ new Set([
10715
- ".md",
10716
- ".json",
10717
- ".yaml",
10718
- ".yml",
10719
- ".toml",
10720
- ".lock",
10721
- ".sql",
10722
- ".txt",
10723
- ".csv",
10724
- ".psd",
10725
- ".png",
10726
- ".jpg",
10727
- ".jpeg",
10728
- ".gif",
10729
- ".svg",
10730
- ".ico",
10731
- ".woff",
10732
- ".woff2",
10733
- ".ttf",
10734
- ".eot",
10735
- ".xml",
10736
- ".axml",
10737
- ".resx",
10738
- ".config"
11790
+ "model",
11791
+ "module",
11792
+ "component"
10739
11793
  ]);
10740
- NOISE_PATHS = [
10741
- ".gitignore",
10742
- ".github/",
10743
- ".vscode/",
10744
- ".idea/",
10745
- "node_modules/",
10746
- "dist/",
10747
- "build/",
10748
- "vendor/",
10749
- "vendors/",
10750
- "/scripts/",
10751
- "areas/helppage/",
10752
- "packages.config",
10753
- "resources/drawable/",
10754
- "resources/layout/",
10755
- "resources/values/",
10756
- "wwwroot/lib/",
10757
- "wwwroot/css/",
10758
- "wwwroot/js/lib/",
10759
- ".min.js",
10760
- ".min.css",
10761
- ".designer.cs",
10762
- "assemblyinfo.cs"
10763
- ];
10764
- CONCEPT_SYNONYMS = {
10765
- error: ["interceptor", "exception", "catch", "middleware", "handler"],
10766
- errors: ["interceptor", "exception", "catch", "middleware", "handler"],
10767
- retry: ["interceptor", "backoff", "retry", "queue"],
10768
- retries: ["interceptor", "backoff", "retry", "queue"],
10769
- auth: ["interceptor", "token", "login", "guard", "middleware", "session"],
10770
- authentication: ["interceptor", "token", "login", "guard", "middleware"],
10771
- authorization: ["guard", "role", "permission", "policy"],
10772
- cache: ["invalidat", "ttl", "expire", "store", "memo"],
10773
- caching: ["invalidat", "ttl", "expire", "store", "memo"],
10774
- logging: ["logger", "trace", "monitor", "telemetry"],
10775
- validation: ["validator", "schema", "sanitiz", "constraint"],
10776
- notification: ["email", "alert", "signal", "push", "webhook"],
10777
- upload: ["multipart", "stream", "blob", "attachment", "photo"],
10778
- performance: ["optimize", "cache", "lazy", "batch", "throttle"],
10779
- security: ["auth", "token", "encrypt", "cors", "csrf", "sanitiz"],
10780
- testing: ["spec", "mock", "fixture", "assert", "stub"],
10781
- deploy: ["pipeline", "docker", "build", "release", "ci-cd"],
10782
- database: ["repository", "migration", "query", "schema", "orm"],
10783
- api: ["controller", "endpoint", "route", "middleware", "interceptor"]
10784
- };
10785
- init_queries();
10786
11794
  init_queries();
10787
- CACHE_TTL = 12e4;
10788
- CACHE_MAX = 20;
10789
- cache = /* @__PURE__ */ new Map();
11795
+ watchers = /* @__PURE__ */ new Map();
11796
+ debounceTimers = /* @__PURE__ */ new Map();
11797
+ IGNORED = [
11798
+ /(^|[/\\])\../,
11799
+ "**/node_modules/**",
11800
+ "**/.git/**",
11801
+ "**/dist/**",
11802
+ "**/build/**",
11803
+ "**/__pycache__/**",
11804
+ "**/target/**",
11805
+ "**/.next/**",
11806
+ "**/.nuxt/**",
11807
+ "**/coverage/**",
11808
+ "**/.turbo/**",
11809
+ "**/out/**",
11810
+ "**/.output/**",
11811
+ "**/*.log",
11812
+ "**/*.lock",
11813
+ "**/pnpm-lock.yaml",
11814
+ "**/package-lock.json",
11815
+ "**/yarn.lock"
11816
+ ];
11817
+ DEBOUNCE_MS = 500;
10790
11818
  init_queries();
10791
11819
  RequestTrace = class {
10792
11820
  steps = [];
@@ -10822,10 +11850,10 @@ var init_dist = __esm({
10822
11850
  });
10823
11851
 
10824
11852
  // apps/daemon/src/config.ts
10825
- import { readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
10826
- import { join as join4, dirname as dirname3 } from "path";
10827
- import { homedir as homedir2 } from "os";
10828
11853
  import { randomUUID as randomUUID2 } from "crypto";
11854
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
11855
+ import { homedir as homedir2 } from "os";
11856
+ import { dirname as dirname3, join as join4 } from "path";
10829
11857
  function readConfig() {
10830
11858
  try {
10831
11859
  return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
@@ -10876,31 +11904,45 @@ var cloud_capabilities_exports = {};
10876
11904
  __export(cloud_capabilities_exports, {
10877
11905
  createCloudCapabilities: () => createCloudCapabilities
10878
11906
  });
10879
- function createCloudCapabilities(apiKey, trackUsage, logRequest) {
11907
+ function createCloudCapabilities(getKey, refreshKey, trackUsage, logRequest) {
10880
11908
  const CLOUD_API_URL = getCloudUrl();
10881
- const headers = {
10882
- Authorization: `Bearer ${apiKey}`,
10883
- "Content-Type": "application/json"
10884
- };
11909
+ async function cloudFetch(path, reqBody) {
11910
+ const apiKey = await getKey();
11911
+ if (!apiKey) throw new Error("No API key available");
11912
+ const start = performance.now();
11913
+ let res = await fetch(`${CLOUD_API_URL}${path}`, {
11914
+ method: "POST",
11915
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
11916
+ body: reqBody
11917
+ });
11918
+ let resText = await res.text();
11919
+ let duration3 = Math.round(performance.now() - start);
11920
+ logRequest?.("POST", path, res.status, duration3, "cloud", reqBody, resText);
11921
+ if (res.status === 401) {
11922
+ const newKey = await refreshKey();
11923
+ if (newKey) {
11924
+ const retryStart = performance.now();
11925
+ res = await fetch(`${CLOUD_API_URL}${path}`, {
11926
+ method: "POST",
11927
+ headers: { Authorization: `Bearer ${newKey}`, "Content-Type": "application/json" },
11928
+ body: reqBody
11929
+ });
11930
+ resText = await res.text();
11931
+ duration3 = Math.round(performance.now() - retryStart);
11932
+ logRequest?.("POST", path, res.status, duration3, "cloud-retry", reqBody, resText);
11933
+ }
11934
+ }
11935
+ return { res, resText };
11936
+ }
10885
11937
  return {
10886
11938
  async embedTexts(texts, isQuery) {
10887
- const start = performance.now();
10888
11939
  const reqBody = JSON.stringify({
10889
11940
  input: texts,
10890
11941
  model: "voyage-code-3",
10891
11942
  input_type: isQuery ? "query" : "document"
10892
11943
  });
10893
- const res = await fetch(`${CLOUD_API_URL}/api/proxy/embed`, {
10894
- method: "POST",
10895
- headers,
10896
- body: reqBody
10897
- });
10898
- const resText = await res.text();
10899
- const duration3 = Math.round(performance.now() - start);
10900
- logRequest?.("POST", "/api/proxy/embed", res.status, duration3, "cloud", reqBody, resText);
10901
- if (!res.ok) {
10902
- throw new Error(`Cloud embed failed (${res.status}): ${resText}`);
10903
- }
11944
+ const { res, resText } = await cloudFetch("/api/proxy/embed", reqBody);
11945
+ if (!res.ok) throw new Error(`Cloud embed failed (${res.status}): ${resText}`);
10904
11946
  const data = JSON.parse(resText);
10905
11947
  trackUsage?.("embedding_requests");
10906
11948
  trackUsage?.("embedding_chunks", texts.length);
@@ -10914,25 +11956,18 @@ function createCloudCapabilities(apiKey, trackUsage, logRequest) {
10914
11956
  "",
10915
11957
  content.slice(0, 2e3)
10916
11958
  ].filter(Boolean).join("\n");
10917
- const start = performance.now();
10918
11959
  const reqBody = JSON.stringify({
10919
11960
  messages: [
10920
- { role: "system", content: "You are a code analyst. Output ONLY a 1-sentence purpose summary for the given file. No preamble." },
11961
+ {
11962
+ role: "system",
11963
+ content: "You are a code analyst. Output ONLY a 1-sentence purpose summary for the given file. No preamble."
11964
+ },
10921
11965
  { role: "user", content: prompt }
10922
11966
  ],
10923
11967
  max_tokens: 128
10924
11968
  });
10925
- const res = await fetch(`${CLOUD_API_URL}/api/proxy/chat`, {
10926
- method: "POST",
10927
- headers,
10928
- body: reqBody
10929
- });
10930
- const resText = await res.text();
10931
- const duration3 = Math.round(performance.now() - start);
10932
- logRequest?.("POST", "/api/proxy/chat", res.status, duration3, "cloud", reqBody, resText);
10933
- if (!res.ok) {
10934
- throw new Error(`Cloud purpose failed (${res.status}): ${resText}`);
10935
- }
11969
+ const { res, resText } = await cloudFetch("/api/proxy/chat", reqBody);
11970
+ if (!res.ok) throw new Error(`Cloud purpose failed (${res.status}): ${resText}`);
10936
11971
  const data = JSON.parse(resText);
10937
11972
  trackUsage?.("purpose_requests");
10938
11973
  return data.choices?.[0]?.message?.content?.trim() ?? "";
@@ -15655,7 +16690,7 @@ function flattenError(error3, mapper = (issue2) => issue2.message) {
15655
16690
  }
15656
16691
  return { formErrors, fieldErrors };
15657
16692
  }
15658
- function formatError(error3, _mapper) {
16693
+ function formatError2(error3, _mapper) {
15659
16694
  const mapper = _mapper || function(issue2) {
15660
16695
  return issue2.message;
15661
16696
  };
@@ -19301,7 +20336,7 @@ var init_errors4 = __esm({
19301
20336
  inst.name = "ZodError";
19302
20337
  Object.defineProperties(inst, {
19303
20338
  format: {
19304
- value: (mapper) => formatError(inst, mapper)
20339
+ value: (mapper) => formatError2(inst, mapper)
19305
20340
  // enumerable: false,
19306
20341
  },
19307
20342
  flatten: {
@@ -32382,7 +33417,17 @@ __export(mcp_exports, {
32382
33417
  });
32383
33418
  import { randomUUID as randomUUID3 } from "crypto";
32384
33419
  function logMcp(method, path, status, duration3, reqBody, resSize, resBody, trace) {
32385
- getRawDb().prepare(LOG_SQL).run(randomUUID3(), method, path, status, duration3, reqBody ?? null, resSize ?? null, resBody ?? null, trace ?? null);
33420
+ getRawDb().prepare(LOG_SQL).run(
33421
+ randomUUID3(),
33422
+ method,
33423
+ path,
33424
+ status,
33425
+ duration3,
33426
+ reqBody ?? null,
33427
+ resSize ?? null,
33428
+ resBody ?? null,
33429
+ trace ?? null
33430
+ );
32386
33431
  }
32387
33432
  function text2(s) {
32388
33433
  return { content: [{ type: "text", text: s }] };
@@ -32401,7 +33446,7 @@ function createMcpServer(db, caps) {
32401
33446
  server.registerTool(
32402
33447
  "get_context",
32403
33448
  {
32404
- description: "Prefer this tool over Grep/Glob when searching for files relevant to a task across the codebase. Returns ranked files, imports, and co-change clusters for a development goal. Skip for simple lookups where you already know the file path or location.",
33449
+ description: "Returns ranked files with import dependency chains and git co-change clusters for a development goal. Surfaces structural relationships (imports, co-changes, exports) that keyword search alone won't find. Use when exploring architecture, tracing data flow, assessing change impact, or navigating an unfamiliar codebase. Skip for simple lookups where you already know the file path.",
32405
33450
  inputSchema: { repo_path: external_exports.string(), goal: external_exports.string() }
32406
33451
  },
32407
33452
  async ({ repo_path, goal }) => {
@@ -32416,11 +33461,29 @@ function createMcpServer(db, caps) {
32416
33461
  const result = await buildContext(db, repoId, goal, caps, trace, { useEmbeddings });
32417
33462
  const duration3 = Math.round(performance.now() - start);
32418
33463
  const reqBody = JSON.stringify({ repo_path, goal });
32419
- logMcp("MCP", "/tool/get_context", 200, duration3, reqBody, result.context_pack.length, result.context_pack, trace.serialize());
33464
+ logMcp(
33465
+ "MCP",
33466
+ "/tool/get_context",
33467
+ 200,
33468
+ duration3,
33469
+ reqBody,
33470
+ result.context_pack.length,
33471
+ result.context_pack,
33472
+ trace.serialize()
33473
+ );
32420
33474
  return text2(result.context_pack);
32421
33475
  } catch (e) {
32422
33476
  const duration3 = Math.round(performance.now() - start);
32423
- logMcp("MCP", "/tool/get_context", 500, duration3, JSON.stringify({ repo_path, goal }), void 0, e.message, trace.serialize());
33477
+ logMcp(
33478
+ "MCP",
33479
+ "/tool/get_context",
33480
+ 500,
33481
+ duration3,
33482
+ JSON.stringify({ repo_path, goal }),
33483
+ void 0,
33484
+ e.message,
33485
+ trace.serialize()
33486
+ );
32424
33487
  return error2(e.message);
32425
33488
  }
32426
33489
  }
@@ -32458,7 +33521,7 @@ function createMcpServer(db, caps) {
32458
33521
  const repo = repoQueries.getByPath(db, repo_path);
32459
33522
  if (!repo) {
32460
33523
  logMcp("MCP", "/tool/get_status", 404, Math.round(performance.now() - start), JSON.stringify({ repo_path }));
32461
- return error2("repo not found at " + repo_path);
33524
+ return error2(`repo not found at ${repo_path}`);
32462
33525
  }
32463
33526
  const status = await getRepoStatus(db, repo.id);
32464
33527
  const body = JSON.stringify(status, null, 2);
@@ -32488,11 +33551,29 @@ function createMcpServer(db, caps) {
32488
33551
  const result = await runIndex(db, repoId, caps, force ?? false, void 0, trace);
32489
33552
  const body = JSON.stringify(result, null, 2);
32490
33553
  const duration3 = Math.round(performance.now() - start);
32491
- logMcp("MCP", "/tool/index_repo", 200, duration3, JSON.stringify({ repo_path, force }), body.length, body, trace.serialize());
33554
+ logMcp(
33555
+ "MCP",
33556
+ "/tool/index_repo",
33557
+ 200,
33558
+ duration3,
33559
+ JSON.stringify({ repo_path, force }),
33560
+ body.length,
33561
+ body,
33562
+ trace.serialize()
33563
+ );
32492
33564
  return text2(body);
32493
33565
  } catch (e) {
32494
33566
  const duration3 = Math.round(performance.now() - start);
32495
- logMcp("MCP", "/tool/index_repo", 500, duration3, JSON.stringify({ repo_path, force }), void 0, e.message, trace.serialize());
33567
+ logMcp(
33568
+ "MCP",
33569
+ "/tool/index_repo",
33570
+ 500,
33571
+ duration3,
33572
+ JSON.stringify({ repo_path, force }),
33573
+ void 0,
33574
+ e.message,
33575
+ trace.serialize()
33576
+ );
32496
33577
  return error2(e.message);
32497
33578
  }
32498
33579
  }
@@ -34816,9 +35897,9 @@ var server_exports = {};
34816
35897
  __export(server_exports, {
34817
35898
  createApp: () => createApp
34818
35899
  });
34819
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync, watch as watch2 } from "fs";
34820
- import { join as join5, extname as extname3 } from "path";
35900
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync, watch as watch2, writeFileSync as writeFileSync2 } from "fs";
34821
35901
  import { homedir as homedir3 } from "os";
35902
+ import { extname as extname3, join as join5 } from "path";
34822
35903
  function getDbSizeMb() {
34823
35904
  const dbPath = join5(homedir3(), ".lens", "data.db");
34824
35905
  try {
@@ -34849,7 +35930,7 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34849
35930
  const app = new Hono2();
34850
35931
  const telemetryEnabled = isTelemetryEnabled();
34851
35932
  setTelemetryEnabled(telemetryEnabled);
34852
- const { telemetry_id, first_run } = ensureTelemetryId();
35933
+ const { first_run } = ensureTelemetryId();
34853
35934
  if (first_run) {
34854
35935
  console.log(
34855
35936
  "[LENS] Anonymous telemetry is enabled. No PII, no repo paths, no code.\n[LENS] Opt out: lens config set telemetry false"
@@ -34925,18 +36006,6 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34925
36006
  const trace = c.get("trace");
34926
36007
  const { root_path, name, remote_url } = await c.req.json();
34927
36008
  if (!root_path) return c.json({ error: "root_path required" }, 400);
34928
- trace.step("quotaCheck");
34929
- const currentRepos = listRepos(db).length;
34930
- const maxRepos = quotaCache?.quota?.maxRepos ?? 50;
34931
- trace.end("quotaCheck", `${currentRepos}/${maxRepos}`);
34932
- if (currentRepos >= maxRepos) {
34933
- return c.json({
34934
- error: "Repo limit reached",
34935
- current: currentRepos,
34936
- limit: maxRepos,
34937
- plan: quotaCache?.plan ?? "unknown"
34938
- }, 429);
34939
- }
34940
36009
  trace.step("registerRepo");
34941
36010
  const result = registerRepo(db, root_path, name, remote_url);
34942
36011
  trace.end("registerRepo", result.created ? "created" : "existing");
@@ -34952,37 +36021,64 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
34952
36021
  const tasks = [];
34953
36022
  if (repo?.enable_vocab_clusters && quotaRemaining("embeddingChunks") > 0) {
34954
36023
  bg.step("vocabClusters");
34955
- tasks.push(buildVocabClusters(db, result.repo_id, caps).then(() => {
34956
- bg.end("vocabClusters");
34957
- results.vocabClusters = "done";
34958
- }));
36024
+ tasks.push(
36025
+ buildVocabClusters(db, result.repo_id, caps).then(() => {
36026
+ bg.end("vocabClusters");
36027
+ results.vocabClusters = "done";
36028
+ })
36029
+ );
34959
36030
  } else {
34960
36031
  bg.add("vocabClusters", 0, !repo?.enable_vocab_clusters ? "disabled" : "quota exceeded");
34961
36032
  }
34962
36033
  if (repo?.enable_embeddings && quotaRemaining("embeddingChunks") > 0) {
34963
36034
  bg.step("embeddings");
34964
- tasks.push(ensureEmbedded(db, result.repo_id, caps).then((er) => {
34965
- bg.end("embeddings", `${er.embedded_count} embedded`);
34966
- results.embeddings = er;
34967
- }));
36035
+ tasks.push(
36036
+ ensureEmbedded(db, result.repo_id, caps).then((er) => {
36037
+ bg.end("embeddings", `${er.embedded_count} embedded`);
36038
+ results.embeddings = er;
36039
+ })
36040
+ );
34968
36041
  } else {
34969
36042
  bg.add("embeddings", 0, !repo?.enable_embeddings ? "disabled" : "quota exceeded");
34970
36043
  }
34971
36044
  if (repo?.enable_summaries && quotaRemaining("purposeRequests") > 0) {
34972
36045
  bg.step("purpose");
34973
- tasks.push(enrichPurpose(db, result.repo_id, caps).then((pr) => {
34974
- bg.end("purpose", `${pr.enriched} enriched`);
34975
- results.purpose = pr;
34976
- }));
36046
+ tasks.push(
36047
+ enrichPurpose(db, result.repo_id, caps).then((pr) => {
36048
+ bg.end("purpose", `${pr.enriched} enriched`);
36049
+ results.purpose = pr;
36050
+ })
36051
+ );
34977
36052
  } else {
34978
36053
  bg.add("purpose", 0, !repo?.enable_summaries ? "disabled" : "quota exceeded");
34979
36054
  }
34980
36055
  await Promise.all(tasks);
34981
36056
  const resBody = JSON.stringify(results);
34982
36057
  const bgDuration = Math.round(performance.now() - bgStart);
34983
- logQueries.insert(db, "BG", `/enrichment/${result.repo_id}`, 200, bgDuration, "system", reqBody, resBody.length, resBody, bg.serialize());
36058
+ logQueries.insert(
36059
+ db,
36060
+ "BG",
36061
+ `/enrichment/${result.repo_id}`,
36062
+ 200,
36063
+ bgDuration,
36064
+ "system",
36065
+ reqBody,
36066
+ resBody.length,
36067
+ resBody,
36068
+ bg.serialize()
36069
+ );
34984
36070
  }).catch((e) => {
34985
- logQueries.insert(db, "BG", `/enrichment/${result.repo_id}`, 500, 0, "system", JSON.stringify({ repo_id: result.repo_id }), void 0, String(e));
36071
+ logQueries.insert(
36072
+ db,
36073
+ "BG",
36074
+ `/enrichment/${result.repo_id}`,
36075
+ 500,
36076
+ 0,
36077
+ "system",
36078
+ JSON.stringify({ repo_id: result.repo_id }),
36079
+ void 0,
36080
+ String(e)
36081
+ );
34986
36082
  });
34987
36083
  }
34988
36084
  trace.step("startWatcher");
@@ -35099,6 +36195,20 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35099
36195
  return c.json({ error: e.message }, 500);
35100
36196
  }
35101
36197
  });
36198
+ trackRoute("POST", "/eval/run");
36199
+ app.post("/eval/run", async (c) => {
36200
+ try {
36201
+ const trace = c.get("trace");
36202
+ const { repo_id, filter_kind } = await c.req.json();
36203
+ if (!repo_id) return c.json({ error: "repo_id required" }, 400);
36204
+ trace.step("runEval");
36205
+ const summary = await runEval(db, repo_id, { filterKind: filter_kind });
36206
+ trace.end("runEval", `${summary.total} queries`);
36207
+ return c.json(summary);
36208
+ } catch (e) {
36209
+ return c.json({ error: e.message }, 500);
36210
+ }
36211
+ });
35102
36212
  trackRoute("POST", "/index/run");
35103
36213
  app.post("/index/run", async (c) => {
35104
36214
  try {
@@ -35114,35 +36224,52 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35114
36224
  const tasks = [];
35115
36225
  if (repo?.enable_vocab_clusters && quotaRemaining("embeddingChunks") > 0) {
35116
36226
  bg.step("vocabClusters");
35117
- tasks.push(buildVocabClusters(db, repo_id, caps).then(() => {
35118
- bg.end("vocabClusters");
35119
- results.vocabClusters = "done";
35120
- }));
36227
+ tasks.push(
36228
+ buildVocabClusters(db, repo_id, caps).then(() => {
36229
+ bg.end("vocabClusters");
36230
+ results.vocabClusters = "done";
36231
+ })
36232
+ );
35121
36233
  } else {
35122
36234
  bg.add("vocabClusters", 0, !repo?.enable_vocab_clusters ? "disabled" : "quota exceeded");
35123
36235
  }
35124
36236
  if (repo?.enable_embeddings && quotaRemaining("embeddingChunks") > 0) {
35125
36237
  bg.step("embeddings");
35126
- tasks.push(ensureEmbedded(db, repo_id, caps).then((er) => {
35127
- bg.end("embeddings", `${er.embedded_count} embedded`);
35128
- results.embeddings = er;
35129
- }));
36238
+ tasks.push(
36239
+ ensureEmbedded(db, repo_id, caps).then((er) => {
36240
+ bg.end("embeddings", `${er.embedded_count} embedded`);
36241
+ results.embeddings = er;
36242
+ })
36243
+ );
35130
36244
  } else {
35131
36245
  bg.add("embeddings", 0, !repo?.enable_embeddings ? "disabled" : "quota exceeded");
35132
36246
  }
35133
36247
  if (repo?.enable_summaries && quotaRemaining("purposeRequests") > 0) {
35134
36248
  bg.step("purpose");
35135
- tasks.push(enrichPurpose(db, repo_id, caps).then((pr) => {
35136
- bg.end("purpose", `${pr.enriched} enriched`);
35137
- results.purpose = pr;
35138
- }));
36249
+ tasks.push(
36250
+ enrichPurpose(db, repo_id, caps).then((pr) => {
36251
+ bg.end("purpose", `${pr.enriched} enriched`);
36252
+ results.purpose = pr;
36253
+ })
36254
+ );
35139
36255
  } else {
35140
36256
  bg.add("purpose", 0, !repo?.enable_summaries ? "disabled" : "quota exceeded");
35141
36257
  }
35142
36258
  Promise.all(tasks).then(() => {
35143
36259
  const resBody = JSON.stringify(results);
35144
36260
  const bgDuration = Math.round(performance.now() - bgStart);
35145
- logQueries.insert(db, "BG", `/enrichment/${repo_id}`, 200, bgDuration, "system", reqBody, resBody.length, resBody, bg.serialize());
36261
+ logQueries.insert(
36262
+ db,
36263
+ "BG",
36264
+ `/enrichment/${repo_id}`,
36265
+ 200,
36266
+ bgDuration,
36267
+ "system",
36268
+ reqBody,
36269
+ resBody.length,
36270
+ resBody,
36271
+ bg.serialize()
36272
+ );
35146
36273
  }).catch((e) => {
35147
36274
  logQueries.insert(db, "BG", `/enrichment/${repo_id}`, 500, 0, "system", reqBody, void 0, String(e));
35148
36275
  });
@@ -35288,24 +36415,34 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35288
36415
  expires_at: Math.floor(Date.now() / 1e3) + (data.expires_in ?? 3600)
35289
36416
  };
35290
36417
  }
35291
- const repoClients = /* @__PURE__ */ new Set();
36418
+ const sseClients = /* @__PURE__ */ new Set();
35292
36419
  const encoder = new TextEncoder();
35293
- function emitRepoEvent() {
35294
- for (const ctrl of repoClients) {
36420
+ function emitSSE(type) {
36421
+ const payload = encoder.encode(`data: ${JSON.stringify({ type })}
36422
+
36423
+ `);
36424
+ for (const ctrl of sseClients) {
35295
36425
  try {
35296
- ctrl.enqueue(encoder.encode("data: repo-changed\n\n"));
36426
+ ctrl.enqueue(payload);
35297
36427
  } catch {
35298
- repoClients.delete(ctrl);
36428
+ sseClients.delete(ctrl);
35299
36429
  }
35300
36430
  }
35301
36431
  }
35302
- trackRoute("GET", "/api/repo/events");
35303
- app.get("/api/repo/events", (c) => {
36432
+ function emitRepoEvent() {
36433
+ emitSSE("repo");
36434
+ }
36435
+ function emitAuthEvent() {
36436
+ emitSSE("auth");
36437
+ }
36438
+ trackRoute("GET", "/api/events");
36439
+ app.get("/api/events", (_c) => {
35304
36440
  const stream = new ReadableStream({
35305
36441
  start(ctrl) {
35306
- repoClients.add(ctrl);
36442
+ sseClients.add(ctrl);
35307
36443
  },
35308
- cancel() {
36444
+ cancel(ctrl) {
36445
+ sseClients.delete(ctrl);
35309
36446
  }
35310
36447
  });
35311
36448
  return new Response(stream, {
@@ -35316,17 +36453,7 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35316
36453
  }
35317
36454
  });
35318
36455
  });
35319
- const authClients = /* @__PURE__ */ new Set();
35320
36456
  const authDir = join5(homedir3(), ".lens");
35321
- function emitAuthEvent() {
35322
- for (const ctrl of authClients) {
35323
- try {
35324
- ctrl.enqueue(encoder.encode("data: auth-changed\n\n"));
35325
- } catch {
35326
- authClients.delete(ctrl);
35327
- }
35328
- }
35329
- }
35330
36457
  try {
35331
36458
  watch2(authDir, (_, filename) => {
35332
36459
  if (filename && filename !== "auth.json") return;
@@ -35334,23 +36461,6 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35334
36461
  });
35335
36462
  } catch {
35336
36463
  }
35337
- trackRoute("GET", "/api/auth/events");
35338
- app.get("/api/auth/events", (c) => {
35339
- const stream = new ReadableStream({
35340
- start(ctrl) {
35341
- authClients.add(ctrl);
35342
- },
35343
- cancel() {
35344
- }
35345
- });
35346
- return new Response(stream, {
35347
- headers: {
35348
- "Content-Type": "text/event-stream",
35349
- "Cache-Control": "no-cache",
35350
- Connection: "keep-alive"
35351
- }
35352
- });
35353
- });
35354
36464
  trackRoute("POST", "/api/auth/notify");
35355
36465
  app.post("/api/auth/notify", async (c) => {
35356
36466
  await refreshQuotaCache();
@@ -35602,7 +36712,9 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35602
36712
  const search = c.req.query("search") || void 0;
35603
36713
  const meta = metadataQueries.getByRepo(db, id);
35604
36714
  const raw2 = getRawDb();
35605
- 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);
36715
+ const chunkCounts = raw2.prepare(
36716
+ "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"
36717
+ ).all(id);
35606
36718
  const countMap = new Map(chunkCounts.map((r) => [r.path, r]));
35607
36719
  let all = meta.map((m) => {
35608
36720
  const counts = countMap.get(m.path);
@@ -35637,12 +36749,16 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35637
36749
  const meta = allMeta.find((m) => m.path === filePath);
35638
36750
  if (!meta) return c.json({ error: "file not found" }, 404);
35639
36751
  const raw2 = getRawDb();
35640
- 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);
36752
+ const chunkRow = raw2.prepare(
36753
+ "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 = ?"
36754
+ ).get(id, filePath);
35641
36755
  const allImports = importQueries.getBySources(db, id, [filePath]);
35642
36756
  const allImporters = importQueries.getByTargets(db, id, [filePath]);
35643
36757
  const gitStats = statsQueries.getByRepo(db, id);
35644
36758
  const fileStat = gitStats.get(filePath);
35645
- 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);
36759
+ const cochangeRows = raw2.prepare(
36760
+ "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"
36761
+ ).all(id, filePath, filePath);
35646
36762
  const cochanges = cochangeRows.map((r) => ({
35647
36763
  path: r.path_a === filePath ? r.path_b : r.path_a,
35648
36764
  count: r.cochange_count
@@ -35682,7 +36798,9 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35682
36798
  const raw2 = getRawDb();
35683
36799
  const where = pathFilter ? "WHERE repo_id = ? AND path = ?" : "WHERE repo_id = ?";
35684
36800
  const params = pathFilter ? [id, pathFilter] : [id];
35685
- 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);
36801
+ const rows = raw2.prepare(
36802
+ `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 ?`
36803
+ ).all(...params, limit, offset);
35686
36804
  const totalRow = raw2.prepare(`SELECT count(*) as count FROM chunks ${where}`).get(...params);
35687
36805
  return c.json({
35688
36806
  chunks: rows.map((r) => ({ ...r, has_embedding: r.has_embedding === 1 })),
@@ -35698,7 +36816,9 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35698
36816
  const id = c.req.param("id");
35699
36817
  const chunkId = c.req.param("chunkId");
35700
36818
  const raw2 = getRawDb();
35701
- 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);
36819
+ const row = raw2.prepare(
36820
+ "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 = ?"
36821
+ ).get(chunkId, id);
35702
36822
  if (!row) return c.json({ error: "chunk not found" }, 404);
35703
36823
  return c.json({ ...row, has_embedding: row.has_embedding === 1 });
35704
36824
  } catch (e) {
@@ -35739,7 +36859,9 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35739
36859
  app.get("/api/dashboard/tables", (c) => {
35740
36860
  try {
35741
36861
  const raw2 = getRawDb();
35742
- const tables = raw2.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle%' ORDER BY name").all();
36862
+ const tables = raw2.prepare(
36863
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle%' ORDER BY name"
36864
+ ).all();
35743
36865
  const result = tables.map((t) => {
35744
36866
  const row = raw2.prepare(`SELECT count(*) as count FROM "${t.name}"`).get();
35745
36867
  return { name: t.name, count: row.count };
@@ -35875,7 +36997,8 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35875
36997
  if (apiKey) {
35876
36998
  const { createCloudCapabilities: createCloudCapabilities2 } = await Promise.resolve().then(() => (init_cloud_capabilities(), cloud_capabilities_exports));
35877
36999
  caps = createCloudCapabilities2(
35878
- apiKey,
37000
+ () => readApiKey(),
37001
+ provisionApiKey,
35879
37002
  (counter, amount) => {
35880
37003
  try {
35881
37004
  usageQueries.increment(db, counter, amount);
@@ -35905,7 +37028,7 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35905
37028
  caps = void 0;
35906
37029
  }
35907
37030
  } catch {
35908
- quotaCache = { plan: "free", usage: {}, quota: {}, fetchedAt: Date.now() };
37031
+ quotaCache = { plan: "free", usage: {}, quota: {}, subscription: null, fetchedAt: Date.now() };
35909
37032
  caps = void 0;
35910
37033
  }
35911
37034
  }
@@ -35934,7 +37057,10 @@ function createApp(db, dashboardDist, initialCaps, initialPlanData) {
35934
37057
  signal: AbortSignal.timeout(1e4)
35935
37058
  });
35936
37059
  if (res.ok) {
35937
- telemetryQueries.markSynced(db, unsynced.map((e) => e.id));
37060
+ telemetryQueries.markSynced(
37061
+ db,
37062
+ unsynced.map((e) => e.id)
37063
+ );
35938
37064
  }
35939
37065
  } catch {
35940
37066
  }
@@ -35953,16 +37079,15 @@ var TEMPLATE, startedAt, MIME_TYPES, QUOTA_TTL, quotaCache;
35953
37079
  var init_server3 = __esm({
35954
37080
  "apps/daemon/src/server.ts"() {
35955
37081
  "use strict";
37082
+ init_dist();
35956
37083
  init_dist2();
35957
37084
  init_config();
35958
- init_dist();
35959
37085
  TEMPLATE = `<!-- LENS \u2014 Repo Context Daemon -->
35960
37086
  ## LENS Context
35961
37087
 
35962
- This repo is indexed by LENS. Use the MCP context tool or run:
35963
- \`\`\`
35964
- lens context "<your goal>"
35965
- \`\`\``;
37088
+ This repo is indexed by LENS. When exploring architecture, tracing data flow, or assessing change impact, call \`mcp__lens__get_context\` \u2014 it returns ranked files with import chains and co-change clusters that keyword search alone won't surface.
37089
+
37090
+ CLI alternative: \`lens context "<your goal>"\``;
35966
37091
  startedAt = Date.now();
35967
37092
  MIME_TYPES = {
35968
37093
  ".html": "text/html",
@@ -36548,13 +37673,14 @@ var init_dist3 = __esm({
36548
37673
  // apps/daemon/src/index.ts
36549
37674
  init_dist();
36550
37675
  init_config();
36551
- import { writeFileSync as writeFileSync3, unlinkSync, readFileSync as readFileSync3, statSync as statSync2, watchFile as watchFile2, openSync } from "fs";
36552
- import { join as join6 } from "path";
36553
- import { homedir as homedir4 } from "os";
36554
37676
  import { spawn } from "child_process";
37677
+ import { openSync, readFileSync as readFileSync3, statSync as statSync2, unlinkSync, watchFile as watchFile2, writeFileSync as writeFileSync3 } from "fs";
37678
+ import { homedir as homedir4 } from "os";
37679
+ import { join as join6 } from "path";
36555
37680
  var LENS_DIR = join6(homedir4(), ".lens");
36556
37681
  var PID_FILE = join6(LENS_DIR, "daemon.pid");
36557
37682
  var LOG_FILE = join6(LENS_DIR, "daemon.log");
37683
+ var AUTH_FILE = join6(LENS_DIR, "auth.json");
36558
37684
  function writePid() {
36559
37685
  writeFileSync3(PID_FILE, String(process.pid));
36560
37686
  }
@@ -36564,76 +37690,97 @@ function removePid() {
36564
37690
  } catch {
36565
37691
  }
36566
37692
  }
36567
- async function ensureApiKey(data) {
36568
- if (data.api_key) return data.api_key;
36569
- if (!data.access_token) return void 0;
36570
- const cloudUrl = getCloudUrl();
37693
+ function readApiKeyFromDisk() {
37694
+ try {
37695
+ const data = JSON.parse(readFileSync3(AUTH_FILE, "utf-8"));
37696
+ return data.api_key || null;
37697
+ } catch {
37698
+ return null;
37699
+ }
37700
+ }
37701
+ async function provisionNewKey() {
36571
37702
  try {
37703
+ const data = JSON.parse(readFileSync3(AUTH_FILE, "utf-8"));
37704
+ if (!data.access_token) return null;
37705
+ const cloudUrl = getCloudUrl();
36572
37706
  const res = await fetch(`${cloudUrl}/auth/key`, {
36573
37707
  headers: { Authorization: `Bearer ${data.access_token}` }
36574
37708
  });
36575
- if (!res.ok) {
36576
- console.error(`[LENS] API key provisioning failed (${res.status})`);
36577
- return void 0;
36578
- }
37709
+ if (!res.ok) return null;
36579
37710
  const { api_key } = await res.json();
36580
37711
  data.api_key = api_key;
36581
- const authPath = join6(homedir4(), ".lens", "auth.json");
36582
- writeFileSync3(authPath, JSON.stringify(data, null, 2), { mode: 384 });
36583
- console.error("[LENS] API key auto-provisioned");
37712
+ writeFileSync3(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
37713
+ console.error("[LENS] API key re-provisioned");
36584
37714
  return api_key;
36585
- } catch (e) {
36586
- console.error(`[LENS] API key provisioning error: ${e?.message}`);
36587
- return void 0;
37715
+ } catch {
37716
+ return null;
36588
37717
  }
36589
37718
  }
36590
37719
  async function loadCapabilities(db) {
36591
37720
  try {
36592
- const authPath = join6(homedir4(), ".lens", "auth.json");
36593
- const data = JSON.parse(readFileSync3(authPath, "utf-8"));
36594
- const apiKey = await ensureApiKey(data);
36595
- if (!apiKey) return {};
37721
+ let apiKey = readApiKeyFromDisk();
37722
+ if (!apiKey) {
37723
+ apiKey = await provisionNewKey();
37724
+ if (!apiKey) return {};
37725
+ }
36596
37726
  const cloudUrl = getCloudUrl();
36597
37727
  const res = await fetch(`${cloudUrl}/api/usage/current`, {
36598
37728
  headers: { Authorization: `Bearer ${apiKey}` }
36599
37729
  });
37730
+ if (res.status === 401) {
37731
+ apiKey = await provisionNewKey();
37732
+ if (!apiKey) return {};
37733
+ const retry = await fetch(`${cloudUrl}/api/usage/current`, {
37734
+ headers: { Authorization: `Bearer ${apiKey}` }
37735
+ });
37736
+ if (!retry.ok) {
37737
+ console.error(`[LENS] Plan check failed after re-provision (${retry.status})`);
37738
+ return {};
37739
+ }
37740
+ const usageData2 = await retry.json();
37741
+ return buildCapsResult(db, usageData2);
37742
+ }
36600
37743
  if (!res.ok) {
36601
37744
  console.error(`[LENS] Plan check failed (${res.status}), capabilities disabled`);
36602
37745
  return {};
36603
37746
  }
36604
37747
  const usageData = await res.json();
36605
- const planData = {
36606
- plan: usageData.plan ?? "free",
36607
- usage: usageData.usage ?? {},
36608
- quota: usageData.quota ?? {}
36609
- };
36610
- if (usageData.plan !== "pro") {
36611
- console.error(`[LENS] Plan: ${usageData.plan ?? "free"} \u2014 Pro features disabled`);
36612
- return { planData };
36613
- }
36614
- const { createCloudCapabilities: createCloudCapabilities2 } = await Promise.resolve().then(() => (init_cloud_capabilities(), cloud_capabilities_exports));
36615
- const { usageQueries: usageQueries2, logQueries: logQueries2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
36616
- const caps = createCloudCapabilities2(
36617
- apiKey,
36618
- (counter, amount) => {
36619
- try {
36620
- usageQueries2.increment(db, counter, amount);
36621
- } catch {
36622
- }
36623
- },
36624
- (method, path, status, duration3, source, reqBody, resBody) => {
36625
- try {
36626
- logQueries2.insert(db, method, path, status, duration3, source, reqBody, resBody?.length, resBody);
36627
- } catch {
36628
- }
36629
- }
36630
- );
36631
- console.error("[LENS] Cloud capabilities enabled (Pro plan)");
36632
- return { caps, planData };
37748
+ return buildCapsResult(db, usageData);
36633
37749
  } catch {
36634
37750
  return {};
36635
37751
  }
36636
37752
  }
37753
+ async function buildCapsResult(db, usageData) {
37754
+ const planData = {
37755
+ plan: usageData.plan ?? "free",
37756
+ usage: usageData.usage ?? {},
37757
+ quota: usageData.quota ?? {}
37758
+ };
37759
+ if (usageData.plan !== "pro") {
37760
+ console.error(`[LENS] Plan: ${usageData.plan ?? "free"} \u2014 Pro features disabled`);
37761
+ return { planData };
37762
+ }
37763
+ const { createCloudCapabilities: createCloudCapabilities2 } = await Promise.resolve().then(() => (init_cloud_capabilities(), cloud_capabilities_exports));
37764
+ const { usageQueries: usageQueries2, logQueries: logQueries2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
37765
+ const caps = createCloudCapabilities2(
37766
+ () => Promise.resolve(readApiKeyFromDisk()),
37767
+ provisionNewKey,
37768
+ (counter, amount) => {
37769
+ try {
37770
+ usageQueries2.increment(db, counter, amount);
37771
+ } catch {
37772
+ }
37773
+ },
37774
+ (method, path, status, duration3, source, reqBody, resBody) => {
37775
+ try {
37776
+ logQueries2.insert(db, method, path, status, duration3, source, reqBody, resBody?.length, resBody);
37777
+ } catch {
37778
+ }
37779
+ }
37780
+ );
37781
+ console.error("[LENS] Cloud capabilities enabled (Pro plan)");
37782
+ return { caps, planData };
37783
+ }
36637
37784
  async function main() {
36638
37785
  const db = openDb();
36639
37786
  const { caps, planData } = await loadCapabilities(db);
@@ -36665,11 +37812,11 @@ async function main() {
36665
37812
  var shutdown = shutdown2;
36666
37813
  const { createApp: createApp2 } = await Promise.resolve().then(() => (init_server3(), server_exports));
36667
37814
  const { serve: serve2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports2));
36668
- const { resolve: resolve4, dirname: dirname5 } = await import("path");
37815
+ const { resolve: resolve4, dirname: dirname4 } = await import("path");
36669
37816
  const { existsSync: existsSync3 } = await import("fs");
36670
37817
  const { fileURLToPath } = await import("url");
36671
37818
  const port = Number(process.env.LENS_PORT) || 4111;
36672
- const selfDir = dirname5(fileURLToPath(import.meta.url));
37819
+ const selfDir = dirname4(fileURLToPath(import.meta.url));
36673
37820
  const candidates = [
36674
37821
  resolve4(selfDir, "../../dashboard/dist"),
36675
37822
  // monorepo dev