@liendev/lien 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -378,8 +378,8 @@ var init_errors = __esm({
378
378
  });
379
379
 
380
380
  // src/config/service.ts
381
- import fs8 from "fs/promises";
382
- import path8 from "path";
381
+ import fs9 from "fs/promises";
382
+ import path9 from "path";
383
383
  var ConfigService, configService;
384
384
  var init_service = __esm({
385
385
  "src/config/service.ts"() {
@@ -401,13 +401,13 @@ var init_service = __esm({
401
401
  async load(rootDir = process.cwd()) {
402
402
  const configPath = this.getConfigPath(rootDir);
403
403
  try {
404
- const configContent = await fs8.readFile(configPath, "utf-8");
404
+ const configContent = await fs9.readFile(configPath, "utf-8");
405
405
  const userConfig = JSON.parse(configContent);
406
406
  if (this.needsMigration(userConfig)) {
407
407
  console.log("\u{1F504} Migrating config from v0.2.0 to v0.3.0...");
408
408
  const result = await this.migrate(rootDir);
409
409
  if (result.migrated && result.backupPath) {
410
- const backupFilename = path8.basename(result.backupPath);
410
+ const backupFilename = path9.basename(result.backupPath);
411
411
  console.log(`\u2705 Migration complete! Backup saved as ${backupFilename}`);
412
412
  console.log("\u{1F4DD} Your config now uses the framework-based structure.");
413
413
  }
@@ -463,7 +463,7 @@ ${validation.errors.join("\n")}`,
463
463
  }
464
464
  try {
465
465
  const configJson = JSON.stringify(config, null, 2) + "\n";
466
- await fs8.writeFile(configPath, configJson, "utf-8");
466
+ await fs9.writeFile(configPath, configJson, "utf-8");
467
467
  } catch (error) {
468
468
  throw wrapError(error, "Failed to save configuration", { path: configPath });
469
469
  }
@@ -477,7 +477,7 @@ ${validation.errors.join("\n")}`,
477
477
  async exists(rootDir = process.cwd()) {
478
478
  const configPath = this.getConfigPath(rootDir);
479
479
  try {
480
- await fs8.access(configPath);
480
+ await fs9.access(configPath);
481
481
  return true;
482
482
  } catch {
483
483
  return false;
@@ -494,7 +494,7 @@ ${validation.errors.join("\n")}`,
494
494
  async migrate(rootDir = process.cwd()) {
495
495
  const configPath = this.getConfigPath(rootDir);
496
496
  try {
497
- const configContent = await fs8.readFile(configPath, "utf-8");
497
+ const configContent = await fs9.readFile(configPath, "utf-8");
498
498
  const oldConfig = JSON.parse(configContent);
499
499
  if (!this.needsMigration(oldConfig)) {
500
500
  return {
@@ -512,7 +512,7 @@ ${validation.errors.join("\n")}`,
512
512
  );
513
513
  }
514
514
  const backupPath = `${configPath}.v0.2.0.backup`;
515
- await fs8.copyFile(configPath, backupPath);
515
+ await fs9.copyFile(configPath, backupPath);
516
516
  await this.save(rootDir, newConfig);
517
517
  return {
518
518
  migrated: true,
@@ -610,7 +610,7 @@ ${validation.errors.join("\n")}`,
610
610
  * Get the full path to the config file
611
611
  */
612
612
  getConfigPath(rootDir) {
613
- return path8.join(rootDir, _ConfigService.CONFIG_FILENAME);
613
+ return path9.join(rootDir, _ConfigService.CONFIG_FILENAME);
614
614
  }
615
615
  /**
616
616
  * Validate modern (v0.3.0+) configuration
@@ -774,7 +774,7 @@ ${validation.errors.join("\n")}`,
774
774
  errors.push(`frameworks[${index}] missing required field: path`);
775
775
  } else if (typeof fw.path !== "string") {
776
776
  errors.push(`frameworks[${index}].path must be a string`);
777
- } else if (path8.isAbsolute(fw.path)) {
777
+ } else if (path9.isAbsolute(fw.path)) {
778
778
  errors.push(`frameworks[${index}].path must be relative, got: ${fw.path}`);
779
779
  }
780
780
  if (fw.enabled === void 0) {
@@ -834,12 +834,12 @@ __export(utils_exports, {
834
834
  });
835
835
  import { exec } from "child_process";
836
836
  import { promisify } from "util";
837
- import fs9 from "fs/promises";
838
- import path9 from "path";
837
+ import fs10 from "fs/promises";
838
+ import path10 from "path";
839
839
  async function isGitRepo(rootDir) {
840
840
  try {
841
- const gitDir = path9.join(rootDir, ".git");
842
- await fs9.access(gitDir);
841
+ const gitDir = path10.join(rootDir, ".git");
842
+ await fs10.access(gitDir);
843
843
  return true;
844
844
  } catch {
845
845
  return false;
@@ -878,7 +878,7 @@ async function getChangedFiles(rootDir, fromRef, toRef) {
878
878
  // 10 second timeout for diffs
879
879
  }
880
880
  );
881
- const files = stdout.trim().split("\n").filter(Boolean).map((file) => path9.join(rootDir, file));
881
+ const files = stdout.trim().split("\n").filter(Boolean).map((file) => path10.join(rootDir, file));
882
882
  return files;
883
883
  } catch (error) {
884
884
  throw new Error(`Failed to get changed files: ${error}`);
@@ -893,7 +893,7 @@ async function getChangedFilesInCommit(rootDir, commitSha) {
893
893
  timeout: 1e4
894
894
  }
895
895
  );
896
- const files = stdout.trim().split("\n").filter(Boolean).map((file) => path9.join(rootDir, file));
896
+ const files = stdout.trim().split("\n").filter(Boolean).map((file) => path10.join(rootDir, file));
897
897
  return files;
898
898
  } catch (error) {
899
899
  throw new Error(`Failed to get changed files in commit: ${error}`);
@@ -908,7 +908,7 @@ async function getChangedFilesBetweenCommits(rootDir, fromCommit, toCommit) {
908
908
  timeout: 1e4
909
909
  }
910
910
  );
911
- const files = stdout.trim().split("\n").filter(Boolean).map((file) => path9.join(rootDir, file));
911
+ const files = stdout.trim().split("\n").filter(Boolean).map((file) => path10.join(rootDir, file));
912
912
  return files;
913
913
  } catch (error) {
914
914
  throw new Error(`Failed to get changed files between commits: ${error}`);
@@ -931,21 +931,21 @@ var init_utils = __esm({
931
931
  });
932
932
 
933
933
  // src/vectordb/version.ts
934
- import fs10 from "fs/promises";
935
- import path10 from "path";
934
+ import fs11 from "fs/promises";
935
+ import path11 from "path";
936
936
  async function writeVersionFile(indexPath) {
937
937
  try {
938
- const versionFilePath = path10.join(indexPath, VERSION_FILE);
938
+ const versionFilePath = path11.join(indexPath, VERSION_FILE);
939
939
  const timestamp = Date.now().toString();
940
- await fs10.writeFile(versionFilePath, timestamp, "utf-8");
940
+ await fs11.writeFile(versionFilePath, timestamp, "utf-8");
941
941
  } catch (error) {
942
942
  console.error(`Warning: Failed to write version file: ${error}`);
943
943
  }
944
944
  }
945
945
  async function readVersionFile(indexPath) {
946
946
  try {
947
- const versionFilePath = path10.join(indexPath, VERSION_FILE);
948
- const content = await fs10.readFile(versionFilePath, "utf-8");
947
+ const versionFilePath = path11.join(indexPath, VERSION_FILE);
948
+ const content = await fs11.readFile(versionFilePath, "utf-8");
949
949
  const timestamp = parseInt(content.trim(), 10);
950
950
  return isNaN(timestamp) ? 0 : timestamp;
951
951
  } catch (error) {
@@ -963,8 +963,8 @@ var init_version2 = __esm({
963
963
  // src/indexer/scanner.ts
964
964
  import { glob } from "glob";
965
965
  import ignore from "ignore";
966
- import fs12 from "fs/promises";
967
- import path12 from "path";
966
+ import fs13 from "fs/promises";
967
+ import path13 from "path";
968
968
  async function scanCodebaseWithFrameworks(rootDir, config) {
969
969
  const allFiles = [];
970
970
  for (const framework of config.frameworks) {
@@ -977,16 +977,16 @@ async function scanCodebaseWithFrameworks(rootDir, config) {
977
977
  return allFiles;
978
978
  }
979
979
  async function scanFramework(rootDir, framework) {
980
- const frameworkPath = path12.join(rootDir, framework.path);
981
- const gitignorePath = path12.join(frameworkPath, ".gitignore");
980
+ const frameworkPath = path13.join(rootDir, framework.path);
981
+ const gitignorePath = path13.join(frameworkPath, ".gitignore");
982
982
  let ig = ignore();
983
983
  try {
984
- const gitignoreContent = await fs12.readFile(gitignorePath, "utf-8");
984
+ const gitignoreContent = await fs13.readFile(gitignorePath, "utf-8");
985
985
  ig = ignore().add(gitignoreContent);
986
986
  } catch (e) {
987
- const rootGitignorePath = path12.join(rootDir, ".gitignore");
987
+ const rootGitignorePath = path13.join(rootDir, ".gitignore");
988
988
  try {
989
- const gitignoreContent = await fs12.readFile(rootGitignorePath, "utf-8");
989
+ const gitignoreContent = await fs13.readFile(rootGitignorePath, "utf-8");
990
990
  ig = ignore().add(gitignoreContent);
991
991
  } catch (e2) {
992
992
  }
@@ -1008,15 +1008,15 @@ async function scanFramework(rootDir, framework) {
1008
1008
  }
1009
1009
  const uniqueFiles = Array.from(new Set(allFiles));
1010
1010
  return uniqueFiles.filter((file) => !ig.ignores(file)).map((file) => {
1011
- return framework.path === "." ? file : path12.join(framework.path, file);
1011
+ return framework.path === "." ? file : path13.join(framework.path, file);
1012
1012
  });
1013
1013
  }
1014
1014
  async function scanCodebase(options) {
1015
1015
  const { rootDir, includePatterns = [], excludePatterns = [] } = options;
1016
- const gitignorePath = path12.join(rootDir, ".gitignore");
1016
+ const gitignorePath = path13.join(rootDir, ".gitignore");
1017
1017
  let ig = ignore();
1018
1018
  try {
1019
- const gitignoreContent = await fs12.readFile(gitignorePath, "utf-8");
1019
+ const gitignoreContent = await fs13.readFile(gitignorePath, "utf-8");
1020
1020
  ig = ignore().add(gitignoreContent);
1021
1021
  } catch (e) {
1022
1022
  }
@@ -1043,12 +1043,12 @@ async function scanCodebase(options) {
1043
1043
  }
1044
1044
  const uniqueFiles = Array.from(new Set(allFiles));
1045
1045
  return uniqueFiles.filter((file) => {
1046
- const relativePath = path12.relative(rootDir, file);
1046
+ const relativePath = path13.relative(rootDir, file);
1047
1047
  return !ig.ignores(relativePath);
1048
1048
  });
1049
1049
  }
1050
1050
  function detectLanguage(filepath) {
1051
- const ext = path12.extname(filepath).toLowerCase();
1051
+ const ext = path13.extname(filepath).toLowerCase();
1052
1052
  const languageMap = {
1053
1053
  ".ts": "typescript",
1054
1054
  ".tsx": "typescript",
@@ -2591,10 +2591,10 @@ var init_types2 = __esm({
2591
2591
  });
2592
2592
 
2593
2593
  // src/vectordb/boosting/strategies.ts
2594
- import path13 from "path";
2594
+ import path14 from "path";
2595
2595
  function isDocumentationFile(filepath) {
2596
2596
  const lower = filepath.toLowerCase();
2597
- const filename = path13.basename(filepath).toLowerCase();
2597
+ const filename = path14.basename(filepath).toLowerCase();
2598
2598
  if (filename.startsWith("readme")) return true;
2599
2599
  if (filename.startsWith("changelog")) return true;
2600
2600
  if (filename.endsWith(".md") || filename.endsWith(".mdx") || filename.endsWith(".markdown")) {
@@ -2651,7 +2651,7 @@ var init_strategies = __esm({
2651
2651
  FilenameBoostingStrategy = class {
2652
2652
  name = "filename-matching";
2653
2653
  apply(query, filepath, baseScore) {
2654
- const filename = path13.basename(filepath, path13.extname(filepath)).toLowerCase();
2654
+ const filename = path14.basename(filepath, path14.extname(filepath)).toLowerCase();
2655
2655
  const queryTokens = query.toLowerCase().split(/\s+/);
2656
2656
  let boostFactor = 1;
2657
2657
  for (const token of queryTokens) {
@@ -3153,7 +3153,7 @@ __export(lancedb_exports, {
3153
3153
  VectorDB: () => VectorDB
3154
3154
  });
3155
3155
  import * as lancedb from "vectordb";
3156
- import path14 from "path";
3156
+ import path15 from "path";
3157
3157
  import os2 from "os";
3158
3158
  import crypto2 from "crypto";
3159
3159
  var VectorDB;
@@ -3174,9 +3174,9 @@ var init_lancedb = __esm({
3174
3174
  lastVersionCheck = 0;
3175
3175
  currentVersion = 0;
3176
3176
  constructor(projectRoot) {
3177
- const projectName = path14.basename(projectRoot);
3177
+ const projectName = path15.basename(projectRoot);
3178
3178
  const pathHash = crypto2.createHash("md5").update(projectRoot).digest("hex").substring(0, 8);
3179
- this.dbPath = path14.join(
3179
+ this.dbPath = path15.join(
3180
3180
  os2.homedir(),
3181
3181
  ".lien",
3182
3182
  "indices",
@@ -3348,8 +3348,8 @@ var manifest_exports = {};
3348
3348
  __export(manifest_exports, {
3349
3349
  ManifestManager: () => ManifestManager
3350
3350
  });
3351
- import fs13 from "fs/promises";
3352
- import path15 from "path";
3351
+ import fs14 from "fs/promises";
3352
+ import path16 from "path";
3353
3353
  var MANIFEST_FILE, ManifestManager;
3354
3354
  var init_manifest = __esm({
3355
3355
  "src/indexer/manifest.ts"() {
@@ -3371,7 +3371,7 @@ var init_manifest = __esm({
3371
3371
  */
3372
3372
  constructor(indexPath) {
3373
3373
  this.indexPath = indexPath;
3374
- this.manifestPath = path15.join(indexPath, MANIFEST_FILE);
3374
+ this.manifestPath = path16.join(indexPath, MANIFEST_FILE);
3375
3375
  }
3376
3376
  /**
3377
3377
  * Loads the manifest from disk.
@@ -3384,7 +3384,7 @@ var init_manifest = __esm({
3384
3384
  */
3385
3385
  async load() {
3386
3386
  try {
3387
- const content = await fs13.readFile(this.manifestPath, "utf-8");
3387
+ const content = await fs14.readFile(this.manifestPath, "utf-8");
3388
3388
  const manifest = JSON.parse(content);
3389
3389
  if (manifest.formatVersion !== INDEX_FORMAT_VERSION) {
3390
3390
  console.error(
@@ -3411,7 +3411,7 @@ var init_manifest = __esm({
3411
3411
  */
3412
3412
  async save(manifest) {
3413
3413
  try {
3414
- await fs13.mkdir(this.indexPath, { recursive: true });
3414
+ await fs14.mkdir(this.indexPath, { recursive: true });
3415
3415
  const manifestToSave = {
3416
3416
  ...manifest,
3417
3417
  formatVersion: INDEX_FORMAT_VERSION,
@@ -3419,7 +3419,7 @@ var init_manifest = __esm({
3419
3419
  lastIndexed: Date.now()
3420
3420
  };
3421
3421
  const content = JSON.stringify(manifestToSave, null, 2);
3422
- await fs13.writeFile(this.manifestPath, content, "utf-8");
3422
+ await fs14.writeFile(this.manifestPath, content, "utf-8");
3423
3423
  } catch (error) {
3424
3424
  console.error(`[Lien] Warning: Failed to save manifest: ${error}`);
3425
3425
  }
@@ -3556,7 +3556,7 @@ var init_manifest = __esm({
3556
3556
  */
3557
3557
  async clear() {
3558
3558
  try {
3559
- await fs13.unlink(this.manifestPath);
3559
+ await fs14.unlink(this.manifestPath);
3560
3560
  } catch (error) {
3561
3561
  if (error.code !== "ENOENT") {
3562
3562
  console.error(`[Lien] Warning: Failed to clear manifest: ${error}`);
@@ -3585,8 +3585,8 @@ var tracker_exports = {};
3585
3585
  __export(tracker_exports, {
3586
3586
  GitStateTracker: () => GitStateTracker
3587
3587
  });
3588
- import fs14 from "fs/promises";
3589
- import path16 from "path";
3588
+ import fs15 from "fs/promises";
3589
+ import path17 from "path";
3590
3590
  var GitStateTracker;
3591
3591
  var init_tracker = __esm({
3592
3592
  "src/git/tracker.ts"() {
@@ -3598,7 +3598,7 @@ var init_tracker = __esm({
3598
3598
  currentState = null;
3599
3599
  constructor(rootDir, indexPath) {
3600
3600
  this.rootDir = rootDir;
3601
- this.stateFile = path16.join(indexPath, ".git-state.json");
3601
+ this.stateFile = path17.join(indexPath, ".git-state.json");
3602
3602
  }
3603
3603
  /**
3604
3604
  * Loads the last known git state from disk.
@@ -3606,7 +3606,7 @@ var init_tracker = __esm({
3606
3606
  */
3607
3607
  async loadState() {
3608
3608
  try {
3609
- const content = await fs14.readFile(this.stateFile, "utf-8");
3609
+ const content = await fs15.readFile(this.stateFile, "utf-8");
3610
3610
  return JSON.parse(content);
3611
3611
  } catch {
3612
3612
  return null;
@@ -3618,7 +3618,7 @@ var init_tracker = __esm({
3618
3618
  async saveState(state) {
3619
3619
  try {
3620
3620
  const content = JSON.stringify(state, null, 2);
3621
- await fs14.writeFile(this.stateFile, content, "utf-8");
3621
+ await fs15.writeFile(this.stateFile, content, "utf-8");
3622
3622
  } catch (error) {
3623
3623
  console.error(`[Lien] Warning: Failed to save git state: ${error}`);
3624
3624
  }
@@ -3769,7 +3769,7 @@ var init_tracker = __esm({
3769
3769
  });
3770
3770
 
3771
3771
  // src/indexer/change-detector.ts
3772
- import fs15 from "fs/promises";
3772
+ import fs16 from "fs/promises";
3773
3773
  async function detectChanges(rootDir, vectorDB, config) {
3774
3774
  const manifest = new ManifestManager(vectorDB.dbPath);
3775
3775
  const savedManifest = await manifest.load();
@@ -3873,7 +3873,7 @@ async function mtimeBasedDetection(rootDir, savedManifest, config) {
3873
3873
  const fileStats = /* @__PURE__ */ new Map();
3874
3874
  for (const filepath of currentFiles) {
3875
3875
  try {
3876
- const stats = await fs15.stat(filepath);
3876
+ const stats = await fs16.stat(filepath);
3877
3877
  fileStats.set(filepath, stats.mtimeMs);
3878
3878
  } catch {
3879
3879
  continue;
@@ -3927,7 +3927,7 @@ var init_result = __esm({
3927
3927
  });
3928
3928
 
3929
3929
  // src/indexer/incremental.ts
3930
- import fs16 from "fs/promises";
3930
+ import fs17 from "fs/promises";
3931
3931
  async function processFileContent(filepath, content, embeddings, config, verbose) {
3932
3932
  const chunkSize = isModernConfig(config) ? config.core.chunkSize : isLegacyConfig(config) ? config.indexing.chunkSize : 75;
3933
3933
  const chunkOverlap = isModernConfig(config) ? config.core.chunkOverlap : isLegacyConfig(config) ? config.indexing.chunkOverlap : 10;
@@ -3966,7 +3966,7 @@ async function indexSingleFile(filepath, vectorDB, embeddings, config, options =
3966
3966
  const { verbose } = options;
3967
3967
  try {
3968
3968
  try {
3969
- await fs16.access(filepath);
3969
+ await fs17.access(filepath);
3970
3970
  } catch {
3971
3971
  if (verbose) {
3972
3972
  console.error(`[Lien] File deleted: ${filepath}`);
@@ -3976,9 +3976,9 @@ async function indexSingleFile(filepath, vectorDB, embeddings, config, options =
3976
3976
  await manifest2.removeFile(filepath);
3977
3977
  return;
3978
3978
  }
3979
- const content = await fs16.readFile(filepath, "utf-8");
3979
+ const content = await fs17.readFile(filepath, "utf-8");
3980
3980
  const result = await processFileContent(filepath, content, embeddings, config, verbose || false);
3981
- const stats = await fs16.stat(filepath);
3981
+ const stats = await fs17.stat(filepath);
3982
3982
  const manifest = new ManifestManager(vectorDB.dbPath);
3983
3983
  if (result === null) {
3984
3984
  await vectorDB.deleteByFile(filepath);
@@ -4009,8 +4009,8 @@ async function indexSingleFile(filepath, vectorDB, embeddings, config, options =
4009
4009
  }
4010
4010
  async function processSingleFileForIndexing(filepath, embeddings, config, verbose) {
4011
4011
  try {
4012
- const stats = await fs16.stat(filepath);
4013
- const content = await fs16.readFile(filepath, "utf-8");
4012
+ const stats = await fs17.stat(filepath);
4013
+ const content = await fs17.readFile(filepath, "utf-8");
4014
4014
  const result = await processFileContent(filepath, content, embeddings, config, verbose);
4015
4015
  return Ok({
4016
4016
  filepath,
@@ -4281,7 +4281,7 @@ var indexer_exports = {};
4281
4281
  __export(indexer_exports, {
4282
4282
  indexCodebase: () => indexCodebase
4283
4283
  });
4284
- import fs17 from "fs/promises";
4284
+ import fs18 from "fs/promises";
4285
4285
  import ora from "ora";
4286
4286
  import chalk5 from "chalk";
4287
4287
  import pLimit from "p-limit";
@@ -4454,8 +4454,8 @@ async function performFullIndex(rootDir, vectorDB, config, options, spinner) {
4454
4454
  const filePromises = files.map(
4455
4455
  (file) => limit(async () => {
4456
4456
  try {
4457
- const stats = await fs17.stat(file);
4458
- const content = await fs17.readFile(file, "utf-8");
4457
+ const stats = await fs18.stat(file);
4458
+ const content = await fs18.readFile(file, "utf-8");
4459
4459
  const chunkSize = isModernConfig(config) ? config.core.chunkSize : 75;
4460
4460
  const chunkOverlap = isModernConfig(config) ? config.core.chunkOverlap : 10;
4461
4461
  const useAST = isModernConfig(config) ? config.chunking.useAST : true;
@@ -4583,8 +4583,8 @@ import { dirname as dirname4, join as join4 } from "path";
4583
4583
 
4584
4584
  // src/cli/init.ts
4585
4585
  init_schema();
4586
- import fs7 from "fs/promises";
4587
- import path7 from "path";
4586
+ import fs8 from "fs/promises";
4587
+ import path8 from "path";
4588
4588
  import { fileURLToPath as fileURLToPath3 } from "url";
4589
4589
  import chalk3 from "chalk";
4590
4590
  import inquirer from "inquirer";
@@ -4751,8 +4751,8 @@ var MigrationManager = class {
4751
4751
  };
4752
4752
 
4753
4753
  // src/frameworks/detector-service.ts
4754
- import fs6 from "fs/promises";
4755
- import path6 from "path";
4754
+ import fs7 from "fs/promises";
4755
+ import path7 from "path";
4756
4756
 
4757
4757
  // src/frameworks/types.ts
4758
4758
  var defaultDetectionOptions = {
@@ -4884,10 +4884,132 @@ var nodejsDetector = {
4884
4884
  }
4885
4885
  };
4886
4886
 
4887
- // src/frameworks/laravel/detector.ts
4887
+ // src/frameworks/php/detector.ts
4888
4888
  import fs4 from "fs/promises";
4889
4889
  import path4 from "path";
4890
4890
 
4891
+ // src/frameworks/php/config.ts
4892
+ async function generatePhpConfig(_rootDir, _relativePath) {
4893
+ return {
4894
+ include: [
4895
+ // PHP source code
4896
+ "src/**/*.php",
4897
+ "lib/**/*.php",
4898
+ "app/**/*.php",
4899
+ "tests/**/*.php",
4900
+ "*.php",
4901
+ // Common PHP project files
4902
+ "config/**/*.php",
4903
+ "public/**/*.php",
4904
+ // Documentation
4905
+ "**/*.md",
4906
+ "**/*.mdx",
4907
+ "docs/**/*.md",
4908
+ "README.md",
4909
+ "CHANGELOG.md"
4910
+ ],
4911
+ exclude: [
4912
+ "vendor/**",
4913
+ "node_modules/**",
4914
+ "dist/**",
4915
+ "build/**",
4916
+ "storage/**",
4917
+ "cache/**",
4918
+ // Test artifacts
4919
+ "coverage/**",
4920
+ "test-results/**",
4921
+ ".phpunit.cache/**",
4922
+ // Build outputs
4923
+ "__generated__/**"
4924
+ ]
4925
+ };
4926
+ }
4927
+
4928
+ // src/frameworks/php/detector.ts
4929
+ var phpDetector = {
4930
+ name: "php",
4931
+ priority: 50,
4932
+ // Generic, yields to specific frameworks like Laravel
4933
+ async detect(rootDir, relativePath) {
4934
+ const fullPath = path4.join(rootDir, relativePath);
4935
+ const result = {
4936
+ detected: false,
4937
+ name: "php",
4938
+ path: relativePath,
4939
+ confidence: "low",
4940
+ evidence: []
4941
+ };
4942
+ const composerJsonPath = path4.join(fullPath, "composer.json");
4943
+ let composerJson = null;
4944
+ try {
4945
+ const content = await fs4.readFile(composerJsonPath, "utf-8");
4946
+ composerJson = JSON.parse(content);
4947
+ result.evidence.push("Found composer.json");
4948
+ } catch {
4949
+ return result;
4950
+ }
4951
+ const hasLaravel = composerJson.require?.["laravel/framework"] || composerJson["require-dev"]?.["laravel/framework"];
4952
+ if (hasLaravel) {
4953
+ return result;
4954
+ }
4955
+ result.detected = true;
4956
+ result.confidence = "high";
4957
+ const phpDirs = ["src", "lib", "app", "tests"];
4958
+ let foundDirs = 0;
4959
+ for (const dir of phpDirs) {
4960
+ try {
4961
+ const dirPath = path4.join(fullPath, dir);
4962
+ const stats = await fs4.stat(dirPath);
4963
+ if (stats.isDirectory()) {
4964
+ foundDirs++;
4965
+ }
4966
+ } catch {
4967
+ }
4968
+ }
4969
+ if (foundDirs > 0) {
4970
+ result.evidence.push(`Found PHP project structure (${foundDirs} directories)`);
4971
+ }
4972
+ if (composerJson.require?.php) {
4973
+ result.version = composerJson.require.php;
4974
+ result.evidence.push(`PHP ${composerJson.require.php}`);
4975
+ }
4976
+ const testFrameworks = [
4977
+ { name: "phpunit/phpunit", display: "PHPUnit" },
4978
+ { name: "pestphp/pest", display: "Pest" },
4979
+ { name: "codeception/codeception", display: "Codeception" },
4980
+ { name: "behat/behat", display: "Behat" }
4981
+ ];
4982
+ for (const framework of testFrameworks) {
4983
+ if (composerJson.require?.[framework.name] || composerJson["require-dev"]?.[framework.name]) {
4984
+ result.evidence.push(`${framework.display} test framework detected`);
4985
+ break;
4986
+ }
4987
+ }
4988
+ const tools2 = [
4989
+ { name: "symfony/framework-bundle", display: "Symfony" },
4990
+ { name: "symfony/http-kernel", display: "Symfony" },
4991
+ { name: "symfony/symfony", display: "Symfony (monolithic)" },
4992
+ { name: "doctrine/orm", display: "Doctrine ORM" },
4993
+ { name: "guzzlehttp/guzzle", display: "Guzzle HTTP" },
4994
+ { name: "monolog/monolog", display: "Monolog" }
4995
+ ];
4996
+ for (const tool of tools2) {
4997
+ if (composerJson.require?.[tool.name]) {
4998
+ result.evidence.push(`${tool.display} detected`);
4999
+ break;
5000
+ }
5001
+ }
5002
+ return result;
5003
+ },
5004
+ async generateConfig(rootDir, relativePath) {
5005
+ return generatePhpConfig(rootDir, relativePath);
5006
+ }
5007
+ };
5008
+
5009
+ // src/frameworks/laravel/detector.ts
5010
+ import fs5 from "fs/promises";
5011
+ import path5 from "path";
5012
+
4891
5013
  // src/frameworks/laravel/config.ts
4892
5014
  async function generateLaravelConfig(_rootDir, _relativePath) {
4893
5015
  return {
@@ -4943,7 +5065,7 @@ var laravelDetector = {
4943
5065
  priority: 100,
4944
5066
  // Laravel takes precedence over Node.js
4945
5067
  async detect(rootDir, relativePath) {
4946
- const fullPath = path4.join(rootDir, relativePath);
5068
+ const fullPath = path5.join(rootDir, relativePath);
4947
5069
  const result = {
4948
5070
  detected: false,
4949
5071
  name: "laravel",
@@ -4951,10 +5073,10 @@ var laravelDetector = {
4951
5073
  confidence: "low",
4952
5074
  evidence: []
4953
5075
  };
4954
- const composerJsonPath = path4.join(fullPath, "composer.json");
5076
+ const composerJsonPath = path5.join(fullPath, "composer.json");
4955
5077
  let composerJson = null;
4956
5078
  try {
4957
- const content = await fs4.readFile(composerJsonPath, "utf-8");
5079
+ const content = await fs5.readFile(composerJsonPath, "utf-8");
4958
5080
  composerJson = JSON.parse(content);
4959
5081
  result.evidence.push("Found composer.json");
4960
5082
  } catch {
@@ -4965,9 +5087,9 @@ var laravelDetector = {
4965
5087
  return result;
4966
5088
  }
4967
5089
  result.evidence.push("Laravel framework detected in composer.json");
4968
- const artisanPath = path4.join(fullPath, "artisan");
5090
+ const artisanPath = path5.join(fullPath, "artisan");
4969
5091
  try {
4970
- await fs4.access(artisanPath);
5092
+ await fs5.access(artisanPath);
4971
5093
  result.evidence.push("Found artisan file");
4972
5094
  result.confidence = "high";
4973
5095
  } catch {
@@ -4977,8 +5099,8 @@ var laravelDetector = {
4977
5099
  let foundDirs = 0;
4978
5100
  for (const dir of laravelDirs) {
4979
5101
  try {
4980
- const dirPath = path4.join(fullPath, dir);
4981
- const stats = await fs4.stat(dirPath);
5102
+ const dirPath = path5.join(fullPath, dir);
5103
+ const stats = await fs5.stat(dirPath);
4982
5104
  if (stats.isDirectory()) {
4983
5105
  foundDirs++;
4984
5106
  }
@@ -4990,14 +5112,14 @@ var laravelDetector = {
4990
5112
  result.confidence = "high";
4991
5113
  }
4992
5114
  const testDirsToCheck = [
4993
- path4.join(fullPath, "tests", "Feature"),
4994
- path4.join(fullPath, "tests", "Unit")
5115
+ path5.join(fullPath, "tests", "Feature"),
5116
+ path5.join(fullPath, "tests", "Unit")
4995
5117
  ];
4996
5118
  for (const testDir of testDirsToCheck) {
4997
5119
  try {
4998
- const stats = await fs4.stat(testDir);
5120
+ const stats = await fs5.stat(testDir);
4999
5121
  if (stats.isDirectory()) {
5000
- const dirName = path4.basename(path4.dirname(testDir)) + "/" + path4.basename(testDir);
5122
+ const dirName = path5.basename(path5.dirname(testDir)) + "/" + path5.basename(testDir);
5001
5123
  result.evidence.push(`Found ${dirName} test directory`);
5002
5124
  }
5003
5125
  } catch {
@@ -5015,8 +5137,8 @@ var laravelDetector = {
5015
5137
  };
5016
5138
 
5017
5139
  // src/frameworks/shopify/detector.ts
5018
- import fs5 from "fs/promises";
5019
- import path5 from "path";
5140
+ import fs6 from "fs/promises";
5141
+ import path6 from "path";
5020
5142
 
5021
5143
  // src/frameworks/shopify/config.ts
5022
5144
  async function generateShopifyConfig(_rootDir, _relativePath) {
@@ -5074,7 +5196,7 @@ var shopifyDetector = {
5074
5196
  priority: 100,
5075
5197
  // High priority (same as Laravel)
5076
5198
  async detect(rootDir, relativePath) {
5077
- const fullPath = path5.join(rootDir, relativePath);
5199
+ const fullPath = path6.join(rootDir, relativePath);
5078
5200
  const result = {
5079
5201
  detected: false,
5080
5202
  name: "shopify",
@@ -5082,18 +5204,18 @@ var shopifyDetector = {
5082
5204
  confidence: "low",
5083
5205
  evidence: []
5084
5206
  };
5085
- const settingsSchemaPath = path5.join(fullPath, "config", "settings_schema.json");
5207
+ const settingsSchemaPath = path6.join(fullPath, "config", "settings_schema.json");
5086
5208
  let hasSettingsSchema = false;
5087
5209
  try {
5088
- await fs5.access(settingsSchemaPath);
5210
+ await fs6.access(settingsSchemaPath);
5089
5211
  hasSettingsSchema = true;
5090
5212
  result.evidence.push("Found config/settings_schema.json");
5091
5213
  } catch {
5092
5214
  }
5093
- const themeLayoutPath = path5.join(fullPath, "layout", "theme.liquid");
5215
+ const themeLayoutPath = path6.join(fullPath, "layout", "theme.liquid");
5094
5216
  let hasThemeLayout = false;
5095
5217
  try {
5096
- await fs5.access(themeLayoutPath);
5218
+ await fs6.access(themeLayoutPath);
5097
5219
  hasThemeLayout = true;
5098
5220
  result.evidence.push("Found layout/theme.liquid");
5099
5221
  } catch {
@@ -5102,8 +5224,8 @@ var shopifyDetector = {
5102
5224
  let foundDirs = 0;
5103
5225
  for (const dir of shopifyDirs) {
5104
5226
  try {
5105
- const dirPath = path5.join(fullPath, dir);
5106
- const stats = await fs5.stat(dirPath);
5227
+ const dirPath = path6.join(fullPath, dir);
5228
+ const stats = await fs6.stat(dirPath);
5107
5229
  if (stats.isDirectory()) {
5108
5230
  foundDirs++;
5109
5231
  }
@@ -5114,14 +5236,14 @@ var shopifyDetector = {
5114
5236
  result.evidence.push(`Shopify directory structure detected (${foundDirs}/${shopifyDirs.length} dirs)`);
5115
5237
  }
5116
5238
  try {
5117
- const tomlPath = path5.join(fullPath, "shopify.theme.toml");
5118
- await fs5.access(tomlPath);
5239
+ const tomlPath = path6.join(fullPath, "shopify.theme.toml");
5240
+ await fs6.access(tomlPath);
5119
5241
  result.evidence.push("Found shopify.theme.toml");
5120
5242
  } catch {
5121
5243
  }
5122
5244
  try {
5123
- const ignorePath = path5.join(fullPath, ".shopifyignore");
5124
- await fs5.access(ignorePath);
5245
+ const ignorePath = path6.join(fullPath, ".shopifyignore");
5246
+ await fs6.access(ignorePath);
5125
5247
  result.evidence.push("Found .shopifyignore");
5126
5248
  } catch {
5127
5249
  }
@@ -5150,6 +5272,7 @@ var shopifyDetector = {
5150
5272
  // src/frameworks/registry.ts
5151
5273
  var frameworkDetectors = [
5152
5274
  nodejsDetector,
5275
+ phpDetector,
5153
5276
  laravelDetector,
5154
5277
  shopifyDetector
5155
5278
  ];
@@ -5167,7 +5290,7 @@ async function detectAllFrameworks(rootDir, options = {}) {
5167
5290
  return results;
5168
5291
  }
5169
5292
  async function detectAtPath(rootDir, relativePath, results, visited) {
5170
- const fullPath = path6.join(rootDir, relativePath);
5293
+ const fullPath = path7.join(rootDir, relativePath);
5171
5294
  if (visited.has(fullPath)) {
5172
5295
  return;
5173
5296
  }
@@ -5234,9 +5357,9 @@ async function scanSubdirectories(rootDir, relativePath, results, visited, depth
5234
5357
  if (depth >= options.maxDepth) {
5235
5358
  return;
5236
5359
  }
5237
- const fullPath = path6.join(rootDir, relativePath);
5360
+ const fullPath = path7.join(rootDir, relativePath);
5238
5361
  try {
5239
- const entries = await fs6.readdir(fullPath, { withFileTypes: true });
5362
+ const entries = await fs7.readdir(fullPath, { withFileTypes: true });
5240
5363
  const dirs = entries.filter((e) => e.isDirectory());
5241
5364
  for (const dir of dirs) {
5242
5365
  if (options.skipDirs.includes(dir.name)) {
@@ -5245,7 +5368,7 @@ async function scanSubdirectories(rootDir, relativePath, results, visited, depth
5245
5368
  if (dir.name.startsWith(".")) {
5246
5369
  continue;
5247
5370
  }
5248
- const subPath = relativePath === "." ? dir.name : path6.join(relativePath, dir.name);
5371
+ const subPath = relativePath === "." ? dir.name : path7.join(relativePath, dir.name);
5249
5372
  await detectAtPath(rootDir, subPath, results, visited);
5250
5373
  await scanSubdirectories(rootDir, subPath, results, visited, depth + 1, options);
5251
5374
  }
@@ -5256,14 +5379,14 @@ async function scanSubdirectories(rootDir, relativePath, results, visited, depth
5256
5379
 
5257
5380
  // src/cli/init.ts
5258
5381
  var __filename3 = fileURLToPath3(import.meta.url);
5259
- var __dirname3 = path7.dirname(__filename3);
5382
+ var __dirname3 = path8.dirname(__filename3);
5260
5383
  async function initCommand(options = {}) {
5261
5384
  const rootDir = options.path || process.cwd();
5262
- const configPath = path7.join(rootDir, ".lien.config.json");
5385
+ const configPath = path8.join(rootDir, ".lien.config.json");
5263
5386
  try {
5264
5387
  let configExists = false;
5265
5388
  try {
5266
- await fs7.access(configPath);
5389
+ await fs8.access(configPath);
5267
5390
  configExists = true;
5268
5391
  } catch {
5269
5392
  }
@@ -5400,22 +5523,22 @@ async function createNewConfig(rootDir, options) {
5400
5523
  ]);
5401
5524
  if (installCursorRules) {
5402
5525
  try {
5403
- const cursorRulesDir = path7.join(rootDir, ".cursor");
5404
- await fs7.mkdir(cursorRulesDir, { recursive: true });
5405
- const templatePath = path7.join(__dirname3, "../CURSOR_RULES_TEMPLATE.md");
5406
- const rulesPath = path7.join(cursorRulesDir, "rules");
5526
+ const cursorRulesDir = path8.join(rootDir, ".cursor");
5527
+ await fs8.mkdir(cursorRulesDir, { recursive: true });
5528
+ const templatePath = path8.join(__dirname3, "../CURSOR_RULES_TEMPLATE.md");
5529
+ const rulesPath = path8.join(cursorRulesDir, "rules");
5407
5530
  let targetPath;
5408
5531
  let isDirectory = false;
5409
5532
  let isFile = false;
5410
5533
  try {
5411
- const stats = await fs7.stat(rulesPath);
5534
+ const stats = await fs8.stat(rulesPath);
5412
5535
  isDirectory = stats.isDirectory();
5413
5536
  isFile = stats.isFile();
5414
5537
  } catch {
5415
5538
  }
5416
5539
  if (isDirectory) {
5417
- targetPath = path7.join(rulesPath, "lien.mdc");
5418
- await fs7.copyFile(templatePath, targetPath);
5540
+ targetPath = path8.join(rulesPath, "lien.mdc");
5541
+ await fs8.copyFile(templatePath, targetPath);
5419
5542
  console.log(chalk3.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
5420
5543
  } else if (isFile) {
5421
5544
  const { convertToDir } = await inquirer.prompt([
@@ -5427,11 +5550,11 @@ async function createNewConfig(rootDir, options) {
5427
5550
  }
5428
5551
  ]);
5429
5552
  if (convertToDir) {
5430
- const existingRules = await fs7.readFile(rulesPath, "utf-8");
5431
- await fs7.unlink(rulesPath);
5432
- await fs7.mkdir(rulesPath);
5433
- await fs7.writeFile(path7.join(rulesPath, "project.mdc"), existingRules);
5434
- await fs7.copyFile(templatePath, path7.join(rulesPath, "lien.mdc"));
5553
+ const existingRules = await fs8.readFile(rulesPath, "utf-8");
5554
+ await fs8.unlink(rulesPath);
5555
+ await fs8.mkdir(rulesPath);
5556
+ await fs8.writeFile(path8.join(rulesPath, "project.mdc"), existingRules);
5557
+ await fs8.copyFile(templatePath, path8.join(rulesPath, "lien.mdc"));
5435
5558
  console.log(chalk3.green("\u2713 Converted .cursor/rules to directory"));
5436
5559
  console.log(chalk3.green(" - Your project rules: .cursor/rules/project.mdc"));
5437
5560
  console.log(chalk3.green(" - Lien rules: .cursor/rules/lien.mdc"));
@@ -5439,9 +5562,9 @@ async function createNewConfig(rootDir, options) {
5439
5562
  console.log(chalk3.dim("Skipped Cursor rules installation (preserving existing file)"));
5440
5563
  }
5441
5564
  } else {
5442
- await fs7.mkdir(rulesPath, { recursive: true });
5443
- targetPath = path7.join(rulesPath, "lien.mdc");
5444
- await fs7.copyFile(templatePath, targetPath);
5565
+ await fs8.mkdir(rulesPath, { recursive: true });
5566
+ targetPath = path8.join(rulesPath, "lien.mdc");
5567
+ await fs8.copyFile(templatePath, targetPath);
5445
5568
  console.log(chalk3.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
5446
5569
  }
5447
5570
  } catch (error) {
@@ -5455,8 +5578,8 @@ async function createNewConfig(rootDir, options) {
5455
5578
  ...defaultConfig,
5456
5579
  frameworks
5457
5580
  };
5458
- const configPath = path7.join(rootDir, ".lien.config.json");
5459
- await fs7.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5581
+ const configPath = path8.join(rootDir, ".lien.config.json");
5582
+ await fs8.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5460
5583
  console.log(chalk3.green("\n\u2713 Created .lien.config.json"));
5461
5584
  console.log(chalk3.green(`\u2713 Configured ${frameworks.length} framework(s)`));
5462
5585
  console.log(chalk3.dim("\nNext steps:"));
@@ -5494,16 +5617,16 @@ init_service();
5494
5617
  init_utils();
5495
5618
  init_version2();
5496
5619
  import chalk4 from "chalk";
5497
- import fs11 from "fs/promises";
5498
- import path11 from "path";
5620
+ import fs12 from "fs/promises";
5621
+ import path12 from "path";
5499
5622
  import os from "os";
5500
5623
  import crypto from "crypto";
5501
5624
  init_schema();
5502
5625
  async function statusCommand() {
5503
5626
  const rootDir = process.cwd();
5504
- const projectName = path11.basename(rootDir);
5627
+ const projectName = path12.basename(rootDir);
5505
5628
  const pathHash = crypto.createHash("md5").update(rootDir).digest("hex").substring(0, 8);
5506
- const indexPath = path11.join(os.homedir(), ".lien", "indices", `${projectName}-${pathHash}`);
5629
+ const indexPath = path12.join(os.homedir(), ".lien", "indices", `${projectName}-${pathHash}`);
5507
5630
  showCompactBanner();
5508
5631
  console.log(chalk4.bold("Status\n"));
5509
5632
  const hasConfig = await configService.exists(rootDir);
@@ -5513,11 +5636,11 @@ async function statusCommand() {
5513
5636
  return;
5514
5637
  }
5515
5638
  try {
5516
- const stats = await fs11.stat(indexPath);
5639
+ const stats = await fs12.stat(indexPath);
5517
5640
  console.log(chalk4.dim("Index location:"), indexPath);
5518
5641
  console.log(chalk4.dim("Index status:"), chalk4.green("\u2713 Exists"));
5519
5642
  try {
5520
- const files = await fs11.readdir(indexPath, { recursive: true });
5643
+ const files = await fs12.readdir(indexPath, { recursive: true });
5521
5644
  console.log(chalk4.dim("Index files:"), files.length);
5522
5645
  } catch (e) {
5523
5646
  }
@@ -5546,9 +5669,9 @@ async function statusCommand() {
5546
5669
  const commit = await getCurrentCommit(rootDir);
5547
5670
  console.log(chalk4.dim(" Current branch:"), branch);
5548
5671
  console.log(chalk4.dim(" Current commit:"), commit.substring(0, 8));
5549
- const gitStateFile = path11.join(indexPath, ".git-state.json");
5672
+ const gitStateFile = path12.join(indexPath, ".git-state.json");
5550
5673
  try {
5551
- const gitStateContent = await fs11.readFile(gitStateFile, "utf-8");
5674
+ const gitStateContent = await fs12.readFile(gitStateFile, "utf-8");
5552
5675
  const gitState = JSON.parse(gitStateContent);
5553
5676
  if (gitState.branch !== branch || gitState.commit !== commit) {
5554
5677
  console.log(chalk4.yellow(" \u26A0\uFE0F Git state changed - will reindex on next serve"));
@@ -5614,8 +5737,8 @@ async function indexCommand(options) {
5614
5737
 
5615
5738
  // src/cli/serve.ts
5616
5739
  import chalk7 from "chalk";
5617
- import fs18 from "fs/promises";
5618
- import path17 from "path";
5740
+ import fs19 from "fs/promises";
5741
+ import path18 from "path";
5619
5742
 
5620
5743
  // src/mcp/server.ts
5621
5744
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -5688,6 +5811,17 @@ var ListFunctionsSchema = z4.object({
5688
5811
  )
5689
5812
  });
5690
5813
 
5814
+ // src/mcp/schemas/dependents.schema.ts
5815
+ import { z as z5 } from "zod";
5816
+ var GetDependentsSchema = z5.object({
5817
+ filepath: z5.string().min(1, "Filepath cannot be empty").describe(
5818
+ "Path to file to find dependents for (relative to workspace root).\n\nExample: 'src/utils/validate.ts'\n\nReturns all files that import or depend on this file.\n\nNote: Scans up to 10,000 code chunks. For very large codebases,\nresults may be incomplete (a warning will be included if truncated)."
5819
+ ),
5820
+ depth: z5.number().int().min(1).max(1).default(1).describe(
5821
+ "Depth of transitive dependencies. Only depth=1 (direct dependents) is currently supported.\n\n1 = Direct dependents only"
5822
+ )
5823
+ });
5824
+
5691
5825
  // src/mcp/tools.ts
5692
5826
  var tools = [
5693
5827
  toMCPToolSchema(
@@ -5722,15 +5856,39 @@ MANDATORY: Call this BEFORE editing any file. Accepts single path or array of pa
5722
5856
 
5723
5857
  Single file:
5724
5858
  get_files_context({ filepaths: "src/auth.ts" })
5859
+
5860
+ Returns:
5861
+ {
5862
+ file: "src/auth.ts",
5863
+ chunks: [...],
5864
+ testAssociations: ["src/__tests__/auth.test.ts"]
5865
+ }
5725
5866
 
5726
5867
  Multiple files (batch):
5727
5868
  get_files_context({ filepaths: ["src/auth.ts", "src/user.ts"] })
5869
+
5870
+ Returns:
5871
+ {
5872
+ files: {
5873
+ "src/auth.ts": {
5874
+ chunks: [...],
5875
+ testAssociations: ["src/__tests__/auth.test.ts"]
5876
+ },
5877
+ "src/user.ts": {
5878
+ chunks: [...],
5879
+ testAssociations: ["src/__tests__/user.test.ts"]
5880
+ }
5881
+ }
5882
+ }
5728
5883
 
5729
5884
  Returns for each file:
5730
5885
  - All chunks and related code
5731
- - testAssociations (which tests cover this file)
5886
+ - testAssociations: Array of test files that import this file (reverse dependency lookup)
5732
5887
  - Relevance scoring
5733
5888
 
5889
+ ALWAYS check testAssociations before modifying source code.
5890
+ After changes, remind the user to run the associated tests.
5891
+
5734
5892
  Batch calls are more efficient than multiple single-file calls.`
5735
5893
  ),
5736
5894
  toMCPToolSchema(
@@ -5743,6 +5901,20 @@ Examples:
5743
5901
  - "Find service classes" \u2192 list_functions({ pattern: ".*Service$" })
5744
5902
 
5745
5903
  10x faster than semantic_search for structural/architectural queries. Use semantic_search instead when searching by what code DOES.`
5904
+ ),
5905
+ toMCPToolSchema(
5906
+ GetDependentsSchema,
5907
+ "get_dependents",
5908
+ `Find all code that depends on a file (reverse dependency lookup). Use for impact analysis:
5909
+ - "What breaks if I change this?"
5910
+ - "Is this safe to delete?"
5911
+ - "What imports this module?"
5912
+
5913
+ Returns:
5914
+ - List of files that import the target
5915
+ - Risk level (low/medium/high/critical) based on dependent count and complexity
5916
+
5917
+ Example: get_dependents({ filepath: "src/utils/validate.ts" })`
5746
5918
  )
5747
5919
  ];
5748
5920
 
@@ -5942,6 +6114,50 @@ function wrapToolHandler(schema, handler) {
5942
6114
  };
5943
6115
  }
5944
6116
 
6117
+ // src/mcp/utils/path-matching.ts
6118
+ function normalizePath(path19, workspaceRoot) {
6119
+ let normalized = path19.replace(/['"]/g, "").trim().replace(/\\/g, "/");
6120
+ normalized = normalized.replace(/\.(ts|tsx|js|jsx)$/, "");
6121
+ if (normalized.startsWith(workspaceRoot + "/")) {
6122
+ normalized = normalized.substring(workspaceRoot.length + 1);
6123
+ }
6124
+ return normalized;
6125
+ }
6126
+ function matchesAtBoundary(str, pattern) {
6127
+ const index = str.indexOf(pattern);
6128
+ if (index === -1) return false;
6129
+ const charBefore = index > 0 ? str[index - 1] : "/";
6130
+ if (charBefore !== "/" && index !== 0) return false;
6131
+ const endIndex = index + pattern.length;
6132
+ if (endIndex === str.length) return true;
6133
+ const charAfter = str[endIndex];
6134
+ return charAfter === "/";
6135
+ }
6136
+ function matchesFile(normalizedImport, normalizedTarget) {
6137
+ if (normalizedImport === normalizedTarget) return true;
6138
+ if (matchesAtBoundary(normalizedImport, normalizedTarget)) {
6139
+ return true;
6140
+ }
6141
+ if (matchesAtBoundary(normalizedTarget, normalizedImport)) {
6142
+ return true;
6143
+ }
6144
+ const cleanedImport = normalizedImport.replace(/^(\.\.?\/)+/, "");
6145
+ if (matchesAtBoundary(cleanedImport, normalizedTarget) || matchesAtBoundary(normalizedTarget, cleanedImport)) {
6146
+ return true;
6147
+ }
6148
+ return false;
6149
+ }
6150
+ function getCanonicalPath(filepath, workspaceRoot) {
6151
+ let canonical = filepath.replace(/\\/g, "/");
6152
+ if (canonical.startsWith(workspaceRoot + "/")) {
6153
+ canonical = canonical.substring(workspaceRoot.length + 1);
6154
+ }
6155
+ return canonical;
6156
+ }
6157
+ function isTestFile2(filepath) {
6158
+ return /\.(test|spec)\.[^/]+$/.test(filepath) || /(^|[/\\])(test|tests|__tests__)[/\\]/.test(filepath);
6159
+ }
6160
+
5945
6161
  // src/mcp/server.ts
5946
6162
  init_errors();
5947
6163
  var __filename4 = fileURLToPath4(import.meta.url);
@@ -5953,6 +6169,31 @@ try {
5953
6169
  } catch {
5954
6170
  packageJson3 = require4(join3(__dirname4, "../../package.json"));
5955
6171
  }
6172
+ var DEPENDENT_COUNT_THRESHOLDS = {
6173
+ LOW: 5,
6174
+ // Few dependents, safe to change
6175
+ MEDIUM: 15,
6176
+ // Moderate impact, review dependents
6177
+ HIGH: 30
6178
+ // High impact, careful planning needed
6179
+ };
6180
+ var COMPLEXITY_THRESHOLDS = {
6181
+ HIGH_COMPLEXITY_DEPENDENT: 10,
6182
+ // Individual file is complex
6183
+ CRITICAL_AVG: 15,
6184
+ // Average complexity indicates systemic complexity
6185
+ CRITICAL_MAX: 25,
6186
+ // Peak complexity indicates hotspot
6187
+ HIGH_AVG: 10,
6188
+ // Moderately complex on average
6189
+ HIGH_MAX: 20,
6190
+ // Some complex functions exist
6191
+ MEDIUM_AVG: 6,
6192
+ // Slightly above simple code
6193
+ MEDIUM_MAX: 15
6194
+ // Occasional branching
6195
+ };
6196
+ var SCAN_LIMIT = 1e4;
5956
6197
  async function startMCPServer(options) {
5957
6198
  const { rootDir, verbose, watch } = options;
5958
6199
  const log = (message) => {
@@ -6049,6 +6290,7 @@ async function startMCPServer(options) {
6049
6290
  const isSingleFile = !Array.isArray(validatedArgs.filepaths);
6050
6291
  log(`Getting context for: ${filepaths.join(", ")}`);
6051
6292
  await checkAndReconnect();
6293
+ const workspaceRoot = process.cwd().replace(/\\/g, "/");
6052
6294
  const fileEmbeddings = await Promise.all(filepaths.map((fp) => embeddings.embed(fp)));
6053
6295
  const allFileSearches = await Promise.all(
6054
6296
  fileEmbeddings.map(
@@ -6057,9 +6299,11 @@ async function startMCPServer(options) {
6057
6299
  );
6058
6300
  const fileChunksMap = filepaths.map((filepath, i) => {
6059
6301
  const allResults = allFileSearches[i];
6060
- return allResults.filter(
6061
- (r) => r.metadata.file.includes(filepath) || filepath.includes(r.metadata.file)
6062
- );
6302
+ const targetCanonical = getCanonicalPath(filepath, workspaceRoot);
6303
+ return allResults.filter((r) => {
6304
+ const chunkCanonical = getCanonicalPath(r.metadata.file, workspaceRoot);
6305
+ return chunkCanonical === targetCanonical;
6306
+ });
6063
6307
  });
6064
6308
  let relatedChunksMap = [];
6065
6309
  if (validatedArgs.includeRelated) {
@@ -6076,18 +6320,57 @@ async function startMCPServer(options) {
6076
6320
  relatedChunksMap = Array.from({ length: filepaths.length }, () => []);
6077
6321
  filesWithChunks.forEach(({ filepath, index }, i) => {
6078
6322
  const related = relatedSearches[i];
6079
- relatedChunksMap[index] = related.filter(
6080
- (r) => !r.metadata.file.includes(filepath) && !filepath.includes(r.metadata.file)
6081
- );
6323
+ const targetCanonical = getCanonicalPath(filepath, workspaceRoot);
6324
+ relatedChunksMap[index] = related.filter((r) => {
6325
+ const chunkCanonical = getCanonicalPath(r.metadata.file, workspaceRoot);
6326
+ return chunkCanonical !== targetCanonical;
6327
+ });
6082
6328
  });
6083
6329
  }
6084
6330
  }
6331
+ const allChunks = await vectorDB.scanWithFilter({ limit: SCAN_LIMIT });
6332
+ if (allChunks.length === SCAN_LIMIT) {
6333
+ log(`WARNING: Scanned ${SCAN_LIMIT} chunks (limit reached). Test associations may be incomplete for large codebases.`);
6334
+ }
6335
+ const pathCache = /* @__PURE__ */ new Map();
6336
+ const normalizePathCached = (path19) => {
6337
+ if (pathCache.has(path19)) return pathCache.get(path19);
6338
+ const normalized = normalizePath(path19, workspaceRoot);
6339
+ pathCache.set(path19, normalized);
6340
+ return normalized;
6341
+ };
6342
+ const testAssociationsMap = filepaths.map((filepath) => {
6343
+ const normalizedTarget = normalizePathCached(filepath);
6344
+ const testFiles = /* @__PURE__ */ new Set();
6345
+ for (const chunk of allChunks) {
6346
+ const chunkFile2 = getCanonicalPath(chunk.metadata.file, workspaceRoot);
6347
+ if (!isTestFile2(chunkFile2)) continue;
6348
+ const imports = chunk.metadata.imports || [];
6349
+ for (const imp of imports) {
6350
+ const normalizedImport = normalizePathCached(imp);
6351
+ if (matchesFile(normalizedImport, normalizedTarget)) {
6352
+ testFiles.add(chunkFile2);
6353
+ break;
6354
+ }
6355
+ }
6356
+ }
6357
+ return Array.from(testFiles);
6358
+ });
6085
6359
  const filesData = {};
6086
6360
  filepaths.forEach((filepath, i) => {
6087
6361
  const fileChunks = fileChunksMap[i];
6088
6362
  const relatedChunks = relatedChunksMap[i] || [];
6363
+ const seenChunks = /* @__PURE__ */ new Set();
6364
+ const dedupedChunks = [...fileChunks, ...relatedChunks].filter((chunk) => {
6365
+ const canonicalFile = getCanonicalPath(chunk.metadata.file, workspaceRoot);
6366
+ const chunkId = `${canonicalFile}:${chunk.metadata.startLine}-${chunk.metadata.endLine}`;
6367
+ if (seenChunks.has(chunkId)) return false;
6368
+ seenChunks.add(chunkId);
6369
+ return true;
6370
+ });
6089
6371
  filesData[filepath] = {
6090
- chunks: [...fileChunks, ...relatedChunks]
6372
+ chunks: dedupedChunks,
6373
+ testAssociations: testAssociationsMap[i]
6091
6374
  };
6092
6375
  });
6093
6376
  log(`Found ${Object.values(filesData).reduce((sum, f) => sum + f.chunks.length, 0)} total chunks`);
@@ -6096,7 +6379,8 @@ async function startMCPServer(options) {
6096
6379
  return {
6097
6380
  indexInfo: getIndexMetadata(),
6098
6381
  file: filepath,
6099
- chunks: filesData[filepath].chunks
6382
+ chunks: filesData[filepath].chunks,
6383
+ testAssociations: filesData[filepath].testAssociations
6100
6384
  };
6101
6385
  } else {
6102
6386
  return {
@@ -6147,6 +6431,144 @@ async function startMCPServer(options) {
6147
6431
  };
6148
6432
  }
6149
6433
  )(args);
6434
+ case "get_dependents":
6435
+ return await wrapToolHandler(
6436
+ GetDependentsSchema,
6437
+ async (validatedArgs) => {
6438
+ log(`Finding dependents of: ${validatedArgs.filepath}`);
6439
+ await checkAndReconnect();
6440
+ const allChunks = await vectorDB.scanWithFilter({ limit: SCAN_LIMIT });
6441
+ if (allChunks.length === SCAN_LIMIT) {
6442
+ log(`WARNING: Scanned ${SCAN_LIMIT} chunks (limit reached). Results may be incomplete for large codebases.`);
6443
+ }
6444
+ log(`Scanning ${allChunks.length} chunks for imports...`);
6445
+ const workspaceRoot = process.cwd().replace(/\\/g, "/");
6446
+ const pathCache = /* @__PURE__ */ new Map();
6447
+ const normalizePathCached = (path19) => {
6448
+ if (pathCache.has(path19)) return pathCache.get(path19);
6449
+ const normalized = normalizePath(path19, workspaceRoot);
6450
+ pathCache.set(path19, normalized);
6451
+ return normalized;
6452
+ };
6453
+ const importIndex = /* @__PURE__ */ new Map();
6454
+ for (const chunk of allChunks) {
6455
+ const imports = chunk.metadata.imports || [];
6456
+ for (const imp of imports) {
6457
+ const normalizedImport = normalizePathCached(imp);
6458
+ if (!importIndex.has(normalizedImport)) {
6459
+ importIndex.set(normalizedImport, []);
6460
+ }
6461
+ importIndex.get(normalizedImport).push(chunk);
6462
+ }
6463
+ }
6464
+ const normalizedTarget = normalizePathCached(validatedArgs.filepath);
6465
+ const dependentChunks = [];
6466
+ const seenChunkIds = /* @__PURE__ */ new Set();
6467
+ if (importIndex.has(normalizedTarget)) {
6468
+ for (const chunk of importIndex.get(normalizedTarget)) {
6469
+ const chunkId = `${chunk.metadata.file}:${chunk.metadata.startLine}-${chunk.metadata.endLine}`;
6470
+ if (!seenChunkIds.has(chunkId)) {
6471
+ dependentChunks.push(chunk);
6472
+ seenChunkIds.add(chunkId);
6473
+ }
6474
+ }
6475
+ }
6476
+ for (const [normalizedImport, chunks] of importIndex.entries()) {
6477
+ if (normalizedImport !== normalizedTarget && matchesFile(normalizedImport, normalizedTarget)) {
6478
+ for (const chunk of chunks) {
6479
+ const chunkId = `${chunk.metadata.file}:${chunk.metadata.startLine}-${chunk.metadata.endLine}`;
6480
+ if (!seenChunkIds.has(chunkId)) {
6481
+ dependentChunks.push(chunk);
6482
+ seenChunkIds.add(chunkId);
6483
+ }
6484
+ }
6485
+ }
6486
+ }
6487
+ const chunksByFile = /* @__PURE__ */ new Map();
6488
+ for (const chunk of dependentChunks) {
6489
+ const canonical = getCanonicalPath(chunk.metadata.file, workspaceRoot);
6490
+ const existing = chunksByFile.get(canonical) || [];
6491
+ existing.push(chunk);
6492
+ chunksByFile.set(canonical, existing);
6493
+ }
6494
+ const fileComplexities = [];
6495
+ for (const [filepath, chunks] of chunksByFile.entries()) {
6496
+ const complexities = chunks.map((c) => c.metadata.complexity).filter((c) => typeof c === "number" && c > 0);
6497
+ if (complexities.length > 0) {
6498
+ const sum = complexities.reduce((a, b) => a + b, 0);
6499
+ const avg = sum / complexities.length;
6500
+ const max = Math.max(...complexities);
6501
+ fileComplexities.push({
6502
+ filepath,
6503
+ avgComplexity: Math.round(avg * 10) / 10,
6504
+ // Round to 1 decimal
6505
+ maxComplexity: max,
6506
+ complexityScore: sum,
6507
+ chunksWithComplexity: complexities.length
6508
+ });
6509
+ }
6510
+ }
6511
+ let complexityMetrics;
6512
+ if (fileComplexities.length > 0) {
6513
+ const allAvgs = fileComplexities.map((f) => f.avgComplexity);
6514
+ const allMaxes = fileComplexities.map((f) => f.maxComplexity);
6515
+ const totalAvg = allAvgs.reduce((a, b) => a + b, 0) / allAvgs.length;
6516
+ const globalMax = Math.max(...allMaxes);
6517
+ const highComplexityDependents = fileComplexities.filter((f) => f.maxComplexity > COMPLEXITY_THRESHOLDS.HIGH_COMPLEXITY_DEPENDENT).sort((a, b) => b.maxComplexity - a.maxComplexity).slice(0, 5).map((f) => ({
6518
+ filepath: f.filepath,
6519
+ maxComplexity: f.maxComplexity,
6520
+ avgComplexity: f.avgComplexity
6521
+ }));
6522
+ let complexityRiskBoost = "low";
6523
+ if (totalAvg > COMPLEXITY_THRESHOLDS.CRITICAL_AVG || globalMax > COMPLEXITY_THRESHOLDS.CRITICAL_MAX) {
6524
+ complexityRiskBoost = "critical";
6525
+ } else if (totalAvg > COMPLEXITY_THRESHOLDS.HIGH_AVG || globalMax > COMPLEXITY_THRESHOLDS.HIGH_MAX) {
6526
+ complexityRiskBoost = "high";
6527
+ } else if (totalAvg > COMPLEXITY_THRESHOLDS.MEDIUM_AVG || globalMax > COMPLEXITY_THRESHOLDS.MEDIUM_MAX) {
6528
+ complexityRiskBoost = "medium";
6529
+ }
6530
+ complexityMetrics = {
6531
+ averageComplexity: Math.round(totalAvg * 10) / 10,
6532
+ maxComplexity: globalMax,
6533
+ filesWithComplexityData: fileComplexities.length,
6534
+ highComplexityDependents,
6535
+ complexityRiskBoost
6536
+ };
6537
+ } else {
6538
+ complexityMetrics = {
6539
+ averageComplexity: 0,
6540
+ maxComplexity: 0,
6541
+ filesWithComplexityData: 0,
6542
+ highComplexityDependents: [],
6543
+ complexityRiskBoost: "low"
6544
+ };
6545
+ }
6546
+ const uniqueFiles = Array.from(chunksByFile.keys()).map((filepath) => ({
6547
+ filepath,
6548
+ isTestFile: isTestFile2(filepath)
6549
+ }));
6550
+ const count = uniqueFiles.length;
6551
+ let riskLevel = count === 0 ? "low" : count <= DEPENDENT_COUNT_THRESHOLDS.LOW ? "low" : count <= DEPENDENT_COUNT_THRESHOLDS.MEDIUM ? "medium" : count <= DEPENDENT_COUNT_THRESHOLDS.HIGH ? "high" : "critical";
6552
+ const RISK_ORDER = { low: 0, medium: 1, high: 2, critical: 3 };
6553
+ if (RISK_ORDER[complexityMetrics.complexityRiskBoost] > RISK_ORDER[riskLevel]) {
6554
+ riskLevel = complexityMetrics.complexityRiskBoost;
6555
+ }
6556
+ log(`Found ${count} dependent files (risk: ${riskLevel}${complexityMetrics.filesWithComplexityData > 0 ? ", complexity-boosted" : ""})`);
6557
+ let note;
6558
+ if (allChunks.length === SCAN_LIMIT) {
6559
+ note = `Warning: Scanned ${SCAN_LIMIT} chunks (limit reached). Results may be incomplete for large codebases. Some dependents might not be listed.`;
6560
+ }
6561
+ return {
6562
+ indexInfo: getIndexMetadata(),
6563
+ filepath: validatedArgs.filepath,
6564
+ dependentCount: count,
6565
+ riskLevel,
6566
+ dependents: uniqueFiles,
6567
+ complexityMetrics,
6568
+ note
6569
+ };
6570
+ }
6571
+ )(args);
6150
6572
  default:
6151
6573
  throw new LienError(
6152
6574
  `Unknown tool: ${name}`,
@@ -6319,11 +6741,11 @@ async function startMCPServer(options) {
6319
6741
 
6320
6742
  // src/cli/serve.ts
6321
6743
  async function serveCommand(options) {
6322
- const rootDir = options.root ? path17.resolve(options.root) : process.cwd();
6744
+ const rootDir = options.root ? path18.resolve(options.root) : process.cwd();
6323
6745
  try {
6324
6746
  if (options.root) {
6325
6747
  try {
6326
- const stats = await fs18.stat(rootDir);
6748
+ const stats = await fs19.stat(rootDir);
6327
6749
  if (!stats.isDirectory()) {
6328
6750
  console.error(chalk7.red(`Error: --root path is not a directory: ${rootDir}`));
6329
6751
  process.exit(1);