@liendev/lien 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +1692 -675
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/constants.ts
|
|
13
|
-
var DEFAULT_CHUNK_SIZE, DEFAULT_CHUNK_OVERLAP, DEFAULT_CONCURRENCY, DEFAULT_EMBEDDING_BATCH_SIZE, EMBEDDING_DIMENSIONS, DEFAULT_EMBEDDING_MODEL, DEFAULT_PORT, VERSION_CHECK_INTERVAL_MS, DEFAULT_GIT_POLL_INTERVAL_MS, DEFAULT_DEBOUNCE_MS, CURRENT_CONFIG_VERSION;
|
|
13
|
+
var DEFAULT_CHUNK_SIZE, DEFAULT_CHUNK_OVERLAP, DEFAULT_CONCURRENCY, DEFAULT_EMBEDDING_BATCH_SIZE, EMBEDDING_MICRO_BATCH_SIZE, VECTOR_DB_MAX_BATCH_SIZE, VECTOR_DB_MIN_BATCH_SIZE, EMBEDDING_DIMENSIONS, DEFAULT_EMBEDDING_MODEL, DEFAULT_PORT, VERSION_CHECK_INTERVAL_MS, DEFAULT_GIT_POLL_INTERVAL_MS, DEFAULT_DEBOUNCE_MS, CURRENT_CONFIG_VERSION, INDEX_FORMAT_VERSION;
|
|
14
14
|
var init_constants = __esm({
|
|
15
15
|
"src/constants.ts"() {
|
|
16
16
|
"use strict";
|
|
@@ -18,6 +18,9 @@ var init_constants = __esm({
|
|
|
18
18
|
DEFAULT_CHUNK_OVERLAP = 10;
|
|
19
19
|
DEFAULT_CONCURRENCY = 4;
|
|
20
20
|
DEFAULT_EMBEDDING_BATCH_SIZE = 50;
|
|
21
|
+
EMBEDDING_MICRO_BATCH_SIZE = 10;
|
|
22
|
+
VECTOR_DB_MAX_BATCH_SIZE = 1e3;
|
|
23
|
+
VECTOR_DB_MIN_BATCH_SIZE = 10;
|
|
21
24
|
EMBEDDING_DIMENSIONS = 384;
|
|
22
25
|
DEFAULT_EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
23
26
|
DEFAULT_PORT = 7133;
|
|
@@ -25,6 +28,7 @@ var init_constants = __esm({
|
|
|
25
28
|
DEFAULT_GIT_POLL_INTERVAL_MS = 1e4;
|
|
26
29
|
DEFAULT_DEBOUNCE_MS = 1e3;
|
|
27
30
|
CURRENT_CONFIG_VERSION = "0.3.0";
|
|
31
|
+
INDEX_FORMAT_VERSION = 1;
|
|
28
32
|
}
|
|
29
33
|
});
|
|
30
34
|
|
|
@@ -58,8 +62,8 @@ var init_schema = __esm({
|
|
|
58
62
|
pollIntervalMs: DEFAULT_GIT_POLL_INTERVAL_MS
|
|
59
63
|
},
|
|
60
64
|
fileWatching: {
|
|
61
|
-
enabled:
|
|
62
|
-
//
|
|
65
|
+
enabled: true,
|
|
66
|
+
// Enabled by default (fast with incremental indexing!)
|
|
63
67
|
debounceMs: DEFAULT_DEBOUNCE_MS
|
|
64
68
|
},
|
|
65
69
|
frameworks: []
|
|
@@ -116,75 +120,6 @@ var init_merge = __esm({
|
|
|
116
120
|
}
|
|
117
121
|
});
|
|
118
122
|
|
|
119
|
-
// src/utils/banner.ts
|
|
120
|
-
var banner_exports = {};
|
|
121
|
-
__export(banner_exports, {
|
|
122
|
-
showBanner: () => showBanner,
|
|
123
|
-
showCompactBanner: () => showCompactBanner
|
|
124
|
-
});
|
|
125
|
-
import figlet from "figlet";
|
|
126
|
-
import chalk from "chalk";
|
|
127
|
-
import { createRequire } from "module";
|
|
128
|
-
import { fileURLToPath } from "url";
|
|
129
|
-
import { dirname, join } from "path";
|
|
130
|
-
function wrapInBox(text, footer, padding = 1) {
|
|
131
|
-
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
132
|
-
const maxLength = Math.max(...lines.map((line) => line.length));
|
|
133
|
-
const horizontalBorder = "\u2500".repeat(maxLength + padding * 2);
|
|
134
|
-
const top = `\u250C${horizontalBorder}\u2510`;
|
|
135
|
-
const bottom = `\u2514${horizontalBorder}\u2518`;
|
|
136
|
-
const separator = `\u251C${horizontalBorder}\u2524`;
|
|
137
|
-
const paddedLines = lines.map((line) => {
|
|
138
|
-
const padRight = " ".repeat(maxLength - line.length + padding);
|
|
139
|
-
const padLeft = " ".repeat(padding);
|
|
140
|
-
return `\u2502${padLeft}${line}${padRight}\u2502`;
|
|
141
|
-
});
|
|
142
|
-
const totalPad = maxLength - footer.length;
|
|
143
|
-
const leftPad = Math.floor(totalPad / 2);
|
|
144
|
-
const rightPad = totalPad - leftPad;
|
|
145
|
-
const centeredFooter = " ".repeat(leftPad) + footer + " ".repeat(rightPad);
|
|
146
|
-
const paddedFooter = `\u2502${" ".repeat(padding)}${centeredFooter}${" ".repeat(padding)}\u2502`;
|
|
147
|
-
return [top, ...paddedLines, separator, paddedFooter, bottom].join("\n");
|
|
148
|
-
}
|
|
149
|
-
function showBanner() {
|
|
150
|
-
const banner = figlet.textSync("LIEN", {
|
|
151
|
-
font: "ANSI Shadow",
|
|
152
|
-
horizontalLayout: "fitted",
|
|
153
|
-
verticalLayout: "fitted"
|
|
154
|
-
});
|
|
155
|
-
const footer = `${PACKAGE_NAME} - v${VERSION}`;
|
|
156
|
-
const boxedBanner = wrapInBox(banner.trim(), footer);
|
|
157
|
-
console.error(chalk.cyan(boxedBanner));
|
|
158
|
-
console.error();
|
|
159
|
-
}
|
|
160
|
-
function showCompactBanner() {
|
|
161
|
-
const banner = figlet.textSync("LIEN", {
|
|
162
|
-
font: "ANSI Shadow",
|
|
163
|
-
horizontalLayout: "fitted",
|
|
164
|
-
verticalLayout: "fitted"
|
|
165
|
-
});
|
|
166
|
-
const footer = `${PACKAGE_NAME} - v${VERSION}`;
|
|
167
|
-
const boxedBanner = wrapInBox(banner.trim(), footer);
|
|
168
|
-
console.log(chalk.cyan(boxedBanner));
|
|
169
|
-
console.log();
|
|
170
|
-
}
|
|
171
|
-
var __filename, __dirname, require2, packageJson, PACKAGE_NAME, VERSION;
|
|
172
|
-
var init_banner = __esm({
|
|
173
|
-
"src/utils/banner.ts"() {
|
|
174
|
-
"use strict";
|
|
175
|
-
__filename = fileURLToPath(import.meta.url);
|
|
176
|
-
__dirname = dirname(__filename);
|
|
177
|
-
require2 = createRequire(import.meta.url);
|
|
178
|
-
try {
|
|
179
|
-
packageJson = require2(join(__dirname, "../package.json"));
|
|
180
|
-
} catch {
|
|
181
|
-
packageJson = require2(join(__dirname, "../../package.json"));
|
|
182
|
-
}
|
|
183
|
-
PACKAGE_NAME = packageJson.name;
|
|
184
|
-
VERSION = packageJson.version;
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
|
|
188
123
|
// src/config/migration.ts
|
|
189
124
|
function needsMigration(config) {
|
|
190
125
|
if (!config) {
|
|
@@ -329,8 +264,8 @@ var init_errors = __esm({
|
|
|
329
264
|
});
|
|
330
265
|
|
|
331
266
|
// src/config/service.ts
|
|
332
|
-
import
|
|
333
|
-
import
|
|
267
|
+
import fs6 from "fs/promises";
|
|
268
|
+
import path6 from "path";
|
|
334
269
|
var ConfigService, configService;
|
|
335
270
|
var init_service = __esm({
|
|
336
271
|
"src/config/service.ts"() {
|
|
@@ -352,13 +287,13 @@ var init_service = __esm({
|
|
|
352
287
|
async load(rootDir = process.cwd()) {
|
|
353
288
|
const configPath = this.getConfigPath(rootDir);
|
|
354
289
|
try {
|
|
355
|
-
const configContent = await
|
|
290
|
+
const configContent = await fs6.readFile(configPath, "utf-8");
|
|
356
291
|
const userConfig = JSON.parse(configContent);
|
|
357
292
|
if (this.needsMigration(userConfig)) {
|
|
358
293
|
console.log("\u{1F504} Migrating config from v0.2.0 to v0.3.0...");
|
|
359
294
|
const result = await this.migrate(rootDir);
|
|
360
295
|
if (result.migrated && result.backupPath) {
|
|
361
|
-
const backupFilename =
|
|
296
|
+
const backupFilename = path6.basename(result.backupPath);
|
|
362
297
|
console.log(`\u2705 Migration complete! Backup saved as ${backupFilename}`);
|
|
363
298
|
console.log("\u{1F4DD} Your config now uses the framework-based structure.");
|
|
364
299
|
}
|
|
@@ -414,7 +349,7 @@ ${validation.errors.join("\n")}`,
|
|
|
414
349
|
}
|
|
415
350
|
try {
|
|
416
351
|
const configJson = JSON.stringify(config, null, 2) + "\n";
|
|
417
|
-
await
|
|
352
|
+
await fs6.writeFile(configPath, configJson, "utf-8");
|
|
418
353
|
} catch (error) {
|
|
419
354
|
throw wrapError(error, "Failed to save configuration", { path: configPath });
|
|
420
355
|
}
|
|
@@ -428,7 +363,7 @@ ${validation.errors.join("\n")}`,
|
|
|
428
363
|
async exists(rootDir = process.cwd()) {
|
|
429
364
|
const configPath = this.getConfigPath(rootDir);
|
|
430
365
|
try {
|
|
431
|
-
await
|
|
366
|
+
await fs6.access(configPath);
|
|
432
367
|
return true;
|
|
433
368
|
} catch {
|
|
434
369
|
return false;
|
|
@@ -445,7 +380,7 @@ ${validation.errors.join("\n")}`,
|
|
|
445
380
|
async migrate(rootDir = process.cwd()) {
|
|
446
381
|
const configPath = this.getConfigPath(rootDir);
|
|
447
382
|
try {
|
|
448
|
-
const configContent = await
|
|
383
|
+
const configContent = await fs6.readFile(configPath, "utf-8");
|
|
449
384
|
const oldConfig = JSON.parse(configContent);
|
|
450
385
|
if (!this.needsMigration(oldConfig)) {
|
|
451
386
|
return {
|
|
@@ -463,7 +398,7 @@ ${validation.errors.join("\n")}`,
|
|
|
463
398
|
);
|
|
464
399
|
}
|
|
465
400
|
const backupPath = `${configPath}.v0.2.0.backup`;
|
|
466
|
-
await
|
|
401
|
+
await fs6.copyFile(configPath, backupPath);
|
|
467
402
|
await this.save(rootDir, newConfig);
|
|
468
403
|
return {
|
|
469
404
|
migrated: true,
|
|
@@ -561,7 +496,7 @@ ${validation.errors.join("\n")}`,
|
|
|
561
496
|
* Get the full path to the config file
|
|
562
497
|
*/
|
|
563
498
|
getConfigPath(rootDir) {
|
|
564
|
-
return
|
|
499
|
+
return path6.join(rootDir, _ConfigService.CONFIG_FILENAME);
|
|
565
500
|
}
|
|
566
501
|
/**
|
|
567
502
|
* Validate modern (v0.3.0+) configuration
|
|
@@ -725,7 +660,7 @@ ${validation.errors.join("\n")}`,
|
|
|
725
660
|
errors.push(`frameworks[${index}] missing required field: path`);
|
|
726
661
|
} else if (typeof fw.path !== "string") {
|
|
727
662
|
errors.push(`frameworks[${index}].path must be a string`);
|
|
728
|
-
} else if (
|
|
663
|
+
} else if (path6.isAbsolute(fw.path)) {
|
|
729
664
|
errors.push(`frameworks[${index}].path must be relative, got: ${fw.path}`);
|
|
730
665
|
}
|
|
731
666
|
if (fw.enabled === void 0) {
|
|
@@ -772,22 +707,131 @@ ${validation.errors.join("\n")}`,
|
|
|
772
707
|
}
|
|
773
708
|
});
|
|
774
709
|
|
|
775
|
-
// src/
|
|
710
|
+
// src/git/utils.ts
|
|
711
|
+
var utils_exports = {};
|
|
712
|
+
__export(utils_exports, {
|
|
713
|
+
getChangedFiles: () => getChangedFiles,
|
|
714
|
+
getChangedFilesBetweenCommits: () => getChangedFilesBetweenCommits,
|
|
715
|
+
getChangedFilesInCommit: () => getChangedFilesInCommit,
|
|
716
|
+
getCurrentBranch: () => getCurrentBranch,
|
|
717
|
+
getCurrentCommit: () => getCurrentCommit,
|
|
718
|
+
isGitAvailable: () => isGitAvailable,
|
|
719
|
+
isGitRepo: () => isGitRepo
|
|
720
|
+
});
|
|
721
|
+
import { exec } from "child_process";
|
|
722
|
+
import { promisify } from "util";
|
|
776
723
|
import fs7 from "fs/promises";
|
|
777
724
|
import path7 from "path";
|
|
725
|
+
async function isGitRepo(rootDir) {
|
|
726
|
+
try {
|
|
727
|
+
const gitDir = path7.join(rootDir, ".git");
|
|
728
|
+
await fs7.access(gitDir);
|
|
729
|
+
return true;
|
|
730
|
+
} catch {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function getCurrentBranch(rootDir) {
|
|
735
|
+
try {
|
|
736
|
+
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
|
737
|
+
cwd: rootDir,
|
|
738
|
+
timeout: 5e3
|
|
739
|
+
// 5 second timeout
|
|
740
|
+
});
|
|
741
|
+
return stdout.trim();
|
|
742
|
+
} catch (error) {
|
|
743
|
+
throw new Error(`Failed to get current branch: ${error}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function getCurrentCommit(rootDir) {
|
|
747
|
+
try {
|
|
748
|
+
const { stdout } = await execAsync("git rev-parse HEAD", {
|
|
749
|
+
cwd: rootDir,
|
|
750
|
+
timeout: 5e3
|
|
751
|
+
});
|
|
752
|
+
return stdout.trim();
|
|
753
|
+
} catch (error) {
|
|
754
|
+
throw new Error(`Failed to get current commit: ${error}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async function getChangedFiles(rootDir, fromRef, toRef) {
|
|
758
|
+
try {
|
|
759
|
+
const { stdout } = await execAsync(
|
|
760
|
+
`git diff --name-only ${fromRef}...${toRef}`,
|
|
761
|
+
{
|
|
762
|
+
cwd: rootDir,
|
|
763
|
+
timeout: 1e4
|
|
764
|
+
// 10 second timeout for diffs
|
|
765
|
+
}
|
|
766
|
+
);
|
|
767
|
+
const files = stdout.trim().split("\n").filter(Boolean).map((file) => path7.join(rootDir, file));
|
|
768
|
+
return files;
|
|
769
|
+
} catch (error) {
|
|
770
|
+
throw new Error(`Failed to get changed files: ${error}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function getChangedFilesInCommit(rootDir, commitSha) {
|
|
774
|
+
try {
|
|
775
|
+
const { stdout } = await execAsync(
|
|
776
|
+
`git diff-tree --no-commit-id --name-only -r ${commitSha}`,
|
|
777
|
+
{
|
|
778
|
+
cwd: rootDir,
|
|
779
|
+
timeout: 1e4
|
|
780
|
+
}
|
|
781
|
+
);
|
|
782
|
+
const files = stdout.trim().split("\n").filter(Boolean).map((file) => path7.join(rootDir, file));
|
|
783
|
+
return files;
|
|
784
|
+
} catch (error) {
|
|
785
|
+
throw new Error(`Failed to get changed files in commit: ${error}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
async function getChangedFilesBetweenCommits(rootDir, fromCommit, toCommit) {
|
|
789
|
+
try {
|
|
790
|
+
const { stdout } = await execAsync(
|
|
791
|
+
`git diff --name-only ${fromCommit} ${toCommit}`,
|
|
792
|
+
{
|
|
793
|
+
cwd: rootDir,
|
|
794
|
+
timeout: 1e4
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
const files = stdout.trim().split("\n").filter(Boolean).map((file) => path7.join(rootDir, file));
|
|
798
|
+
return files;
|
|
799
|
+
} catch (error) {
|
|
800
|
+
throw new Error(`Failed to get changed files between commits: ${error}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async function isGitAvailable() {
|
|
804
|
+
try {
|
|
805
|
+
await execAsync("git --version", { timeout: 3e3 });
|
|
806
|
+
return true;
|
|
807
|
+
} catch {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
var execAsync;
|
|
812
|
+
var init_utils = __esm({
|
|
813
|
+
"src/git/utils.ts"() {
|
|
814
|
+
"use strict";
|
|
815
|
+
execAsync = promisify(exec);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// src/vectordb/version.ts
|
|
820
|
+
import fs8 from "fs/promises";
|
|
821
|
+
import path8 from "path";
|
|
778
822
|
async function writeVersionFile(indexPath) {
|
|
779
823
|
try {
|
|
780
|
-
const versionFilePath =
|
|
824
|
+
const versionFilePath = path8.join(indexPath, VERSION_FILE);
|
|
781
825
|
const timestamp = Date.now().toString();
|
|
782
|
-
await
|
|
826
|
+
await fs8.writeFile(versionFilePath, timestamp, "utf-8");
|
|
783
827
|
} catch (error) {
|
|
784
828
|
console.error(`Warning: Failed to write version file: ${error}`);
|
|
785
829
|
}
|
|
786
830
|
}
|
|
787
831
|
async function readVersionFile(indexPath) {
|
|
788
832
|
try {
|
|
789
|
-
const versionFilePath =
|
|
790
|
-
const content = await
|
|
833
|
+
const versionFilePath = path8.join(indexPath, VERSION_FILE);
|
|
834
|
+
const content = await fs8.readFile(versionFilePath, "utf-8");
|
|
791
835
|
const timestamp = parseInt(content.trim(), 10);
|
|
792
836
|
return isNaN(timestamp) ? 0 : timestamp;
|
|
793
837
|
} catch (error) {
|
|
@@ -805,8 +849,8 @@ var init_version = __esm({
|
|
|
805
849
|
// src/indexer/scanner.ts
|
|
806
850
|
import { glob } from "glob";
|
|
807
851
|
import ignore from "ignore";
|
|
808
|
-
import
|
|
809
|
-
import
|
|
852
|
+
import fs10 from "fs/promises";
|
|
853
|
+
import path10 from "path";
|
|
810
854
|
async function scanCodebaseWithFrameworks(rootDir, config) {
|
|
811
855
|
const allFiles = [];
|
|
812
856
|
for (const framework of config.frameworks) {
|
|
@@ -819,16 +863,16 @@ async function scanCodebaseWithFrameworks(rootDir, config) {
|
|
|
819
863
|
return allFiles;
|
|
820
864
|
}
|
|
821
865
|
async function scanFramework(rootDir, framework) {
|
|
822
|
-
const frameworkPath =
|
|
823
|
-
const gitignorePath =
|
|
866
|
+
const frameworkPath = path10.join(rootDir, framework.path);
|
|
867
|
+
const gitignorePath = path10.join(frameworkPath, ".gitignore");
|
|
824
868
|
let ig = ignore();
|
|
825
869
|
try {
|
|
826
|
-
const gitignoreContent = await
|
|
870
|
+
const gitignoreContent = await fs10.readFile(gitignorePath, "utf-8");
|
|
827
871
|
ig = ignore().add(gitignoreContent);
|
|
828
872
|
} catch (e) {
|
|
829
|
-
const rootGitignorePath =
|
|
873
|
+
const rootGitignorePath = path10.join(rootDir, ".gitignore");
|
|
830
874
|
try {
|
|
831
|
-
const gitignoreContent = await
|
|
875
|
+
const gitignoreContent = await fs10.readFile(rootGitignorePath, "utf-8");
|
|
832
876
|
ig = ignore().add(gitignoreContent);
|
|
833
877
|
} catch (e2) {
|
|
834
878
|
}
|
|
@@ -850,15 +894,15 @@ async function scanFramework(rootDir, framework) {
|
|
|
850
894
|
}
|
|
851
895
|
const uniqueFiles = Array.from(new Set(allFiles));
|
|
852
896
|
return uniqueFiles.filter((file) => !ig.ignores(file)).map((file) => {
|
|
853
|
-
return framework.path === "." ? file :
|
|
897
|
+
return framework.path === "." ? file : path10.join(framework.path, file);
|
|
854
898
|
});
|
|
855
899
|
}
|
|
856
900
|
async function scanCodebase(options) {
|
|
857
901
|
const { rootDir, includePatterns = [], excludePatterns = [] } = options;
|
|
858
|
-
const gitignorePath =
|
|
902
|
+
const gitignorePath = path10.join(rootDir, ".gitignore");
|
|
859
903
|
let ig = ignore();
|
|
860
904
|
try {
|
|
861
|
-
const gitignoreContent = await
|
|
905
|
+
const gitignoreContent = await fs10.readFile(gitignorePath, "utf-8");
|
|
862
906
|
ig = ignore().add(gitignoreContent);
|
|
863
907
|
} catch (e) {
|
|
864
908
|
}
|
|
@@ -885,12 +929,12 @@ async function scanCodebase(options) {
|
|
|
885
929
|
}
|
|
886
930
|
const uniqueFiles = Array.from(new Set(allFiles));
|
|
887
931
|
return uniqueFiles.filter((file) => {
|
|
888
|
-
const relativePath =
|
|
932
|
+
const relativePath = path10.relative(rootDir, file);
|
|
889
933
|
return !ig.ignores(relativePath);
|
|
890
934
|
});
|
|
891
935
|
}
|
|
892
936
|
function detectLanguage(filepath) {
|
|
893
|
-
const ext =
|
|
937
|
+
const ext = path10.extname(filepath).toLowerCase();
|
|
894
938
|
const languageMap = {
|
|
895
939
|
".ts": "typescript",
|
|
896
940
|
".tsx": "typescript",
|
|
@@ -915,6 +959,7 @@ function detectLanguage(filepath) {
|
|
|
915
959
|
".kt": "kotlin",
|
|
916
960
|
".cs": "csharp",
|
|
917
961
|
".scala": "scala",
|
|
962
|
+
".liquid": "liquid",
|
|
918
963
|
".md": "markdown",
|
|
919
964
|
".mdx": "markdown",
|
|
920
965
|
".markdown": "markdown"
|
|
@@ -1380,12 +1425,12 @@ __export(lancedb_exports, {
|
|
|
1380
1425
|
VectorDB: () => VectorDB
|
|
1381
1426
|
});
|
|
1382
1427
|
import * as lancedb from "vectordb";
|
|
1383
|
-
import
|
|
1428
|
+
import path11 from "path";
|
|
1384
1429
|
import os2 from "os";
|
|
1385
1430
|
import crypto2 from "crypto";
|
|
1386
1431
|
function isDocumentationFile(filepath) {
|
|
1387
1432
|
const lower = filepath.toLowerCase();
|
|
1388
|
-
const filename =
|
|
1433
|
+
const filename = path11.basename(filepath).toLowerCase();
|
|
1389
1434
|
if (filename.startsWith("readme")) return true;
|
|
1390
1435
|
if (filename.startsWith("changelog")) return true;
|
|
1391
1436
|
if (filename.endsWith(".md") || filename.endsWith(".mdx") || filename.endsWith(".markdown")) {
|
|
@@ -1432,7 +1477,7 @@ function boostPathRelevance(query, filepath, baseScore) {
|
|
|
1432
1477
|
return baseScore * boostFactor;
|
|
1433
1478
|
}
|
|
1434
1479
|
function boostFilenameRelevance(query, filepath, baseScore) {
|
|
1435
|
-
const filename =
|
|
1480
|
+
const filename = path11.basename(filepath, path11.extname(filepath)).toLowerCase();
|
|
1436
1481
|
const queryTokens = query.toLowerCase().split(/\s+/);
|
|
1437
1482
|
let boostFactor = 1;
|
|
1438
1483
|
for (const token of queryTokens) {
|
|
@@ -1447,7 +1492,7 @@ function boostFilenameRelevance(query, filepath, baseScore) {
|
|
|
1447
1492
|
}
|
|
1448
1493
|
function boostForLocationIntent(query, filepath, baseScore) {
|
|
1449
1494
|
let score = baseScore;
|
|
1450
|
-
const filename =
|
|
1495
|
+
const filename = path11.basename(filepath, path11.extname(filepath)).toLowerCase();
|
|
1451
1496
|
const queryTokens = query.toLowerCase().split(/\s+/);
|
|
1452
1497
|
for (const token of queryTokens) {
|
|
1453
1498
|
if (token.length <= 2) continue;
|
|
@@ -1475,7 +1520,7 @@ function boostForConceptualIntent(query, filepath, baseScore) {
|
|
|
1475
1520
|
if (isUtilityFile(filepath)) {
|
|
1476
1521
|
score *= 1.05;
|
|
1477
1522
|
}
|
|
1478
|
-
const filename =
|
|
1523
|
+
const filename = path11.basename(filepath, path11.extname(filepath)).toLowerCase();
|
|
1479
1524
|
const queryTokens = query.toLowerCase().split(/\s+/);
|
|
1480
1525
|
for (const token of queryTokens) {
|
|
1481
1526
|
if (token.length <= 2) continue;
|
|
@@ -1483,7 +1528,7 @@ function boostForConceptualIntent(query, filepath, baseScore) {
|
|
|
1483
1528
|
score *= 0.9;
|
|
1484
1529
|
}
|
|
1485
1530
|
}
|
|
1486
|
-
const pathSegments = filepath.toLowerCase().split(
|
|
1531
|
+
const pathSegments = filepath.toLowerCase().split(path11.sep);
|
|
1487
1532
|
for (const token of queryTokens) {
|
|
1488
1533
|
if (token.length <= 2) continue;
|
|
1489
1534
|
for (const segment of pathSegments) {
|
|
@@ -1529,6 +1574,7 @@ var init_lancedb = __esm({
|
|
|
1529
1574
|
init_errors();
|
|
1530
1575
|
init_relevance();
|
|
1531
1576
|
init_intent_classifier();
|
|
1577
|
+
init_constants();
|
|
1532
1578
|
VectorDB = class _VectorDB {
|
|
1533
1579
|
db = null;
|
|
1534
1580
|
table = null;
|
|
@@ -1537,9 +1583,9 @@ var init_lancedb = __esm({
|
|
|
1537
1583
|
lastVersionCheck = 0;
|
|
1538
1584
|
currentVersion = 0;
|
|
1539
1585
|
constructor(projectRoot) {
|
|
1540
|
-
const projectName =
|
|
1586
|
+
const projectName = path11.basename(projectRoot);
|
|
1541
1587
|
const pathHash = crypto2.createHash("md5").update(projectRoot).digest("hex").substring(0, 8);
|
|
1542
|
-
this.dbPath =
|
|
1588
|
+
this.dbPath = path11.join(
|
|
1543
1589
|
os2.homedir(),
|
|
1544
1590
|
".lien",
|
|
1545
1591
|
"indices",
|
|
@@ -1574,27 +1620,76 @@ var init_lancedb = __esm({
|
|
|
1574
1620
|
contentsLength: contents.length
|
|
1575
1621
|
});
|
|
1576
1622
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
// Ensure arrays have at least empty string for Arrow type inference
|
|
1587
|
-
functionNames: metadatas[i].symbols?.functions && metadatas[i].symbols.functions.length > 0 ? metadatas[i].symbols.functions : [""],
|
|
1588
|
-
classNames: metadatas[i].symbols?.classes && metadatas[i].symbols.classes.length > 0 ? metadatas[i].symbols.classes : [""],
|
|
1589
|
-
interfaceNames: metadatas[i].symbols?.interfaces && metadatas[i].symbols.interfaces.length > 0 ? metadatas[i].symbols.interfaces : [""]
|
|
1590
|
-
}));
|
|
1591
|
-
if (!this.table) {
|
|
1592
|
-
this.table = await this.db.createTable(this.tableName, records);
|
|
1593
|
-
} else {
|
|
1594
|
-
await this.table.add(records);
|
|
1623
|
+
if (vectors.length === 0) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (vectors.length > VECTOR_DB_MAX_BATCH_SIZE) {
|
|
1627
|
+
for (let i = 0; i < vectors.length; i += VECTOR_DB_MAX_BATCH_SIZE) {
|
|
1628
|
+
const batchVectors = vectors.slice(i, Math.min(i + VECTOR_DB_MAX_BATCH_SIZE, vectors.length));
|
|
1629
|
+
const batchMetadata = metadatas.slice(i, Math.min(i + VECTOR_DB_MAX_BATCH_SIZE, vectors.length));
|
|
1630
|
+
const batchContents = contents.slice(i, Math.min(i + VECTOR_DB_MAX_BATCH_SIZE, vectors.length));
|
|
1631
|
+
await this._insertBatchInternal(batchVectors, batchMetadata, batchContents);
|
|
1595
1632
|
}
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1633
|
+
} else {
|
|
1634
|
+
await this._insertBatchInternal(vectors, metadatas, contents);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Internal method to insert a single batch with iterative retry logic.
|
|
1639
|
+
* Uses a queue-based approach to avoid deep recursion on large batch failures.
|
|
1640
|
+
*/
|
|
1641
|
+
async _insertBatchInternal(vectors, metadatas, contents) {
|
|
1642
|
+
const queue = [{ vectors, metadatas, contents }];
|
|
1643
|
+
const failedRecords = [];
|
|
1644
|
+
while (queue.length > 0) {
|
|
1645
|
+
const batch = queue.shift();
|
|
1646
|
+
try {
|
|
1647
|
+
const records = batch.vectors.map((vector, i) => ({
|
|
1648
|
+
vector: Array.from(vector),
|
|
1649
|
+
content: batch.contents[i],
|
|
1650
|
+
file: batch.metadatas[i].file,
|
|
1651
|
+
startLine: batch.metadatas[i].startLine,
|
|
1652
|
+
endLine: batch.metadatas[i].endLine,
|
|
1653
|
+
type: batch.metadatas[i].type,
|
|
1654
|
+
language: batch.metadatas[i].language,
|
|
1655
|
+
// Ensure arrays have at least empty string for Arrow type inference
|
|
1656
|
+
functionNames: batch.metadatas[i].symbols?.functions && batch.metadatas[i].symbols.functions.length > 0 ? batch.metadatas[i].symbols.functions : [""],
|
|
1657
|
+
classNames: batch.metadatas[i].symbols?.classes && batch.metadatas[i].symbols.classes.length > 0 ? batch.metadatas[i].symbols.classes : [""],
|
|
1658
|
+
interfaceNames: batch.metadatas[i].symbols?.interfaces && batch.metadatas[i].symbols.interfaces.length > 0 ? batch.metadatas[i].symbols.interfaces : [""]
|
|
1659
|
+
}));
|
|
1660
|
+
if (!this.table) {
|
|
1661
|
+
this.table = await this.db.createTable(this.tableName, records);
|
|
1662
|
+
} else {
|
|
1663
|
+
await this.table.add(records);
|
|
1664
|
+
}
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
if (batch.vectors.length > VECTOR_DB_MIN_BATCH_SIZE) {
|
|
1667
|
+
const half = Math.floor(batch.vectors.length / 2);
|
|
1668
|
+
queue.push({
|
|
1669
|
+
vectors: batch.vectors.slice(0, half),
|
|
1670
|
+
metadatas: batch.metadatas.slice(0, half),
|
|
1671
|
+
contents: batch.contents.slice(0, half)
|
|
1672
|
+
});
|
|
1673
|
+
queue.push({
|
|
1674
|
+
vectors: batch.vectors.slice(half),
|
|
1675
|
+
metadatas: batch.metadatas.slice(half),
|
|
1676
|
+
contents: batch.contents.slice(half)
|
|
1677
|
+
});
|
|
1678
|
+
} else {
|
|
1679
|
+
failedRecords.push(batch);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (failedRecords.length > 0) {
|
|
1684
|
+
const totalFailed = failedRecords.reduce((sum, batch) => sum + batch.vectors.length, 0);
|
|
1685
|
+
throw new DatabaseError(
|
|
1686
|
+
`Failed to insert ${totalFailed} record(s) after retry attempts`,
|
|
1687
|
+
{
|
|
1688
|
+
failedBatches: failedRecords.length,
|
|
1689
|
+
totalRecords: totalFailed,
|
|
1690
|
+
sampleFile: failedRecords[0].metadatas[0].file
|
|
1691
|
+
}
|
|
1692
|
+
);
|
|
1598
1693
|
}
|
|
1599
1694
|
}
|
|
1600
1695
|
async search(queryVector, limit = 5, query) {
|
|
@@ -1892,86 +1987,1027 @@ var init_lancedb = __esm({
|
|
|
1892
1987
|
}
|
|
1893
1988
|
});
|
|
1894
1989
|
|
|
1895
|
-
// src/
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
includePatterns: config.indexing.include,
|
|
1918
|
-
excludePatterns: config.indexing.exclude
|
|
1919
|
-
});
|
|
1920
|
-
} else {
|
|
1921
|
-
files = await scanCodebase({
|
|
1922
|
-
rootDir,
|
|
1923
|
-
includePatterns: [],
|
|
1924
|
-
excludePatterns: []
|
|
1925
|
-
});
|
|
1926
|
-
}
|
|
1927
|
-
if (files.length === 0) {
|
|
1928
|
-
spinner.fail("No files found to index");
|
|
1929
|
-
return;
|
|
1930
|
-
}
|
|
1931
|
-
spinner.text = `Found ${files.length} files`;
|
|
1932
|
-
spinner.text = "Loading embedding model (this may take a minute on first run)...";
|
|
1933
|
-
const embeddings = new LocalEmbeddings();
|
|
1934
|
-
await embeddings.initialize();
|
|
1935
|
-
spinner.succeed("Embedding model loaded");
|
|
1936
|
-
spinner.start("Initializing vector database...");
|
|
1937
|
-
const vectorDB = new VectorDB(rootDir);
|
|
1938
|
-
await vectorDB.initialize();
|
|
1939
|
-
spinner.succeed("Vector database initialized");
|
|
1940
|
-
const concurrency = isModernConfig(config) ? config.core.concurrency : 4;
|
|
1941
|
-
const batchSize = isModernConfig(config) ? config.core.embeddingBatchSize : 50;
|
|
1942
|
-
spinner.start(`Processing files with ${concurrency}x concurrency...`);
|
|
1943
|
-
const startTime = Date.now();
|
|
1944
|
-
let processedFiles = 0;
|
|
1945
|
-
let processedChunks = 0;
|
|
1946
|
-
const chunkAccumulator = [];
|
|
1947
|
-
const limit = pLimit(concurrency);
|
|
1948
|
-
const processAccumulatedChunks = async () => {
|
|
1949
|
-
if (chunkAccumulator.length === 0) return;
|
|
1950
|
-
const toProcess = chunkAccumulator.splice(0, chunkAccumulator.length);
|
|
1951
|
-
for (let i = 0; i < toProcess.length; i += batchSize) {
|
|
1952
|
-
const batch = toProcess.slice(i, Math.min(i + batchSize, toProcess.length));
|
|
1953
|
-
const texts = batch.map((item) => item.content);
|
|
1954
|
-
const embeddingVectors = await embeddings.embedBatch(texts);
|
|
1955
|
-
await vectorDB.insertBatch(
|
|
1956
|
-
embeddingVectors,
|
|
1957
|
-
batch.map((item) => item.chunk.metadata),
|
|
1958
|
-
texts
|
|
1959
|
-
);
|
|
1960
|
-
processedChunks += batch.length;
|
|
1990
|
+
// src/utils/version.ts
|
|
1991
|
+
import { createRequire as createRequire2 } from "module";
|
|
1992
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1993
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1994
|
+
function getPackageVersion() {
|
|
1995
|
+
return packageJson2.version;
|
|
1996
|
+
}
|
|
1997
|
+
var __filename3, __dirname3, require3, packageJson2;
|
|
1998
|
+
var init_version2 = __esm({
|
|
1999
|
+
"src/utils/version.ts"() {
|
|
2000
|
+
"use strict";
|
|
2001
|
+
__filename3 = fileURLToPath3(import.meta.url);
|
|
2002
|
+
__dirname3 = dirname2(__filename3);
|
|
2003
|
+
require3 = createRequire2(import.meta.url);
|
|
2004
|
+
try {
|
|
2005
|
+
packageJson2 = require3(join2(__dirname3, "../package.json"));
|
|
2006
|
+
} catch {
|
|
2007
|
+
try {
|
|
2008
|
+
packageJson2 = require3(join2(__dirname3, "../../package.json"));
|
|
2009
|
+
} catch {
|
|
2010
|
+
console.warn("[Lien] Warning: Could not load package.json, using fallback version");
|
|
2011
|
+
packageJson2 = { version: "0.0.0-unknown" };
|
|
1961
2012
|
}
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
// src/indexer/manifest.ts
|
|
2018
|
+
var manifest_exports = {};
|
|
2019
|
+
__export(manifest_exports, {
|
|
2020
|
+
ManifestManager: () => ManifestManager
|
|
2021
|
+
});
|
|
2022
|
+
import fs11 from "fs/promises";
|
|
2023
|
+
import path12 from "path";
|
|
2024
|
+
var MANIFEST_FILE, ManifestManager;
|
|
2025
|
+
var init_manifest = __esm({
|
|
2026
|
+
"src/indexer/manifest.ts"() {
|
|
2027
|
+
"use strict";
|
|
2028
|
+
init_constants();
|
|
2029
|
+
init_version2();
|
|
2030
|
+
MANIFEST_FILE = "manifest.json";
|
|
2031
|
+
ManifestManager = class {
|
|
2032
|
+
manifestPath;
|
|
2033
|
+
indexPath;
|
|
2034
|
+
/**
|
|
2035
|
+
* Promise-based lock to prevent race conditions during concurrent updates.
|
|
2036
|
+
* Ensures read-modify-write operations are atomic.
|
|
2037
|
+
*/
|
|
2038
|
+
updateLock = Promise.resolve();
|
|
2039
|
+
/**
|
|
2040
|
+
* Creates a new ManifestManager
|
|
2041
|
+
* @param indexPath - Path to the index directory (same as VectorDB path)
|
|
2042
|
+
*/
|
|
2043
|
+
constructor(indexPath) {
|
|
2044
|
+
this.indexPath = indexPath;
|
|
2045
|
+
this.manifestPath = path12.join(indexPath, MANIFEST_FILE);
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Loads the manifest from disk.
|
|
2049
|
+
* Returns null if:
|
|
2050
|
+
* - Manifest doesn't exist (first run)
|
|
2051
|
+
* - Manifest is corrupt
|
|
2052
|
+
* - Format version is incompatible (triggers full reindex)
|
|
2053
|
+
*
|
|
2054
|
+
* @returns Loaded manifest or null
|
|
2055
|
+
*/
|
|
2056
|
+
async load() {
|
|
2057
|
+
try {
|
|
2058
|
+
const content = await fs11.readFile(this.manifestPath, "utf-8");
|
|
2059
|
+
const manifest = JSON.parse(content);
|
|
2060
|
+
if (manifest.formatVersion !== INDEX_FORMAT_VERSION) {
|
|
2061
|
+
console.error(
|
|
2062
|
+
`[Lien] Index format v${manifest.formatVersion} is incompatible with current v${INDEX_FORMAT_VERSION}`
|
|
2063
|
+
);
|
|
2064
|
+
console.error(`[Lien] Full reindex required after Lien upgrade`);
|
|
2065
|
+
await this.clear();
|
|
2066
|
+
return null;
|
|
2067
|
+
}
|
|
2068
|
+
return manifest;
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
if (error.code === "ENOENT") {
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
console.error(`[Lien] Warning: Failed to load manifest: ${error}`);
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Saves the manifest to disk.
|
|
2079
|
+
* Always saves with current format and package versions.
|
|
2080
|
+
*
|
|
2081
|
+
* @param manifest - Manifest to save
|
|
2082
|
+
*/
|
|
2083
|
+
async save(manifest) {
|
|
2084
|
+
try {
|
|
2085
|
+
await fs11.mkdir(this.indexPath, { recursive: true });
|
|
2086
|
+
const manifestToSave = {
|
|
2087
|
+
...manifest,
|
|
2088
|
+
formatVersion: INDEX_FORMAT_VERSION,
|
|
2089
|
+
lienVersion: getPackageVersion(),
|
|
2090
|
+
lastIndexed: Date.now()
|
|
2091
|
+
};
|
|
2092
|
+
const content = JSON.stringify(manifestToSave, null, 2);
|
|
2093
|
+
await fs11.writeFile(this.manifestPath, content, "utf-8");
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
console.error(`[Lien] Warning: Failed to save manifest: ${error}`);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Adds or updates a file entry in the manifest.
|
|
2100
|
+
* Protected by lock to prevent race conditions during concurrent updates.
|
|
2101
|
+
*
|
|
2102
|
+
* @param filepath - Path to the file
|
|
2103
|
+
* @param entry - File entry metadata
|
|
2104
|
+
*/
|
|
2105
|
+
async updateFile(filepath, entry) {
|
|
2106
|
+
this.updateLock = this.updateLock.then(async () => {
|
|
2107
|
+
const manifest = await this.load() || this.createEmpty();
|
|
2108
|
+
manifest.files[filepath] = entry;
|
|
2109
|
+
await this.save(manifest);
|
|
2110
|
+
}).catch((error) => {
|
|
2111
|
+
console.error(`[Lien] Failed to update manifest for ${filepath}: ${error}`);
|
|
2112
|
+
return void 0;
|
|
2113
|
+
});
|
|
2114
|
+
await this.updateLock;
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Removes a file entry from the manifest.
|
|
2118
|
+
* Protected by lock to prevent race conditions during concurrent updates.
|
|
2119
|
+
*
|
|
2120
|
+
* Note: If the manifest doesn't exist, this is a no-op (not an error).
|
|
2121
|
+
* This can happen legitimately after clearing the index or on fresh installs.
|
|
2122
|
+
*
|
|
2123
|
+
* @param filepath - Path to the file to remove
|
|
2124
|
+
*/
|
|
2125
|
+
async removeFile(filepath) {
|
|
2126
|
+
this.updateLock = this.updateLock.then(async () => {
|
|
2127
|
+
const manifest = await this.load();
|
|
2128
|
+
if (!manifest) {
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
delete manifest.files[filepath];
|
|
2132
|
+
await this.save(manifest);
|
|
2133
|
+
}).catch((error) => {
|
|
2134
|
+
console.error(`[Lien] Failed to remove manifest entry for ${filepath}: ${error}`);
|
|
2135
|
+
return void 0;
|
|
2136
|
+
});
|
|
2137
|
+
await this.updateLock;
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Updates multiple files at once (more efficient than individual updates).
|
|
2141
|
+
* Protected by lock to prevent race conditions during concurrent updates.
|
|
2142
|
+
*
|
|
2143
|
+
* @param entries - Array of file entries to update
|
|
2144
|
+
*/
|
|
2145
|
+
async updateFiles(entries) {
|
|
2146
|
+
this.updateLock = this.updateLock.then(async () => {
|
|
2147
|
+
const manifest = await this.load() || this.createEmpty();
|
|
2148
|
+
for (const entry of entries) {
|
|
2149
|
+
manifest.files[entry.filepath] = entry;
|
|
2150
|
+
}
|
|
2151
|
+
await this.save(manifest);
|
|
2152
|
+
}).catch((error) => {
|
|
2153
|
+
console.error(`[Lien] Failed to update manifest for ${entries.length} files: ${error}`);
|
|
2154
|
+
return void 0;
|
|
2155
|
+
});
|
|
2156
|
+
await this.updateLock;
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Updates the git state in the manifest.
|
|
2160
|
+
* Protected by lock to prevent race conditions during concurrent updates.
|
|
2161
|
+
*
|
|
2162
|
+
* @param gitState - Current git state
|
|
2163
|
+
*/
|
|
2164
|
+
async updateGitState(gitState) {
|
|
2165
|
+
this.updateLock = this.updateLock.then(async () => {
|
|
2166
|
+
const manifest = await this.load() || this.createEmpty();
|
|
2167
|
+
manifest.gitState = gitState;
|
|
2168
|
+
await this.save(manifest);
|
|
2169
|
+
}).catch((error) => {
|
|
2170
|
+
console.error(`[Lien] Failed to update git state in manifest: ${error}`);
|
|
2171
|
+
return void 0;
|
|
2172
|
+
});
|
|
2173
|
+
await this.updateLock;
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Gets the list of files currently in the manifest
|
|
2177
|
+
*
|
|
2178
|
+
* @returns Array of filepaths
|
|
2179
|
+
*/
|
|
2180
|
+
async getIndexedFiles() {
|
|
2181
|
+
const manifest = await this.load();
|
|
2182
|
+
if (!manifest) return [];
|
|
2183
|
+
return Object.keys(manifest.files);
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Detects which files have changed based on mtime comparison
|
|
2187
|
+
*
|
|
2188
|
+
* @param currentFiles - Map of current files with their mtimes
|
|
2189
|
+
* @returns Array of filepaths that have changed
|
|
2190
|
+
*/
|
|
2191
|
+
async getChangedFiles(currentFiles) {
|
|
2192
|
+
const manifest = await this.load();
|
|
2193
|
+
if (!manifest) {
|
|
2194
|
+
return Array.from(currentFiles.keys());
|
|
2195
|
+
}
|
|
2196
|
+
const changedFiles = [];
|
|
2197
|
+
for (const [filepath, mtime] of currentFiles) {
|
|
2198
|
+
const entry = manifest.files[filepath];
|
|
2199
|
+
if (!entry) {
|
|
2200
|
+
changedFiles.push(filepath);
|
|
2201
|
+
} else if (entry.lastModified < mtime) {
|
|
2202
|
+
changedFiles.push(filepath);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
return changedFiles;
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Gets files that are in the manifest but not in the current file list
|
|
2209
|
+
* (i.e., deleted files)
|
|
2210
|
+
*
|
|
2211
|
+
* @param currentFiles - Set of current file paths
|
|
2212
|
+
* @returns Array of deleted file paths
|
|
2213
|
+
*/
|
|
2214
|
+
async getDeletedFiles(currentFiles) {
|
|
2215
|
+
const manifest = await this.load();
|
|
2216
|
+
if (!manifest) return [];
|
|
2217
|
+
const deletedFiles = [];
|
|
2218
|
+
for (const filepath of Object.keys(manifest.files)) {
|
|
2219
|
+
if (!currentFiles.has(filepath)) {
|
|
2220
|
+
deletedFiles.push(filepath);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
return deletedFiles;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Clears the manifest file
|
|
2227
|
+
*/
|
|
2228
|
+
async clear() {
|
|
2229
|
+
try {
|
|
2230
|
+
await fs11.unlink(this.manifestPath);
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
if (error.code !== "ENOENT") {
|
|
2233
|
+
console.error(`[Lien] Warning: Failed to clear manifest: ${error}`);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Creates an empty manifest with current version information
|
|
2239
|
+
*
|
|
2240
|
+
* @returns Empty manifest
|
|
2241
|
+
*/
|
|
2242
|
+
createEmpty() {
|
|
2243
|
+
return {
|
|
2244
|
+
formatVersion: INDEX_FORMAT_VERSION,
|
|
2245
|
+
lienVersion: getPackageVersion(),
|
|
2246
|
+
lastIndexed: Date.now(),
|
|
2247
|
+
files: {}
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
// src/git/tracker.ts
|
|
2255
|
+
var tracker_exports = {};
|
|
2256
|
+
__export(tracker_exports, {
|
|
2257
|
+
GitStateTracker: () => GitStateTracker
|
|
2258
|
+
});
|
|
2259
|
+
import fs12 from "fs/promises";
|
|
2260
|
+
import path13 from "path";
|
|
2261
|
+
var GitStateTracker;
|
|
2262
|
+
var init_tracker = __esm({
|
|
2263
|
+
"src/git/tracker.ts"() {
|
|
2264
|
+
"use strict";
|
|
2265
|
+
init_utils();
|
|
2266
|
+
GitStateTracker = class {
|
|
2267
|
+
stateFile;
|
|
2268
|
+
rootDir;
|
|
2269
|
+
currentState = null;
|
|
2270
|
+
constructor(rootDir, indexPath) {
|
|
2271
|
+
this.rootDir = rootDir;
|
|
2272
|
+
this.stateFile = path13.join(indexPath, ".git-state.json");
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Loads the last known git state from disk.
|
|
2276
|
+
* Returns null if no state file exists (first run).
|
|
2277
|
+
*/
|
|
2278
|
+
async loadState() {
|
|
2279
|
+
try {
|
|
2280
|
+
const content = await fs12.readFile(this.stateFile, "utf-8");
|
|
2281
|
+
return JSON.parse(content);
|
|
2282
|
+
} catch {
|
|
2283
|
+
return null;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Saves the current git state to disk.
|
|
2288
|
+
*/
|
|
2289
|
+
async saveState(state) {
|
|
2290
|
+
try {
|
|
2291
|
+
const content = JSON.stringify(state, null, 2);
|
|
2292
|
+
await fs12.writeFile(this.stateFile, content, "utf-8");
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
console.error(`[Lien] Warning: Failed to save git state: ${error}`);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Gets the current git state from the repository.
|
|
2299
|
+
*
|
|
2300
|
+
* @returns Current git state
|
|
2301
|
+
* @throws Error if git commands fail
|
|
2302
|
+
*/
|
|
2303
|
+
async getCurrentGitState() {
|
|
2304
|
+
const branch = await getCurrentBranch(this.rootDir);
|
|
2305
|
+
const commit = await getCurrentCommit(this.rootDir);
|
|
2306
|
+
return {
|
|
2307
|
+
branch,
|
|
2308
|
+
commit,
|
|
2309
|
+
timestamp: Date.now()
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Initializes the tracker by loading saved state and checking current state.
|
|
2314
|
+
* Should be called once when MCP server starts.
|
|
2315
|
+
*
|
|
2316
|
+
* @returns Array of changed files if state changed, null if no changes or first run
|
|
2317
|
+
*/
|
|
2318
|
+
async initialize() {
|
|
2319
|
+
const isRepo = await isGitRepo(this.rootDir);
|
|
2320
|
+
if (!isRepo) {
|
|
2321
|
+
return null;
|
|
2322
|
+
}
|
|
2323
|
+
try {
|
|
2324
|
+
this.currentState = await this.getCurrentGitState();
|
|
2325
|
+
const previousState = await this.loadState();
|
|
2326
|
+
if (!previousState) {
|
|
2327
|
+
await this.saveState(this.currentState);
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
const branchChanged = previousState.branch !== this.currentState.branch;
|
|
2331
|
+
const commitChanged = previousState.commit !== this.currentState.commit;
|
|
2332
|
+
if (!branchChanged && !commitChanged) {
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
let changedFiles = [];
|
|
2336
|
+
if (branchChanged) {
|
|
2337
|
+
try {
|
|
2338
|
+
changedFiles = await getChangedFiles(
|
|
2339
|
+
this.rootDir,
|
|
2340
|
+
previousState.branch,
|
|
2341
|
+
this.currentState.branch
|
|
2342
|
+
);
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
console.error(`[Lien] Branch diff failed, using commit diff: ${error}`);
|
|
2345
|
+
changedFiles = await getChangedFilesBetweenCommits(
|
|
2346
|
+
this.rootDir,
|
|
2347
|
+
previousState.commit,
|
|
2348
|
+
this.currentState.commit
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
} else if (commitChanged) {
|
|
2352
|
+
changedFiles = await getChangedFilesBetweenCommits(
|
|
2353
|
+
this.rootDir,
|
|
2354
|
+
previousState.commit,
|
|
2355
|
+
this.currentState.commit
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
await this.saveState(this.currentState);
|
|
2359
|
+
return changedFiles;
|
|
2360
|
+
} catch (error) {
|
|
2361
|
+
console.error(`[Lien] Failed to initialize git tracker: ${error}`);
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Checks for git state changes since last check.
|
|
2367
|
+
* This is called periodically by the MCP server.
|
|
2368
|
+
*
|
|
2369
|
+
* @returns Array of changed files if state changed, null if no changes
|
|
2370
|
+
*/
|
|
2371
|
+
async detectChanges() {
|
|
2372
|
+
const isRepo = await isGitRepo(this.rootDir);
|
|
2373
|
+
if (!isRepo) {
|
|
2374
|
+
return null;
|
|
2375
|
+
}
|
|
2376
|
+
try {
|
|
2377
|
+
const newState = await this.getCurrentGitState();
|
|
2378
|
+
if (!this.currentState) {
|
|
2379
|
+
this.currentState = newState;
|
|
2380
|
+
await this.saveState(newState);
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
const branchChanged = this.currentState.branch !== newState.branch;
|
|
2384
|
+
const commitChanged = this.currentState.commit !== newState.commit;
|
|
2385
|
+
if (!branchChanged && !commitChanged) {
|
|
2386
|
+
return null;
|
|
2387
|
+
}
|
|
2388
|
+
let changedFiles = [];
|
|
2389
|
+
if (branchChanged) {
|
|
2390
|
+
try {
|
|
2391
|
+
changedFiles = await getChangedFiles(
|
|
2392
|
+
this.rootDir,
|
|
2393
|
+
this.currentState.branch,
|
|
2394
|
+
newState.branch
|
|
2395
|
+
);
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
console.error(`[Lien] Branch diff failed, using commit diff: ${error}`);
|
|
2398
|
+
changedFiles = await getChangedFilesBetweenCommits(
|
|
2399
|
+
this.rootDir,
|
|
2400
|
+
this.currentState.commit,
|
|
2401
|
+
newState.commit
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2404
|
+
} else if (commitChanged) {
|
|
2405
|
+
changedFiles = await getChangedFilesBetweenCommits(
|
|
2406
|
+
this.rootDir,
|
|
2407
|
+
this.currentState.commit,
|
|
2408
|
+
newState.commit
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
this.currentState = newState;
|
|
2412
|
+
await this.saveState(newState);
|
|
2413
|
+
return changedFiles;
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
console.error(`[Lien] Failed to detect git changes: ${error}`);
|
|
2416
|
+
return null;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Gets the current git state.
|
|
2421
|
+
* Useful for status display.
|
|
2422
|
+
*/
|
|
2423
|
+
getState() {
|
|
2424
|
+
return this.currentState;
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Manually updates the saved state.
|
|
2428
|
+
* Useful after manual reindexing to sync state.
|
|
2429
|
+
*/
|
|
2430
|
+
async updateState() {
|
|
2431
|
+
try {
|
|
2432
|
+
this.currentState = await this.getCurrentGitState();
|
|
2433
|
+
await this.saveState(this.currentState);
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
console.error(`[Lien] Failed to update git state: ${error}`);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
// src/indexer/change-detector.ts
|
|
2443
|
+
import fs13 from "fs/promises";
|
|
2444
|
+
async function detectChanges(rootDir, vectorDB, config) {
|
|
2445
|
+
const manifest = new ManifestManager(vectorDB.dbPath);
|
|
2446
|
+
const savedManifest = await manifest.load();
|
|
2447
|
+
if (!savedManifest) {
|
|
2448
|
+
const allFiles = await getAllFiles(rootDir, config);
|
|
2449
|
+
return {
|
|
2450
|
+
added: allFiles,
|
|
2451
|
+
modified: [],
|
|
2452
|
+
deleted: [],
|
|
2453
|
+
reason: "full"
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
const gitAvailable = await isGitAvailable();
|
|
2457
|
+
const isRepo = await isGitRepo(rootDir);
|
|
2458
|
+
if (gitAvailable && isRepo && savedManifest.gitState) {
|
|
2459
|
+
const gitTracker = new GitStateTracker(rootDir, vectorDB.dbPath);
|
|
2460
|
+
await gitTracker.initialize();
|
|
2461
|
+
const currentState = gitTracker.getState();
|
|
2462
|
+
if (currentState && (currentState.branch !== savedManifest.gitState.branch || currentState.commit !== savedManifest.gitState.commit)) {
|
|
2463
|
+
try {
|
|
2464
|
+
const changedFilesPaths = await getChangedFiles(
|
|
2465
|
+
rootDir,
|
|
2466
|
+
savedManifest.gitState.commit,
|
|
2467
|
+
currentState.commit
|
|
2468
|
+
);
|
|
2469
|
+
const changedFilesSet = new Set(changedFilesPaths);
|
|
2470
|
+
const allFiles = await getAllFiles(rootDir, config);
|
|
2471
|
+
const currentFileSet = new Set(allFiles);
|
|
2472
|
+
const added = [];
|
|
2473
|
+
const modified = [];
|
|
2474
|
+
const deleted = [];
|
|
2475
|
+
for (const filepath of changedFilesPaths) {
|
|
2476
|
+
if (currentFileSet.has(filepath)) {
|
|
2477
|
+
if (savedManifest.files[filepath]) {
|
|
2478
|
+
modified.push(filepath);
|
|
2479
|
+
} else {
|
|
2480
|
+
added.push(filepath);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
for (const filepath of allFiles) {
|
|
2485
|
+
if (!savedManifest.files[filepath] && !changedFilesSet.has(filepath)) {
|
|
2486
|
+
added.push(filepath);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
for (const filepath of Object.keys(savedManifest.files)) {
|
|
2490
|
+
if (!currentFileSet.has(filepath)) {
|
|
2491
|
+
deleted.push(filepath);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return {
|
|
2495
|
+
added,
|
|
2496
|
+
modified,
|
|
2497
|
+
deleted,
|
|
2498
|
+
reason: "git-state-changed"
|
|
2499
|
+
};
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
console.warn(`[Lien] Git diff failed, falling back to full reindex: ${error}`);
|
|
2502
|
+
const allFiles = await getAllFiles(rootDir, config);
|
|
2503
|
+
const currentFileSet = new Set(allFiles);
|
|
2504
|
+
const deleted = [];
|
|
2505
|
+
for (const filepath of Object.keys(savedManifest.files)) {
|
|
2506
|
+
if (!currentFileSet.has(filepath)) {
|
|
2507
|
+
deleted.push(filepath);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
added: allFiles,
|
|
2512
|
+
modified: [],
|
|
2513
|
+
deleted,
|
|
2514
|
+
reason: "git-state-changed"
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return await mtimeBasedDetection(rootDir, savedManifest, config);
|
|
2520
|
+
}
|
|
2521
|
+
async function getAllFiles(rootDir, config) {
|
|
2522
|
+
if (isModernConfig(config) && config.frameworks.length > 0) {
|
|
2523
|
+
return await scanCodebaseWithFrameworks(rootDir, config);
|
|
2524
|
+
} else if (isLegacyConfig(config)) {
|
|
2525
|
+
return await scanCodebase({
|
|
2526
|
+
rootDir,
|
|
2527
|
+
includePatterns: config.indexing.include,
|
|
2528
|
+
excludePatterns: config.indexing.exclude
|
|
2529
|
+
});
|
|
2530
|
+
} else {
|
|
2531
|
+
return await scanCodebase({
|
|
2532
|
+
rootDir,
|
|
2533
|
+
includePatterns: [],
|
|
2534
|
+
excludePatterns: []
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
async function mtimeBasedDetection(rootDir, savedManifest, config) {
|
|
2539
|
+
const added = [];
|
|
2540
|
+
const modified = [];
|
|
2541
|
+
const deleted = [];
|
|
2542
|
+
const currentFiles = await getAllFiles(rootDir, config);
|
|
2543
|
+
const currentFileSet = new Set(currentFiles);
|
|
2544
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
2545
|
+
for (const filepath of currentFiles) {
|
|
2546
|
+
try {
|
|
2547
|
+
const stats = await fs13.stat(filepath);
|
|
2548
|
+
fileStats.set(filepath, stats.mtimeMs);
|
|
2549
|
+
} catch {
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
for (const [filepath, mtime] of fileStats) {
|
|
2554
|
+
const entry = savedManifest.files[filepath];
|
|
2555
|
+
if (!entry) {
|
|
2556
|
+
added.push(filepath);
|
|
2557
|
+
} else if (entry.lastModified < mtime) {
|
|
2558
|
+
modified.push(filepath);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
for (const filepath of Object.keys(savedManifest.files)) {
|
|
2562
|
+
if (!currentFileSet.has(filepath)) {
|
|
2563
|
+
deleted.push(filepath);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
added,
|
|
2568
|
+
modified,
|
|
2569
|
+
deleted,
|
|
2570
|
+
reason: "mtime"
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
var init_change_detector = __esm({
|
|
2574
|
+
"src/indexer/change-detector.ts"() {
|
|
2575
|
+
"use strict";
|
|
2576
|
+
init_manifest();
|
|
2577
|
+
init_scanner();
|
|
2578
|
+
init_schema();
|
|
2579
|
+
init_tracker();
|
|
2580
|
+
init_utils();
|
|
2581
|
+
}
|
|
2582
|
+
});
|
|
2583
|
+
|
|
2584
|
+
// src/indexer/incremental.ts
|
|
2585
|
+
import fs14 from "fs/promises";
|
|
2586
|
+
async function processFileContent(filepath, content, embeddings, config, verbose) {
|
|
2587
|
+
const chunkSize = isModernConfig(config) ? config.core.chunkSize : isLegacyConfig(config) ? config.indexing.chunkSize : 75;
|
|
2588
|
+
const chunkOverlap = isModernConfig(config) ? config.core.chunkOverlap : isLegacyConfig(config) ? config.indexing.chunkOverlap : 10;
|
|
2589
|
+
const chunks = chunkFile(filepath, content, {
|
|
2590
|
+
chunkSize,
|
|
2591
|
+
chunkOverlap
|
|
2592
|
+
});
|
|
2593
|
+
if (chunks.length === 0) {
|
|
2594
|
+
if (verbose) {
|
|
2595
|
+
console.error(`[Lien] Empty file: ${filepath}`);
|
|
2596
|
+
}
|
|
2597
|
+
return null;
|
|
2598
|
+
}
|
|
2599
|
+
const texts = chunks.map((c) => c.content);
|
|
2600
|
+
const vectors = [];
|
|
2601
|
+
for (let j = 0; j < texts.length; j += EMBEDDING_MICRO_BATCH_SIZE) {
|
|
2602
|
+
const microBatch = texts.slice(j, Math.min(j + EMBEDDING_MICRO_BATCH_SIZE, texts.length));
|
|
2603
|
+
const microResults = await embeddings.embedBatch(microBatch);
|
|
2604
|
+
vectors.push(...microResults);
|
|
2605
|
+
if (texts.length > EMBEDDING_MICRO_BATCH_SIZE) {
|
|
2606
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
return {
|
|
2610
|
+
chunkCount: chunks.length,
|
|
2611
|
+
vectors,
|
|
2612
|
+
chunks,
|
|
2613
|
+
texts
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
async function indexSingleFile(filepath, vectorDB, embeddings, config, options = {}) {
|
|
2617
|
+
const { verbose } = options;
|
|
2618
|
+
try {
|
|
2619
|
+
try {
|
|
2620
|
+
await fs14.access(filepath);
|
|
2621
|
+
} catch {
|
|
2622
|
+
if (verbose) {
|
|
2623
|
+
console.error(`[Lien] File deleted: ${filepath}`);
|
|
2624
|
+
}
|
|
2625
|
+
await vectorDB.deleteByFile(filepath);
|
|
2626
|
+
const manifest2 = new ManifestManager(vectorDB.dbPath);
|
|
2627
|
+
await manifest2.removeFile(filepath);
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
const content = await fs14.readFile(filepath, "utf-8");
|
|
2631
|
+
const result = await processFileContent(filepath, content, embeddings, config, verbose || false);
|
|
2632
|
+
const stats = await fs14.stat(filepath);
|
|
2633
|
+
const manifest = new ManifestManager(vectorDB.dbPath);
|
|
2634
|
+
if (result === null) {
|
|
2635
|
+
await vectorDB.deleteByFile(filepath);
|
|
2636
|
+
await manifest.updateFile(filepath, {
|
|
2637
|
+
filepath,
|
|
2638
|
+
lastModified: stats.mtimeMs,
|
|
2639
|
+
chunkCount: 0
|
|
2640
|
+
});
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
await vectorDB.updateFile(
|
|
2644
|
+
filepath,
|
|
2645
|
+
result.vectors,
|
|
2646
|
+
result.chunks.map((c) => c.metadata),
|
|
2647
|
+
result.texts
|
|
2648
|
+
);
|
|
2649
|
+
await manifest.updateFile(filepath, {
|
|
2650
|
+
filepath,
|
|
2651
|
+
lastModified: stats.mtimeMs,
|
|
2652
|
+
chunkCount: result.chunkCount
|
|
2653
|
+
});
|
|
2654
|
+
if (verbose) {
|
|
2655
|
+
console.error(`[Lien] \u2713 Updated ${filepath} (${result.chunkCount} chunks)`);
|
|
2656
|
+
}
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
console.error(`[Lien] \u26A0\uFE0F Failed to index ${filepath}: ${error}`);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
async function indexMultipleFiles(filepaths, vectorDB, embeddings, config, options = {}) {
|
|
2662
|
+
const { verbose } = options;
|
|
2663
|
+
let processedCount = 0;
|
|
2664
|
+
const manifestEntries = [];
|
|
2665
|
+
for (const filepath of filepaths) {
|
|
2666
|
+
let content;
|
|
2667
|
+
let fileMtime;
|
|
2668
|
+
try {
|
|
2669
|
+
const stats = await fs14.stat(filepath);
|
|
2670
|
+
fileMtime = stats.mtimeMs;
|
|
2671
|
+
content = await fs14.readFile(filepath, "utf-8");
|
|
2672
|
+
} catch (error) {
|
|
2673
|
+
if (verbose) {
|
|
2674
|
+
console.error(`[Lien] File not readable: ${filepath}`);
|
|
2675
|
+
}
|
|
2676
|
+
try {
|
|
2677
|
+
await vectorDB.deleteByFile(filepath);
|
|
2678
|
+
const manifest = new ManifestManager(vectorDB.dbPath);
|
|
2679
|
+
await manifest.removeFile(filepath);
|
|
2680
|
+
} catch (error2) {
|
|
2681
|
+
if (verbose) {
|
|
2682
|
+
console.error(`[Lien] Note: ${filepath} not in index`);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
processedCount++;
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
try {
|
|
2689
|
+
const result = await processFileContent(filepath, content, embeddings, config, verbose || false);
|
|
2690
|
+
if (result === null) {
|
|
2691
|
+
try {
|
|
2692
|
+
await vectorDB.deleteByFile(filepath);
|
|
2693
|
+
} catch (error) {
|
|
2694
|
+
}
|
|
2695
|
+
const manifest = new ManifestManager(vectorDB.dbPath);
|
|
2696
|
+
await manifest.updateFile(filepath, {
|
|
2697
|
+
filepath,
|
|
2698
|
+
lastModified: fileMtime,
|
|
2699
|
+
chunkCount: 0
|
|
2700
|
+
});
|
|
2701
|
+
processedCount++;
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2704
|
+
try {
|
|
2705
|
+
await vectorDB.deleteByFile(filepath);
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
}
|
|
2708
|
+
await vectorDB.insertBatch(
|
|
2709
|
+
result.vectors,
|
|
2710
|
+
result.chunks.map((c) => c.metadata),
|
|
2711
|
+
result.texts
|
|
2712
|
+
);
|
|
2713
|
+
manifestEntries.push({
|
|
2714
|
+
filepath,
|
|
2715
|
+
chunkCount: result.chunkCount,
|
|
2716
|
+
mtime: fileMtime
|
|
2717
|
+
});
|
|
2718
|
+
if (verbose) {
|
|
2719
|
+
console.error(`[Lien] \u2713 Updated ${filepath} (${result.chunkCount} chunks)`);
|
|
2720
|
+
}
|
|
2721
|
+
processedCount++;
|
|
2722
|
+
} catch (error) {
|
|
2723
|
+
console.error(`[Lien] \u26A0\uFE0F Failed to index ${filepath}: ${error}`);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
if (manifestEntries.length > 0) {
|
|
2727
|
+
const manifest = new ManifestManager(vectorDB.dbPath);
|
|
2728
|
+
await manifest.updateFiles(
|
|
2729
|
+
manifestEntries.map((entry) => ({
|
|
2730
|
+
filepath: entry.filepath,
|
|
2731
|
+
lastModified: entry.mtime,
|
|
2732
|
+
// Use actual file mtime for accurate change detection
|
|
2733
|
+
chunkCount: entry.chunkCount
|
|
2734
|
+
}))
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
return processedCount;
|
|
2738
|
+
}
|
|
2739
|
+
var init_incremental = __esm({
|
|
2740
|
+
"src/indexer/incremental.ts"() {
|
|
2741
|
+
"use strict";
|
|
2742
|
+
init_chunker();
|
|
2743
|
+
init_schema();
|
|
2744
|
+
init_manifest();
|
|
2745
|
+
init_constants();
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
// src/utils/loading-messages.ts
|
|
2750
|
+
function getIndexingMessage() {
|
|
2751
|
+
const message = INDEXING_MESSAGES[currentIndexingIndex % INDEXING_MESSAGES.length];
|
|
2752
|
+
currentIndexingIndex++;
|
|
2753
|
+
return message;
|
|
2754
|
+
}
|
|
2755
|
+
function getEmbeddingMessage() {
|
|
2756
|
+
const message = EMBEDDING_MESSAGES[currentEmbeddingIndex % EMBEDDING_MESSAGES.length];
|
|
2757
|
+
currentEmbeddingIndex++;
|
|
2758
|
+
return message;
|
|
2759
|
+
}
|
|
2760
|
+
function getModelLoadingMessage() {
|
|
2761
|
+
const message = MODEL_LOADING_MESSAGES[currentModelIndex % MODEL_LOADING_MESSAGES.length];
|
|
2762
|
+
currentModelIndex++;
|
|
2763
|
+
return message;
|
|
2764
|
+
}
|
|
2765
|
+
var INDEXING_MESSAGES, EMBEDDING_MESSAGES, MODEL_LOADING_MESSAGES, currentIndexingIndex, currentEmbeddingIndex, currentModelIndex;
|
|
2766
|
+
var init_loading_messages = __esm({
|
|
2767
|
+
"src/utils/loading-messages.ts"() {
|
|
2768
|
+
"use strict";
|
|
2769
|
+
INDEXING_MESSAGES = [
|
|
2770
|
+
"Teaching AI to read your spaghetti code...",
|
|
2771
|
+
"Convincing the LLM that your variable names make sense...",
|
|
2772
|
+
"Indexing your TODO comments (so many TODOs)...",
|
|
2773
|
+
'Building semantic links faster than you can say "grep"...',
|
|
2774
|
+
"Making your codebase searchable (the good, the bad, and the ugly)...",
|
|
2775
|
+
"Chunking code like a boss...",
|
|
2776
|
+
"Feeding your code to the neural network (it's hungry)...",
|
|
2777
|
+
"Creating embeddings (it's like compression, but fancier)...",
|
|
2778
|
+
"Teaching machines to understand your midnight commits...",
|
|
2779
|
+
"Vectorizing your technical debt...",
|
|
2780
|
+
"Indexing... because Ctrl+F wasn't cutting it anymore...",
|
|
2781
|
+
"Making semantic connections (unlike your last refactor)...",
|
|
2782
|
+
"Processing files faster than your CI pipeline...",
|
|
2783
|
+
"Embedding wisdom from your comments (all 3 of them)...",
|
|
2784
|
+
"Analyzing code semantics (yes, even that one function)...",
|
|
2785
|
+
"Building search index (now with 100% more AI)...",
|
|
2786
|
+
"Crunching vectors like it's nobody's business...",
|
|
2787
|
+
"Linking code fragments across the spacetime continuum...",
|
|
2788
|
+
"Teaching transformers about your coding style...",
|
|
2789
|
+
"Preparing for semantic search domination...",
|
|
2790
|
+
"Indexing your genius (and that hacky workaround from 2019)...",
|
|
2791
|
+
"Making your codebase AI-readable (you're welcome, future you)...",
|
|
2792
|
+
"Converting code to math (engineers love this trick)...",
|
|
2793
|
+
"Building the neural net's mental model of your app...",
|
|
2794
|
+
"Chunking files like a lumberjack, but for code..."
|
|
2795
|
+
];
|
|
2796
|
+
EMBEDDING_MESSAGES = [
|
|
2797
|
+
"Generating embeddings (math is happening)...",
|
|
2798
|
+
"Teaching transformers about your forEach loops...",
|
|
2799
|
+
"Converting code to 384-dimensional space (wild, right?)...",
|
|
2800
|
+
"Running the neural network (the Matrix, but for code)...",
|
|
2801
|
+
"Creating semantic vectors (fancy word for AI magic)...",
|
|
2802
|
+
"Embedding your code into hyperspace...",
|
|
2803
|
+
'Teaching the model what "clean code" means in your codebase...',
|
|
2804
|
+
'Generating vectors faster than you can say "AI"...',
|
|
2805
|
+
"Making math from your methods...",
|
|
2806
|
+
"Transforming code into numbers (the AI way)...",
|
|
2807
|
+
"Processing with transformers.js (yes, it runs locally!)...",
|
|
2808
|
+
"Embedding semantics (your code's hidden meaning)...",
|
|
2809
|
+
"Vectorizing variables (alliteration achieved)...",
|
|
2810
|
+
"Teaching AI the difference between foo and bar...",
|
|
2811
|
+
"Creating embeddings (384 dimensions of awesome)..."
|
|
2812
|
+
];
|
|
2813
|
+
MODEL_LOADING_MESSAGES = [
|
|
2814
|
+
"Waking up the neural network...",
|
|
2815
|
+
"Loading transformer model (patience, young padawan)...",
|
|
2816
|
+
"Downloading AI brain (first run only, promise!)...",
|
|
2817
|
+
"Initializing the semantic search engine...",
|
|
2818
|
+
"Booting up the language model (coffee break recommended)...",
|
|
2819
|
+
"Loading 100MB of pure AI goodness...",
|
|
2820
|
+
"Preparing the transformer for action...",
|
|
2821
|
+
"Model loading (this is why we run locally)...",
|
|
2822
|
+
"Spinning up the embedding generator...",
|
|
2823
|
+
"Getting the AI ready for your codebase..."
|
|
2824
|
+
];
|
|
2825
|
+
currentIndexingIndex = 0;
|
|
2826
|
+
currentEmbeddingIndex = 0;
|
|
2827
|
+
currentModelIndex = 0;
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
// src/indexer/index.ts
|
|
2832
|
+
var indexer_exports = {};
|
|
2833
|
+
__export(indexer_exports, {
|
|
2834
|
+
indexCodebase: () => indexCodebase
|
|
2835
|
+
});
|
|
2836
|
+
import fs15 from "fs/promises";
|
|
2837
|
+
import ora from "ora";
|
|
2838
|
+
import chalk4 from "chalk";
|
|
2839
|
+
import pLimit from "p-limit";
|
|
2840
|
+
async function indexCodebase(options = {}) {
|
|
2841
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
2842
|
+
const spinner = ora("Starting indexing process...").start();
|
|
2843
|
+
let updateInterval;
|
|
2844
|
+
try {
|
|
2845
|
+
spinner.text = "Loading configuration...";
|
|
2846
|
+
const config = await configService.load(rootDir);
|
|
2847
|
+
spinner.text = "Initializing vector database...";
|
|
2848
|
+
const vectorDB = new VectorDB(rootDir);
|
|
2849
|
+
await vectorDB.initialize();
|
|
2850
|
+
if (!options.force) {
|
|
2851
|
+
spinner.text = "Checking for changes...";
|
|
2852
|
+
const manifest2 = new ManifestManager(vectorDB.dbPath);
|
|
2853
|
+
const savedManifest = await manifest2.load();
|
|
2854
|
+
if (savedManifest) {
|
|
2855
|
+
const changes = await detectChanges(rootDir, vectorDB, config);
|
|
2856
|
+
if (changes.reason !== "full") {
|
|
2857
|
+
const totalChanges = changes.added.length + changes.modified.length;
|
|
2858
|
+
const totalDeleted = changes.deleted.length;
|
|
2859
|
+
if (totalChanges === 0 && totalDeleted === 0) {
|
|
2860
|
+
spinner.succeed("No changes detected - index is up to date!");
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
spinner.succeed(
|
|
2864
|
+
`Detected changes: ${totalChanges} files to index, ${totalDeleted} to remove (${changes.reason} detection)`
|
|
2865
|
+
);
|
|
2866
|
+
spinner.start(getModelLoadingMessage());
|
|
2867
|
+
const embeddings2 = new LocalEmbeddings();
|
|
2868
|
+
await embeddings2.initialize();
|
|
2869
|
+
spinner.succeed("Embedding model loaded");
|
|
2870
|
+
if (totalDeleted > 0) {
|
|
2871
|
+
spinner.start(`Removing ${totalDeleted} deleted files...`);
|
|
2872
|
+
let removedCount = 0;
|
|
2873
|
+
for (const filepath of changes.deleted) {
|
|
2874
|
+
try {
|
|
2875
|
+
await vectorDB.deleteByFile(filepath);
|
|
2876
|
+
await manifest2.removeFile(filepath);
|
|
2877
|
+
removedCount++;
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
spinner.warn(`Failed to remove file "${filepath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
spinner.succeed(`Removed ${removedCount}/${totalDeleted} deleted files`);
|
|
2883
|
+
}
|
|
2884
|
+
if (totalChanges > 0) {
|
|
2885
|
+
spinner.start(`Reindexing ${totalChanges} changed files...`);
|
|
2886
|
+
const filesToIndex = [...changes.added, ...changes.modified];
|
|
2887
|
+
const count = await indexMultipleFiles(
|
|
2888
|
+
filesToIndex,
|
|
2889
|
+
vectorDB,
|
|
2890
|
+
embeddings2,
|
|
2891
|
+
config,
|
|
2892
|
+
{ verbose: options.verbose }
|
|
2893
|
+
);
|
|
2894
|
+
await writeVersionFile(vectorDB.dbPath);
|
|
2895
|
+
spinner.succeed(
|
|
2896
|
+
`Incremental reindex complete: ${count}/${totalChanges} files indexed successfully`
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
const { isGitAvailable: isGitAvailable3, isGitRepo: isGitRepo3 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
2900
|
+
const { GitStateTracker: GitStateTracker3 } = await Promise.resolve().then(() => (init_tracker(), tracker_exports));
|
|
2901
|
+
const gitAvailable2 = await isGitAvailable3();
|
|
2902
|
+
const isRepo2 = await isGitRepo3(rootDir);
|
|
2903
|
+
if (gitAvailable2 && isRepo2) {
|
|
2904
|
+
const gitTracker = new GitStateTracker3(rootDir, vectorDB.dbPath);
|
|
2905
|
+
await gitTracker.initialize();
|
|
2906
|
+
const gitState = gitTracker.getState();
|
|
2907
|
+
if (gitState) {
|
|
2908
|
+
await manifest2.updateGitState(gitState);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
console.log(chalk4.dim("\nNext step: Run"), chalk4.bold("lien serve"), chalk4.dim("to start the MCP server"));
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
spinner.text = "Full reindex required...";
|
|
2915
|
+
}
|
|
2916
|
+
} else {
|
|
2917
|
+
spinner.text = "Force flag enabled, performing full reindex...";
|
|
2918
|
+
}
|
|
2919
|
+
spinner.text = "Scanning codebase...";
|
|
2920
|
+
let files;
|
|
2921
|
+
if (isModernConfig(config) && config.frameworks.length > 0) {
|
|
2922
|
+
files = await scanCodebaseWithFrameworks(rootDir, config);
|
|
2923
|
+
} else if (isLegacyConfig(config)) {
|
|
2924
|
+
files = await scanCodebase({
|
|
2925
|
+
rootDir,
|
|
2926
|
+
includePatterns: config.indexing.include,
|
|
2927
|
+
excludePatterns: config.indexing.exclude
|
|
2928
|
+
});
|
|
2929
|
+
} else {
|
|
2930
|
+
files = await scanCodebase({
|
|
2931
|
+
rootDir,
|
|
2932
|
+
includePatterns: [],
|
|
2933
|
+
excludePatterns: []
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
if (files.length === 0) {
|
|
2937
|
+
spinner.fail("No files found to index");
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
spinner.text = `Found ${files.length} files`;
|
|
2941
|
+
spinner.text = getModelLoadingMessage();
|
|
2942
|
+
const embeddings = new LocalEmbeddings();
|
|
2943
|
+
await embeddings.initialize();
|
|
2944
|
+
spinner.succeed("Embedding model loaded");
|
|
2945
|
+
const concurrency = isModernConfig(config) ? config.core.concurrency : 4;
|
|
2946
|
+
const embeddingBatchSize = isModernConfig(config) ? config.core.embeddingBatchSize : 50;
|
|
2947
|
+
const vectorDBBatchSize = 100;
|
|
2948
|
+
spinner.start(`Processing files with ${concurrency}x concurrency...`);
|
|
2949
|
+
const startTime = Date.now();
|
|
2950
|
+
let processedFiles = 0;
|
|
2951
|
+
let processedChunks = 0;
|
|
2952
|
+
const chunkAccumulator = [];
|
|
2953
|
+
const limit = pLimit(concurrency);
|
|
2954
|
+
const indexedFileEntries = [];
|
|
2955
|
+
const progressState = {
|
|
2956
|
+
processedFiles: 0,
|
|
2957
|
+
totalFiles: files.length,
|
|
2958
|
+
wittyMessage: getIndexingMessage()
|
|
2959
|
+
};
|
|
2960
|
+
const SPINNER_UPDATE_INTERVAL_MS = 200;
|
|
2961
|
+
const MESSAGE_ROTATION_INTERVAL_MS = 8e3;
|
|
2962
|
+
const MESSAGE_ROTATION_TICKS = Math.floor(MESSAGE_ROTATION_INTERVAL_MS / SPINNER_UPDATE_INTERVAL_MS);
|
|
2963
|
+
let spinnerTick = 0;
|
|
2964
|
+
updateInterval = setInterval(() => {
|
|
2965
|
+
spinnerTick++;
|
|
2966
|
+
if (spinnerTick >= MESSAGE_ROTATION_TICKS) {
|
|
2967
|
+
progressState.wittyMessage = getIndexingMessage();
|
|
2968
|
+
spinnerTick = 0;
|
|
2969
|
+
}
|
|
2970
|
+
spinner.text = `${progressState.processedFiles}/${progressState.totalFiles} files | ${progressState.wittyMessage}`;
|
|
2971
|
+
}, SPINNER_UPDATE_INTERVAL_MS);
|
|
2972
|
+
const processAccumulatedChunks = async () => {
|
|
2973
|
+
if (chunkAccumulator.length === 0) return;
|
|
2974
|
+
const toProcess = chunkAccumulator.splice(0, chunkAccumulator.length);
|
|
2975
|
+
for (let i = 0; i < toProcess.length; i += embeddingBatchSize) {
|
|
2976
|
+
const batch = toProcess.slice(i, Math.min(i + embeddingBatchSize, toProcess.length));
|
|
2977
|
+
progressState.wittyMessage = getEmbeddingMessage();
|
|
2978
|
+
const texts = batch.map((item) => item.content);
|
|
2979
|
+
const embeddingVectors = [];
|
|
2980
|
+
for (let j = 0; j < texts.length; j += EMBEDDING_MICRO_BATCH_SIZE) {
|
|
2981
|
+
const microBatch = texts.slice(j, Math.min(j + EMBEDDING_MICRO_BATCH_SIZE, texts.length));
|
|
2982
|
+
const microResults = await embeddings.embedBatch(microBatch);
|
|
2983
|
+
embeddingVectors.push(...microResults);
|
|
2984
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2985
|
+
}
|
|
2986
|
+
processedChunks += batch.length;
|
|
2987
|
+
progressState.wittyMessage = `Inserting ${batch.length} chunks into vector space...`;
|
|
2988
|
+
await vectorDB.insertBatch(
|
|
2989
|
+
embeddingVectors,
|
|
2990
|
+
batch.map((item) => item.chunk.metadata),
|
|
2991
|
+
texts
|
|
2992
|
+
);
|
|
2993
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2994
|
+
}
|
|
2995
|
+
progressState.wittyMessage = getIndexingMessage();
|
|
2996
|
+
};
|
|
2997
|
+
const filePromises = files.map(
|
|
2998
|
+
(file) => limit(async () => {
|
|
2999
|
+
try {
|
|
3000
|
+
const stats = await fs15.stat(file);
|
|
3001
|
+
const content = await fs15.readFile(file, "utf-8");
|
|
3002
|
+
const chunkSize = isModernConfig(config) ? config.core.chunkSize : 75;
|
|
3003
|
+
const chunkOverlap = isModernConfig(config) ? config.core.chunkOverlap : 10;
|
|
3004
|
+
const chunks = chunkFile(file, content, {
|
|
3005
|
+
chunkSize,
|
|
3006
|
+
chunkOverlap
|
|
3007
|
+
});
|
|
1973
3008
|
if (chunks.length === 0) {
|
|
1974
3009
|
processedFiles++;
|
|
3010
|
+
progressState.processedFiles = processedFiles;
|
|
1975
3011
|
return;
|
|
1976
3012
|
}
|
|
1977
3013
|
for (const chunk of chunks) {
|
|
@@ -1980,25 +3016,53 @@ async function indexCodebase(options = {}) {
|
|
|
1980
3016
|
content: chunk.content
|
|
1981
3017
|
});
|
|
1982
3018
|
}
|
|
1983
|
-
|
|
3019
|
+
indexedFileEntries.push({
|
|
3020
|
+
filepath: file,
|
|
3021
|
+
chunkCount: chunks.length,
|
|
3022
|
+
mtime: stats.mtimeMs
|
|
3023
|
+
});
|
|
3024
|
+
processedFiles++;
|
|
3025
|
+
progressState.processedFiles = processedFiles;
|
|
3026
|
+
if (chunkAccumulator.length >= vectorDBBatchSize) {
|
|
1984
3027
|
await processAccumulatedChunks();
|
|
1985
3028
|
}
|
|
1986
|
-
processedFiles++;
|
|
1987
|
-
const elapsed = (Date.now() - startTime) / 1e3;
|
|
1988
|
-
const rate = processedFiles / elapsed;
|
|
1989
|
-
const eta = rate > 0 ? Math.round((files.length - processedFiles) / rate) : 0;
|
|
1990
|
-
spinner.text = `Indexed ${processedFiles}/${files.length} files (${processedChunks} chunks) | ${concurrency}x concurrency | ETA: ${eta}s`;
|
|
1991
3029
|
} catch (error) {
|
|
1992
3030
|
if (options.verbose) {
|
|
1993
3031
|
console.error(chalk4.yellow(`
|
|
1994
3032
|
\u26A0\uFE0F Skipping ${file}: ${error}`));
|
|
1995
3033
|
}
|
|
1996
3034
|
processedFiles++;
|
|
3035
|
+
progressState.processedFiles = processedFiles;
|
|
1997
3036
|
}
|
|
1998
3037
|
})
|
|
1999
3038
|
);
|
|
2000
3039
|
await Promise.all(filePromises);
|
|
3040
|
+
progressState.wittyMessage = "Processing final chunks...";
|
|
2001
3041
|
await processAccumulatedChunks();
|
|
3042
|
+
clearInterval(updateInterval);
|
|
3043
|
+
spinner.start("Saving index manifest...");
|
|
3044
|
+
const manifest = new ManifestManager(vectorDB.dbPath);
|
|
3045
|
+
await manifest.updateFiles(
|
|
3046
|
+
indexedFileEntries.map((entry) => ({
|
|
3047
|
+
filepath: entry.filepath,
|
|
3048
|
+
lastModified: entry.mtime,
|
|
3049
|
+
// Use actual file mtime for accurate change detection
|
|
3050
|
+
chunkCount: entry.chunkCount
|
|
3051
|
+
}))
|
|
3052
|
+
);
|
|
3053
|
+
const { isGitAvailable: isGitAvailable2, isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
3054
|
+
const { GitStateTracker: GitStateTracker2 } = await Promise.resolve().then(() => (init_tracker(), tracker_exports));
|
|
3055
|
+
const gitAvailable = await isGitAvailable2();
|
|
3056
|
+
const isRepo = await isGitRepo2(rootDir);
|
|
3057
|
+
if (gitAvailable && isRepo) {
|
|
3058
|
+
const gitTracker = new GitStateTracker2(rootDir, vectorDB.dbPath);
|
|
3059
|
+
await gitTracker.initialize();
|
|
3060
|
+
const gitState = gitTracker.getState();
|
|
3061
|
+
if (gitState) {
|
|
3062
|
+
await manifest.updateGitState(gitState);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
spinner.succeed("Manifest saved");
|
|
2002
3066
|
await writeVersionFile(vectorDB.dbPath);
|
|
2003
3067
|
const totalTime = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2004
3068
|
spinner.succeed(
|
|
@@ -2006,6 +3070,9 @@ async function indexCodebase(options = {}) {
|
|
|
2006
3070
|
);
|
|
2007
3071
|
console.log(chalk4.dim("\nNext step: Run"), chalk4.bold("lien serve"), chalk4.dim("to start the MCP server"));
|
|
2008
3072
|
} catch (error) {
|
|
3073
|
+
if (updateInterval) {
|
|
3074
|
+
clearInterval(updateInterval);
|
|
3075
|
+
}
|
|
2009
3076
|
spinner.fail(`Indexing failed: ${error}`);
|
|
2010
3077
|
throw error;
|
|
2011
3078
|
}
|
|
@@ -2020,29 +3087,94 @@ var init_indexer = __esm({
|
|
|
2020
3087
|
init_service();
|
|
2021
3088
|
init_version();
|
|
2022
3089
|
init_schema();
|
|
3090
|
+
init_manifest();
|
|
3091
|
+
init_change_detector();
|
|
3092
|
+
init_incremental();
|
|
3093
|
+
init_loading_messages();
|
|
3094
|
+
init_constants();
|
|
2023
3095
|
}
|
|
2024
3096
|
});
|
|
2025
3097
|
|
|
2026
3098
|
// src/cli/index.ts
|
|
2027
3099
|
import { Command } from "commander";
|
|
2028
|
-
import { createRequire as
|
|
2029
|
-
import { fileURLToPath as
|
|
2030
|
-
import { dirname as
|
|
3100
|
+
import { createRequire as createRequire4 } from "module";
|
|
3101
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3102
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
2031
3103
|
|
|
2032
3104
|
// src/cli/init.ts
|
|
2033
3105
|
init_schema();
|
|
2034
3106
|
init_merge();
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
import fs4 from "fs/promises";
|
|
2038
|
-
import path4 from "path";
|
|
3107
|
+
import fs5 from "fs/promises";
|
|
3108
|
+
import path5 from "path";
|
|
2039
3109
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2040
3110
|
import chalk2 from "chalk";
|
|
2041
3111
|
import inquirer from "inquirer";
|
|
2042
3112
|
|
|
3113
|
+
// src/utils/banner.ts
|
|
3114
|
+
import figlet from "figlet";
|
|
3115
|
+
import chalk from "chalk";
|
|
3116
|
+
import { createRequire } from "module";
|
|
3117
|
+
import { fileURLToPath } from "url";
|
|
3118
|
+
import { dirname, join } from "path";
|
|
3119
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3120
|
+
var __dirname = dirname(__filename);
|
|
3121
|
+
var require2 = createRequire(import.meta.url);
|
|
3122
|
+
var packageJson;
|
|
3123
|
+
try {
|
|
3124
|
+
packageJson = require2(join(__dirname, "../package.json"));
|
|
3125
|
+
} catch {
|
|
3126
|
+
packageJson = require2(join(__dirname, "../../package.json"));
|
|
3127
|
+
}
|
|
3128
|
+
var PACKAGE_NAME = packageJson.name;
|
|
3129
|
+
var VERSION = packageJson.version;
|
|
3130
|
+
function wrapInBox(text, footer, padding = 1) {
|
|
3131
|
+
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
3132
|
+
const maxLength = Math.max(...lines.map((line) => line.length));
|
|
3133
|
+
const horizontalBorder = "\u2500".repeat(maxLength + padding * 2);
|
|
3134
|
+
const top = `\u250C${horizontalBorder}\u2510`;
|
|
3135
|
+
const bottom = `\u2514${horizontalBorder}\u2518`;
|
|
3136
|
+
const separator = `\u251C${horizontalBorder}\u2524`;
|
|
3137
|
+
const paddedLines = lines.map((line) => {
|
|
3138
|
+
const padRight = " ".repeat(maxLength - line.length + padding);
|
|
3139
|
+
const padLeft = " ".repeat(padding);
|
|
3140
|
+
return `\u2502${padLeft}${line}${padRight}\u2502`;
|
|
3141
|
+
});
|
|
3142
|
+
const totalPad = maxLength - footer.length;
|
|
3143
|
+
const leftPad = Math.floor(totalPad / 2);
|
|
3144
|
+
const rightPad = totalPad - leftPad;
|
|
3145
|
+
const centeredFooter = " ".repeat(leftPad) + footer + " ".repeat(rightPad);
|
|
3146
|
+
const paddedFooter = `\u2502${" ".repeat(padding)}${centeredFooter}${" ".repeat(padding)}\u2502`;
|
|
3147
|
+
return [top, ...paddedLines, separator, paddedFooter, bottom].join("\n");
|
|
3148
|
+
}
|
|
3149
|
+
function showBanner() {
|
|
3150
|
+
const banner = figlet.textSync("LIEN", {
|
|
3151
|
+
font: "ANSI Shadow",
|
|
3152
|
+
horizontalLayout: "fitted",
|
|
3153
|
+
verticalLayout: "fitted"
|
|
3154
|
+
});
|
|
3155
|
+
const footer = `${PACKAGE_NAME} - v${VERSION}`;
|
|
3156
|
+
const boxedBanner = wrapInBox(banner.trim(), footer);
|
|
3157
|
+
console.error(chalk.cyan(boxedBanner));
|
|
3158
|
+
console.error();
|
|
3159
|
+
}
|
|
3160
|
+
function showCompactBanner() {
|
|
3161
|
+
const banner = figlet.textSync("LIEN", {
|
|
3162
|
+
font: "ANSI Shadow",
|
|
3163
|
+
horizontalLayout: "fitted",
|
|
3164
|
+
verticalLayout: "fitted"
|
|
3165
|
+
});
|
|
3166
|
+
const footer = `${PACKAGE_NAME} - v${VERSION}`;
|
|
3167
|
+
const boxedBanner = wrapInBox(banner.trim(), footer);
|
|
3168
|
+
console.log(chalk.cyan(boxedBanner));
|
|
3169
|
+
console.log();
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// src/cli/init.ts
|
|
3173
|
+
init_migration();
|
|
3174
|
+
|
|
2043
3175
|
// src/frameworks/detector-service.ts
|
|
2044
|
-
import
|
|
2045
|
-
import
|
|
3176
|
+
import fs4 from "fs/promises";
|
|
3177
|
+
import path4 from "path";
|
|
2046
3178
|
|
|
2047
3179
|
// src/frameworks/types.ts
|
|
2048
3180
|
var defaultDetectionOptions = {
|
|
@@ -2071,24 +3203,17 @@ import path from "path";
|
|
|
2071
3203
|
async function generateNodeJsConfig(_rootDir, _relativePath) {
|
|
2072
3204
|
return {
|
|
2073
3205
|
include: [
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
"
|
|
2077
|
-
"
|
|
2078
|
-
"
|
|
2079
|
-
"
|
|
2080
|
-
"
|
|
2081
|
-
"
|
|
2082
|
-
"
|
|
2083
|
-
"*.js",
|
|
2084
|
-
"*.mjs",
|
|
2085
|
-
"*.cjs",
|
|
3206
|
+
// Broader patterns to catch all common project structures
|
|
3207
|
+
// (frontend/, src/, lib/, app/, components/, etc.)
|
|
3208
|
+
"**/*.ts",
|
|
3209
|
+
"**/*.tsx",
|
|
3210
|
+
"**/*.js",
|
|
3211
|
+
"**/*.jsx",
|
|
3212
|
+
"**/*.vue",
|
|
3213
|
+
"**/*.mjs",
|
|
3214
|
+
"**/*.cjs",
|
|
2086
3215
|
"**/*.md",
|
|
2087
|
-
"**/*.mdx"
|
|
2088
|
-
"docs/**/*.md",
|
|
2089
|
-
"README.md",
|
|
2090
|
-
"CHANGELOG.md",
|
|
2091
|
-
"CONTRIBUTING.md"
|
|
3216
|
+
"**/*.mdx"
|
|
2092
3217
|
],
|
|
2093
3218
|
exclude: [
|
|
2094
3219
|
"node_modules/**",
|
|
@@ -2097,10 +3222,22 @@ async function generateNodeJsConfig(_rootDir, _relativePath) {
|
|
|
2097
3222
|
"coverage/**",
|
|
2098
3223
|
".next/**",
|
|
2099
3224
|
".nuxt/**",
|
|
3225
|
+
".vite/**",
|
|
3226
|
+
".lien/**",
|
|
2100
3227
|
"out/**",
|
|
2101
3228
|
"*.min.js",
|
|
2102
3229
|
"*.min.css",
|
|
2103
|
-
"*.bundle.js"
|
|
3230
|
+
"*.bundle.js",
|
|
3231
|
+
// Test artifacts (source files are indexed, but not output)
|
|
3232
|
+
"playwright-report/**",
|
|
3233
|
+
"test-results/**",
|
|
3234
|
+
// Build/generated artifacts
|
|
3235
|
+
"__generated__/**",
|
|
3236
|
+
// Common build/cache directories
|
|
3237
|
+
".cache/**",
|
|
3238
|
+
".turbo/**",
|
|
3239
|
+
".vercel/**",
|
|
3240
|
+
".netlify/**"
|
|
2104
3241
|
]
|
|
2105
3242
|
};
|
|
2106
3243
|
}
|
|
@@ -2120,17 +3257,17 @@ var nodejsDetector = {
|
|
|
2120
3257
|
evidence: []
|
|
2121
3258
|
};
|
|
2122
3259
|
const packageJsonPath = path.join(fullPath, "package.json");
|
|
2123
|
-
let
|
|
3260
|
+
let packageJson5 = null;
|
|
2124
3261
|
try {
|
|
2125
3262
|
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
2126
|
-
|
|
3263
|
+
packageJson5 = JSON.parse(content);
|
|
2127
3264
|
result.evidence.push("Found package.json");
|
|
2128
3265
|
} catch {
|
|
2129
3266
|
return result;
|
|
2130
3267
|
}
|
|
2131
3268
|
result.detected = true;
|
|
2132
3269
|
result.confidence = "high";
|
|
2133
|
-
if (
|
|
3270
|
+
if (packageJson5.devDependencies?.typescript || packageJson5.dependencies?.typescript) {
|
|
2134
3271
|
result.evidence.push("TypeScript detected");
|
|
2135
3272
|
}
|
|
2136
3273
|
const testFrameworks = [
|
|
@@ -2141,7 +3278,7 @@ var nodejsDetector = {
|
|
|
2141
3278
|
{ name: "@playwright/test", display: "Playwright" }
|
|
2142
3279
|
];
|
|
2143
3280
|
for (const framework of testFrameworks) {
|
|
2144
|
-
if (
|
|
3281
|
+
if (packageJson5.devDependencies?.[framework.name] || packageJson5.dependencies?.[framework.name]) {
|
|
2145
3282
|
result.evidence.push(`${framework.display} test framework detected`);
|
|
2146
3283
|
break;
|
|
2147
3284
|
}
|
|
@@ -2154,13 +3291,13 @@ var nodejsDetector = {
|
|
|
2154
3291
|
{ name: "@nestjs/core", display: "NestJS" }
|
|
2155
3292
|
];
|
|
2156
3293
|
for (const fw of frameworks) {
|
|
2157
|
-
if (
|
|
3294
|
+
if (packageJson5.dependencies?.[fw.name]) {
|
|
2158
3295
|
result.evidence.push(`${fw.display} detected`);
|
|
2159
3296
|
break;
|
|
2160
3297
|
}
|
|
2161
3298
|
}
|
|
2162
|
-
if (
|
|
2163
|
-
result.version =
|
|
3299
|
+
if (packageJson5.engines?.node) {
|
|
3300
|
+
result.version = packageJson5.engines.node;
|
|
2164
3301
|
}
|
|
2165
3302
|
return result;
|
|
2166
3303
|
},
|
|
@@ -2185,12 +3322,12 @@ async function generateLaravelConfig(_rootDir, _relativePath) {
|
|
|
2185
3322
|
"resources/**/*.php",
|
|
2186
3323
|
"tests/**/*.php",
|
|
2187
3324
|
"*.php",
|
|
2188
|
-
// Frontend assets (Vue/React/Inertia)
|
|
2189
|
-
"
|
|
2190
|
-
"
|
|
2191
|
-
"
|
|
2192
|
-
"
|
|
2193
|
-
"
|
|
3325
|
+
// Frontend assets (Vue/React/Inertia) - Broadened for flexibility
|
|
3326
|
+
"**/*.js",
|
|
3327
|
+
"**/*.ts",
|
|
3328
|
+
"**/*.jsx",
|
|
3329
|
+
"**/*.tsx",
|
|
3330
|
+
"**/*.vue",
|
|
2194
3331
|
// Blade templates
|
|
2195
3332
|
"resources/views/**/*.blade.php",
|
|
2196
3333
|
// Documentation
|
|
@@ -2205,7 +3342,19 @@ async function generateLaravelConfig(_rootDir, _relativePath) {
|
|
|
2205
3342
|
"storage/**",
|
|
2206
3343
|
"bootstrap/cache/**",
|
|
2207
3344
|
"public/**",
|
|
2208
|
-
"node_modules/**"
|
|
3345
|
+
"node_modules/**",
|
|
3346
|
+
"dist/**",
|
|
3347
|
+
"build/**",
|
|
3348
|
+
// Test artifacts (source files are indexed, but not output)
|
|
3349
|
+
"playwright-report/**",
|
|
3350
|
+
"test-results/**",
|
|
3351
|
+
"coverage/**",
|
|
3352
|
+
// Build/generated artifacts
|
|
3353
|
+
"__generated__/**",
|
|
3354
|
+
// Frontend build outputs
|
|
3355
|
+
".vite/**",
|
|
3356
|
+
".nuxt/**",
|
|
3357
|
+
".next/**"
|
|
2209
3358
|
]
|
|
2210
3359
|
};
|
|
2211
3360
|
}
|
|
@@ -2276,21 +3425,153 @@ var laravelDetector = {
|
|
|
2276
3425
|
} catch {
|
|
2277
3426
|
}
|
|
2278
3427
|
}
|
|
2279
|
-
if (composerJson.require?.["laravel/framework"]) {
|
|
2280
|
-
result.version = composerJson.require["laravel/framework"];
|
|
3428
|
+
if (composerJson.require?.["laravel/framework"]) {
|
|
3429
|
+
result.version = composerJson.require["laravel/framework"];
|
|
3430
|
+
}
|
|
3431
|
+
result.detected = true;
|
|
3432
|
+
return result;
|
|
3433
|
+
},
|
|
3434
|
+
async generateConfig(rootDir, relativePath) {
|
|
3435
|
+
return generateLaravelConfig(rootDir, relativePath);
|
|
3436
|
+
}
|
|
3437
|
+
};
|
|
3438
|
+
|
|
3439
|
+
// src/frameworks/shopify/detector.ts
|
|
3440
|
+
import fs3 from "fs/promises";
|
|
3441
|
+
import path3 from "path";
|
|
3442
|
+
|
|
3443
|
+
// src/frameworks/shopify/config.ts
|
|
3444
|
+
async function generateShopifyConfig(_rootDir, _relativePath) {
|
|
3445
|
+
return {
|
|
3446
|
+
include: [
|
|
3447
|
+
// Core Liquid templates
|
|
3448
|
+
"layout/**/*.liquid",
|
|
3449
|
+
"sections/**/*.liquid",
|
|
3450
|
+
"snippets/**/*.liquid",
|
|
3451
|
+
"templates/**/*.liquid",
|
|
3452
|
+
// Matches any nesting level (e.g., templates/customers/account.liquid)
|
|
3453
|
+
// Theme editor blocks (Online Store 2.0)
|
|
3454
|
+
"blocks/**/*.liquid",
|
|
3455
|
+
// Assets (CSS, JS with optional Liquid templating)
|
|
3456
|
+
"assets/**/*.js",
|
|
3457
|
+
"assets/**/*.js.liquid",
|
|
3458
|
+
"assets/**/*.css",
|
|
3459
|
+
"assets/**/*.css.liquid",
|
|
3460
|
+
"assets/**/*.scss",
|
|
3461
|
+
"assets/**/*.scss.liquid",
|
|
3462
|
+
// Configuration files
|
|
3463
|
+
"config/*.json",
|
|
3464
|
+
// Locales (i18n)
|
|
3465
|
+
"locales/*.json",
|
|
3466
|
+
// Documentation
|
|
3467
|
+
"*.md",
|
|
3468
|
+
"docs/**/*.md",
|
|
3469
|
+
// Shopify-specific config files
|
|
3470
|
+
"shopify.theme.toml",
|
|
3471
|
+
".shopifyignore"
|
|
3472
|
+
],
|
|
3473
|
+
exclude: [
|
|
3474
|
+
"node_modules/**",
|
|
3475
|
+
"dist/**",
|
|
3476
|
+
"build/**",
|
|
3477
|
+
".git/**",
|
|
3478
|
+
// Playwright/testing artifacts
|
|
3479
|
+
"playwright-report/**",
|
|
3480
|
+
"test-results/**",
|
|
3481
|
+
// Build/generated artifacts
|
|
3482
|
+
"__generated__/**",
|
|
3483
|
+
// Common frontend build outputs
|
|
3484
|
+
".vite/**",
|
|
3485
|
+
".nuxt/**",
|
|
3486
|
+
".next/**"
|
|
3487
|
+
]
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
// src/frameworks/shopify/detector.ts
|
|
3492
|
+
var shopifyDetector = {
|
|
3493
|
+
name: "shopify",
|
|
3494
|
+
priority: 100,
|
|
3495
|
+
// High priority (same as Laravel)
|
|
3496
|
+
async detect(rootDir, relativePath) {
|
|
3497
|
+
const fullPath = path3.join(rootDir, relativePath);
|
|
3498
|
+
const result = {
|
|
3499
|
+
detected: false,
|
|
3500
|
+
name: "shopify",
|
|
3501
|
+
path: relativePath,
|
|
3502
|
+
confidence: "low",
|
|
3503
|
+
evidence: []
|
|
3504
|
+
};
|
|
3505
|
+
const settingsSchemaPath = path3.join(fullPath, "config", "settings_schema.json");
|
|
3506
|
+
let hasSettingsSchema = false;
|
|
3507
|
+
try {
|
|
3508
|
+
await fs3.access(settingsSchemaPath);
|
|
3509
|
+
hasSettingsSchema = true;
|
|
3510
|
+
result.evidence.push("Found config/settings_schema.json");
|
|
3511
|
+
} catch {
|
|
3512
|
+
}
|
|
3513
|
+
const themeLayoutPath = path3.join(fullPath, "layout", "theme.liquid");
|
|
3514
|
+
let hasThemeLayout = false;
|
|
3515
|
+
try {
|
|
3516
|
+
await fs3.access(themeLayoutPath);
|
|
3517
|
+
hasThemeLayout = true;
|
|
3518
|
+
result.evidence.push("Found layout/theme.liquid");
|
|
3519
|
+
} catch {
|
|
3520
|
+
}
|
|
3521
|
+
const shopifyDirs = ["sections", "snippets", "templates", "locales"];
|
|
3522
|
+
let foundDirs = 0;
|
|
3523
|
+
for (const dir of shopifyDirs) {
|
|
3524
|
+
try {
|
|
3525
|
+
const dirPath = path3.join(fullPath, dir);
|
|
3526
|
+
const stats = await fs3.stat(dirPath);
|
|
3527
|
+
if (stats.isDirectory()) {
|
|
3528
|
+
foundDirs++;
|
|
3529
|
+
}
|
|
3530
|
+
} catch {
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
if (foundDirs >= 2) {
|
|
3534
|
+
result.evidence.push(`Shopify directory structure detected (${foundDirs}/${shopifyDirs.length} dirs)`);
|
|
3535
|
+
}
|
|
3536
|
+
try {
|
|
3537
|
+
const tomlPath = path3.join(fullPath, "shopify.theme.toml");
|
|
3538
|
+
await fs3.access(tomlPath);
|
|
3539
|
+
result.evidence.push("Found shopify.theme.toml");
|
|
3540
|
+
} catch {
|
|
3541
|
+
}
|
|
3542
|
+
try {
|
|
3543
|
+
const ignorePath = path3.join(fullPath, ".shopifyignore");
|
|
3544
|
+
await fs3.access(ignorePath);
|
|
3545
|
+
result.evidence.push("Found .shopifyignore");
|
|
3546
|
+
} catch {
|
|
3547
|
+
}
|
|
3548
|
+
if (hasSettingsSchema && foundDirs >= 2) {
|
|
3549
|
+
result.detected = true;
|
|
3550
|
+
result.confidence = "high";
|
|
3551
|
+
return result;
|
|
3552
|
+
}
|
|
3553
|
+
if (hasSettingsSchema || hasThemeLayout && foundDirs >= 1) {
|
|
3554
|
+
result.detected = true;
|
|
3555
|
+
result.confidence = "medium";
|
|
3556
|
+
return result;
|
|
3557
|
+
}
|
|
3558
|
+
if (foundDirs >= 3) {
|
|
3559
|
+
result.detected = true;
|
|
3560
|
+
result.confidence = "medium";
|
|
3561
|
+
return result;
|
|
2281
3562
|
}
|
|
2282
|
-
result.detected = true;
|
|
2283
3563
|
return result;
|
|
2284
3564
|
},
|
|
2285
3565
|
async generateConfig(rootDir, relativePath) {
|
|
2286
|
-
return
|
|
3566
|
+
return generateShopifyConfig(rootDir, relativePath);
|
|
2287
3567
|
}
|
|
2288
3568
|
};
|
|
2289
3569
|
|
|
2290
3570
|
// src/frameworks/registry.ts
|
|
2291
3571
|
var frameworkDetectors = [
|
|
2292
3572
|
nodejsDetector,
|
|
2293
|
-
laravelDetector
|
|
3573
|
+
laravelDetector,
|
|
3574
|
+
shopifyDetector
|
|
2294
3575
|
];
|
|
2295
3576
|
function getFrameworkDetector(name) {
|
|
2296
3577
|
return frameworkDetectors.find((d) => d.name === name);
|
|
@@ -2306,7 +3587,7 @@ async function detectAllFrameworks(rootDir, options = {}) {
|
|
|
2306
3587
|
return results;
|
|
2307
3588
|
}
|
|
2308
3589
|
async function detectAtPath(rootDir, relativePath, results, visited) {
|
|
2309
|
-
const fullPath =
|
|
3590
|
+
const fullPath = path4.join(rootDir, relativePath);
|
|
2310
3591
|
if (visited.has(fullPath)) {
|
|
2311
3592
|
return;
|
|
2312
3593
|
}
|
|
@@ -2326,25 +3607,56 @@ async function detectAtPath(rootDir, relativePath, results, visited) {
|
|
|
2326
3607
|
}
|
|
2327
3608
|
}
|
|
2328
3609
|
if (detectedAtPath.length > 1) {
|
|
2329
|
-
detectedAtPath.
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
3610
|
+
const highConfidence = detectedAtPath.filter((d) => d.confidence === "high");
|
|
3611
|
+
const mediumConfidence = detectedAtPath.filter((d) => d.confidence === "medium");
|
|
3612
|
+
const lowConfidence = detectedAtPath.filter((d) => d.confidence === "low");
|
|
3613
|
+
if (highConfidence.length > 1) {
|
|
3614
|
+
const cleanResults = highConfidence.map(({ priority, ...result }) => result);
|
|
3615
|
+
results.push(...cleanResults);
|
|
3616
|
+
const names = highConfidence.map((d) => d.name).join(" + ");
|
|
3617
|
+
console.log(` \u2192 Detected hybrid project: ${names}`);
|
|
3618
|
+
if (mediumConfidence.length > 0 || lowConfidence.length > 0) {
|
|
3619
|
+
const skippedNames = [...mediumConfidence, ...lowConfidence].map((d) => d.name).join(", ");
|
|
3620
|
+
console.log(` \u2192 Skipping lower confidence detections: ${skippedNames}`);
|
|
3621
|
+
}
|
|
3622
|
+
} else if (highConfidence.length === 1) {
|
|
3623
|
+
const { priority, ...result } = highConfidence[0];
|
|
3624
|
+
results.push(result);
|
|
3625
|
+
if (mediumConfidence.length > 0 || lowConfidence.length > 0) {
|
|
3626
|
+
const skippedNames = [...mediumConfidence, ...lowConfidence].map((d) => d.name).join(", ");
|
|
3627
|
+
console.log(` \u2192 Skipping lower confidence detections: ${skippedNames}`);
|
|
3628
|
+
}
|
|
3629
|
+
} else if (mediumConfidence.length > 0) {
|
|
3630
|
+
mediumConfidence.sort((a, b) => b.priority - a.priority);
|
|
3631
|
+
const { priority, ...winner } = mediumConfidence[0];
|
|
3632
|
+
results.push(winner);
|
|
3633
|
+
const skipped = [...mediumConfidence.slice(1), ...lowConfidence];
|
|
3634
|
+
if (skipped.length > 0) {
|
|
3635
|
+
const skippedNames = skipped.map((d) => d.name).join(", ");
|
|
3636
|
+
console.log(` \u2192 Skipping ${skippedNames} at ${relativePath} (${winner.name} takes precedence)`);
|
|
3637
|
+
}
|
|
3638
|
+
} else if (lowConfidence.length > 0) {
|
|
3639
|
+
lowConfidence.sort((a, b) => b.priority - a.priority);
|
|
3640
|
+
const { priority, ...winner } = lowConfidence[0];
|
|
3641
|
+
results.push(winner);
|
|
3642
|
+
const skipped = lowConfidence.slice(1);
|
|
3643
|
+
if (skipped.length > 0) {
|
|
3644
|
+
const skippedNames = skipped.map((d) => d.name).join(", ");
|
|
3645
|
+
console.log(` \u2192 Skipping ${skippedNames} at ${relativePath} (${winner.name} takes precedence)`);
|
|
3646
|
+
}
|
|
2336
3647
|
}
|
|
2337
3648
|
} else if (detectedAtPath.length === 1) {
|
|
2338
|
-
|
|
3649
|
+
const { priority, ...result } = detectedAtPath[0];
|
|
3650
|
+
results.push(result);
|
|
2339
3651
|
}
|
|
2340
3652
|
}
|
|
2341
3653
|
async function scanSubdirectories(rootDir, relativePath, results, visited, depth, options) {
|
|
2342
3654
|
if (depth >= options.maxDepth) {
|
|
2343
3655
|
return;
|
|
2344
3656
|
}
|
|
2345
|
-
const fullPath =
|
|
3657
|
+
const fullPath = path4.join(rootDir, relativePath);
|
|
2346
3658
|
try {
|
|
2347
|
-
const entries = await
|
|
3659
|
+
const entries = await fs4.readdir(fullPath, { withFileTypes: true });
|
|
2348
3660
|
const dirs = entries.filter((e) => e.isDirectory());
|
|
2349
3661
|
for (const dir of dirs) {
|
|
2350
3662
|
if (options.skipDirs.includes(dir.name)) {
|
|
@@ -2353,7 +3665,7 @@ async function scanSubdirectories(rootDir, relativePath, results, visited, depth
|
|
|
2353
3665
|
if (dir.name.startsWith(".")) {
|
|
2354
3666
|
continue;
|
|
2355
3667
|
}
|
|
2356
|
-
const subPath = relativePath === "." ? dir.name :
|
|
3668
|
+
const subPath = relativePath === "." ? dir.name : path4.join(relativePath, dir.name);
|
|
2357
3669
|
await detectAtPath(rootDir, subPath, results, visited);
|
|
2358
3670
|
await scanSubdirectories(rootDir, subPath, results, visited, depth + 1, options);
|
|
2359
3671
|
}
|
|
@@ -2364,14 +3676,14 @@ async function scanSubdirectories(rootDir, relativePath, results, visited, depth
|
|
|
2364
3676
|
|
|
2365
3677
|
// src/cli/init.ts
|
|
2366
3678
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2367
|
-
var __dirname2 =
|
|
3679
|
+
var __dirname2 = path5.dirname(__filename2);
|
|
2368
3680
|
async function initCommand(options = {}) {
|
|
2369
3681
|
const rootDir = options.path || process.cwd();
|
|
2370
|
-
const configPath =
|
|
3682
|
+
const configPath = path5.join(rootDir, ".lien.config.json");
|
|
2371
3683
|
try {
|
|
2372
3684
|
let configExists = false;
|
|
2373
3685
|
try {
|
|
2374
|
-
await
|
|
3686
|
+
await fs5.access(configPath);
|
|
2375
3687
|
configExists = true;
|
|
2376
3688
|
} catch {
|
|
2377
3689
|
}
|
|
@@ -2507,22 +3819,22 @@ async function createNewConfig(rootDir, options) {
|
|
|
2507
3819
|
]);
|
|
2508
3820
|
if (installCursorRules) {
|
|
2509
3821
|
try {
|
|
2510
|
-
const cursorRulesDir =
|
|
2511
|
-
await
|
|
2512
|
-
const templatePath =
|
|
2513
|
-
const rulesPath =
|
|
3822
|
+
const cursorRulesDir = path5.join(rootDir, ".cursor");
|
|
3823
|
+
await fs5.mkdir(cursorRulesDir, { recursive: true });
|
|
3824
|
+
const templatePath = path5.join(__dirname2, "../CURSOR_RULES_TEMPLATE.md");
|
|
3825
|
+
const rulesPath = path5.join(cursorRulesDir, "rules");
|
|
2514
3826
|
let targetPath;
|
|
2515
3827
|
let isDirectory = false;
|
|
2516
3828
|
let isFile = false;
|
|
2517
3829
|
try {
|
|
2518
|
-
const stats = await
|
|
3830
|
+
const stats = await fs5.stat(rulesPath);
|
|
2519
3831
|
isDirectory = stats.isDirectory();
|
|
2520
3832
|
isFile = stats.isFile();
|
|
2521
3833
|
} catch {
|
|
2522
3834
|
}
|
|
2523
3835
|
if (isDirectory) {
|
|
2524
|
-
targetPath =
|
|
2525
|
-
await
|
|
3836
|
+
targetPath = path5.join(rulesPath, "lien.mdc");
|
|
3837
|
+
await fs5.copyFile(templatePath, targetPath);
|
|
2526
3838
|
console.log(chalk2.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
|
|
2527
3839
|
} else if (isFile) {
|
|
2528
3840
|
const { convertToDir } = await inquirer.prompt([
|
|
@@ -2534,11 +3846,11 @@ async function createNewConfig(rootDir, options) {
|
|
|
2534
3846
|
}
|
|
2535
3847
|
]);
|
|
2536
3848
|
if (convertToDir) {
|
|
2537
|
-
const existingRules = await
|
|
2538
|
-
await
|
|
2539
|
-
await
|
|
2540
|
-
await
|
|
2541
|
-
await
|
|
3849
|
+
const existingRules = await fs5.readFile(rulesPath, "utf-8");
|
|
3850
|
+
await fs5.unlink(rulesPath);
|
|
3851
|
+
await fs5.mkdir(rulesPath);
|
|
3852
|
+
await fs5.writeFile(path5.join(rulesPath, "project.mdc"), existingRules);
|
|
3853
|
+
await fs5.copyFile(templatePath, path5.join(rulesPath, "lien.mdc"));
|
|
2542
3854
|
console.log(chalk2.green("\u2713 Converted .cursor/rules to directory"));
|
|
2543
3855
|
console.log(chalk2.green(" - Your project rules: .cursor/rules/project.mdc"));
|
|
2544
3856
|
console.log(chalk2.green(" - Lien rules: .cursor/rules/lien.mdc"));
|
|
@@ -2546,9 +3858,9 @@ async function createNewConfig(rootDir, options) {
|
|
|
2546
3858
|
console.log(chalk2.dim("Skipped Cursor rules installation (preserving existing file)"));
|
|
2547
3859
|
}
|
|
2548
3860
|
} else {
|
|
2549
|
-
await
|
|
2550
|
-
targetPath =
|
|
2551
|
-
await
|
|
3861
|
+
await fs5.mkdir(rulesPath, { recursive: true });
|
|
3862
|
+
targetPath = path5.join(rulesPath, "lien.mdc");
|
|
3863
|
+
await fs5.copyFile(templatePath, targetPath);
|
|
2552
3864
|
console.log(chalk2.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
|
|
2553
3865
|
}
|
|
2554
3866
|
} catch (error) {
|
|
@@ -2562,8 +3874,8 @@ async function createNewConfig(rootDir, options) {
|
|
|
2562
3874
|
...defaultConfig,
|
|
2563
3875
|
frameworks
|
|
2564
3876
|
};
|
|
2565
|
-
const configPath =
|
|
2566
|
-
await
|
|
3877
|
+
const configPath = path5.join(rootDir, ".lien.config.json");
|
|
3878
|
+
await fs5.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2567
3879
|
console.log(chalk2.green("\n\u2713 Created .lien.config.json"));
|
|
2568
3880
|
console.log(chalk2.green(`\u2713 Configured ${frameworks.length} framework(s)`));
|
|
2569
3881
|
console.log(chalk2.dim("\nNext steps:"));
|
|
@@ -2598,8 +3910,8 @@ Customizing ${frameworkName} settings:`));
|
|
|
2598
3910
|
async function upgradeConfig(configPath) {
|
|
2599
3911
|
try {
|
|
2600
3912
|
const backupPath = `${configPath}.backup`;
|
|
2601
|
-
await
|
|
2602
|
-
const existingContent = await
|
|
3913
|
+
await fs5.copyFile(configPath, backupPath);
|
|
3914
|
+
const existingContent = await fs5.readFile(configPath, "utf-8");
|
|
2603
3915
|
const existingConfig = JSON.parse(existingContent);
|
|
2604
3916
|
let upgradedConfig;
|
|
2605
3917
|
let migrated = false;
|
|
@@ -2615,7 +3927,7 @@ async function upgradeConfig(configPath) {
|
|
|
2615
3927
|
newFields.forEach((field) => console.log(chalk2.dim(" \u2022"), chalk2.bold(field)));
|
|
2616
3928
|
}
|
|
2617
3929
|
}
|
|
2618
|
-
await
|
|
3930
|
+
await fs5.writeFile(
|
|
2619
3931
|
configPath,
|
|
2620
3932
|
JSON.stringify(upgradedConfig, null, 2) + "\n",
|
|
2621
3933
|
"utf-8"
|
|
@@ -2633,99 +3945,19 @@ async function upgradeConfig(configPath) {
|
|
|
2633
3945
|
|
|
2634
3946
|
// src/cli/status.ts
|
|
2635
3947
|
init_service();
|
|
3948
|
+
init_utils();
|
|
3949
|
+
init_version();
|
|
2636
3950
|
import chalk3 from "chalk";
|
|
2637
|
-
import
|
|
2638
|
-
import
|
|
3951
|
+
import fs9 from "fs/promises";
|
|
3952
|
+
import path9 from "path";
|
|
2639
3953
|
import os from "os";
|
|
2640
3954
|
import crypto from "crypto";
|
|
2641
|
-
|
|
2642
|
-
// src/git/utils.ts
|
|
2643
|
-
import { exec } from "child_process";
|
|
2644
|
-
import { promisify } from "util";
|
|
2645
|
-
import fs6 from "fs/promises";
|
|
2646
|
-
import path6 from "path";
|
|
2647
|
-
var execAsync = promisify(exec);
|
|
2648
|
-
async function isGitRepo(rootDir) {
|
|
2649
|
-
try {
|
|
2650
|
-
const gitDir = path6.join(rootDir, ".git");
|
|
2651
|
-
await fs6.access(gitDir);
|
|
2652
|
-
return true;
|
|
2653
|
-
} catch {
|
|
2654
|
-
return false;
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
async function getCurrentBranch(rootDir) {
|
|
2658
|
-
try {
|
|
2659
|
-
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
|
2660
|
-
cwd: rootDir,
|
|
2661
|
-
timeout: 5e3
|
|
2662
|
-
// 5 second timeout
|
|
2663
|
-
});
|
|
2664
|
-
return stdout.trim();
|
|
2665
|
-
} catch (error) {
|
|
2666
|
-
throw new Error(`Failed to get current branch: ${error}`);
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
async function getCurrentCommit(rootDir) {
|
|
2670
|
-
try {
|
|
2671
|
-
const { stdout } = await execAsync("git rev-parse HEAD", {
|
|
2672
|
-
cwd: rootDir,
|
|
2673
|
-
timeout: 5e3
|
|
2674
|
-
});
|
|
2675
|
-
return stdout.trim();
|
|
2676
|
-
} catch (error) {
|
|
2677
|
-
throw new Error(`Failed to get current commit: ${error}`);
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
async function getChangedFiles(rootDir, fromRef, toRef) {
|
|
2681
|
-
try {
|
|
2682
|
-
const { stdout } = await execAsync(
|
|
2683
|
-
`git diff --name-only ${fromRef}...${toRef}`,
|
|
2684
|
-
{
|
|
2685
|
-
cwd: rootDir,
|
|
2686
|
-
timeout: 1e4
|
|
2687
|
-
// 10 second timeout for diffs
|
|
2688
|
-
}
|
|
2689
|
-
);
|
|
2690
|
-
const files = stdout.trim().split("\n").filter(Boolean).map((file) => path6.join(rootDir, file));
|
|
2691
|
-
return files;
|
|
2692
|
-
} catch (error) {
|
|
2693
|
-
throw new Error(`Failed to get changed files: ${error}`);
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
async function getChangedFilesBetweenCommits(rootDir, fromCommit, toCommit) {
|
|
2697
|
-
try {
|
|
2698
|
-
const { stdout } = await execAsync(
|
|
2699
|
-
`git diff --name-only ${fromCommit} ${toCommit}`,
|
|
2700
|
-
{
|
|
2701
|
-
cwd: rootDir,
|
|
2702
|
-
timeout: 1e4
|
|
2703
|
-
}
|
|
2704
|
-
);
|
|
2705
|
-
const files = stdout.trim().split("\n").filter(Boolean).map((file) => path6.join(rootDir, file));
|
|
2706
|
-
return files;
|
|
2707
|
-
} catch (error) {
|
|
2708
|
-
throw new Error(`Failed to get changed files between commits: ${error}`);
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
async function isGitAvailable() {
|
|
2712
|
-
try {
|
|
2713
|
-
await execAsync("git --version", { timeout: 3e3 });
|
|
2714
|
-
return true;
|
|
2715
|
-
} catch {
|
|
2716
|
-
return false;
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
// src/cli/status.ts
|
|
2721
|
-
init_version();
|
|
2722
|
-
init_banner();
|
|
2723
3955
|
init_schema();
|
|
2724
3956
|
async function statusCommand() {
|
|
2725
3957
|
const rootDir = process.cwd();
|
|
2726
|
-
const projectName =
|
|
3958
|
+
const projectName = path9.basename(rootDir);
|
|
2727
3959
|
const pathHash = crypto.createHash("md5").update(rootDir).digest("hex").substring(0, 8);
|
|
2728
|
-
const indexPath =
|
|
3960
|
+
const indexPath = path9.join(os.homedir(), ".lien", "indices", `${projectName}-${pathHash}`);
|
|
2729
3961
|
showCompactBanner();
|
|
2730
3962
|
console.log(chalk3.bold("Status\n"));
|
|
2731
3963
|
const hasConfig = await configService.exists(rootDir);
|
|
@@ -2735,11 +3967,11 @@ async function statusCommand() {
|
|
|
2735
3967
|
return;
|
|
2736
3968
|
}
|
|
2737
3969
|
try {
|
|
2738
|
-
const stats = await
|
|
3970
|
+
const stats = await fs9.stat(indexPath);
|
|
2739
3971
|
console.log(chalk3.dim("Index location:"), indexPath);
|
|
2740
3972
|
console.log(chalk3.dim("Index status:"), chalk3.green("\u2713 Exists"));
|
|
2741
3973
|
try {
|
|
2742
|
-
const files = await
|
|
3974
|
+
const files = await fs9.readdir(indexPath, { recursive: true });
|
|
2743
3975
|
console.log(chalk3.dim("Index files:"), files.length);
|
|
2744
3976
|
} catch (e) {
|
|
2745
3977
|
}
|
|
@@ -2768,9 +4000,9 @@ async function statusCommand() {
|
|
|
2768
4000
|
const commit = await getCurrentCommit(rootDir);
|
|
2769
4001
|
console.log(chalk3.dim(" Current branch:"), branch);
|
|
2770
4002
|
console.log(chalk3.dim(" Current commit:"), commit.substring(0, 8));
|
|
2771
|
-
const gitStateFile =
|
|
4003
|
+
const gitStateFile = path9.join(indexPath, ".git-state.json");
|
|
2772
4004
|
try {
|
|
2773
|
-
const gitStateContent = await
|
|
4005
|
+
const gitStateContent = await fs9.readFile(gitStateFile, "utf-8");
|
|
2774
4006
|
const gitState = JSON.parse(gitStateContent);
|
|
2775
4007
|
if (gitState.branch !== branch || gitState.commit !== commit) {
|
|
2776
4008
|
console.log(chalk3.yellow(" \u26A0\uFE0F Git state changed - will reindex on next serve"));
|
|
@@ -2805,14 +4037,25 @@ async function statusCommand() {
|
|
|
2805
4037
|
|
|
2806
4038
|
// src/cli/index-cmd.ts
|
|
2807
4039
|
init_indexer();
|
|
2808
|
-
init_banner();
|
|
2809
4040
|
import chalk5 from "chalk";
|
|
2810
4041
|
async function indexCommand(options) {
|
|
2811
4042
|
showCompactBanner();
|
|
2812
4043
|
try {
|
|
4044
|
+
if (options.force) {
|
|
4045
|
+
const { VectorDB: VectorDB2 } = await Promise.resolve().then(() => (init_lancedb(), lancedb_exports));
|
|
4046
|
+
const { ManifestManager: ManifestManager2 } = await Promise.resolve().then(() => (init_manifest(), manifest_exports));
|
|
4047
|
+
console.log(chalk5.yellow("Clearing existing index and manifest..."));
|
|
4048
|
+
const vectorDB = new VectorDB2(process.cwd());
|
|
4049
|
+
await vectorDB.initialize();
|
|
4050
|
+
await vectorDB.clear();
|
|
4051
|
+
const manifest = new ManifestManager2(vectorDB.dbPath);
|
|
4052
|
+
await manifest.clear();
|
|
4053
|
+
console.log(chalk5.green("\u2713 Index and manifest cleared\n"));
|
|
4054
|
+
}
|
|
2813
4055
|
await indexCodebase({
|
|
2814
4056
|
rootDir: process.cwd(),
|
|
2815
|
-
verbose: options.verbose || false
|
|
4057
|
+
verbose: options.verbose || false,
|
|
4058
|
+
force: options.force || false
|
|
2816
4059
|
});
|
|
2817
4060
|
if (options.watch) {
|
|
2818
4061
|
console.log(chalk5.yellow("\n\u26A0\uFE0F Watch mode not yet implemented"));
|
|
@@ -2825,6 +4068,8 @@ async function indexCommand(options) {
|
|
|
2825
4068
|
|
|
2826
4069
|
// src/cli/serve.ts
|
|
2827
4070
|
import chalk6 from "chalk";
|
|
4071
|
+
import fs16 from "fs/promises";
|
|
4072
|
+
import path14 from "path";
|
|
2828
4073
|
|
|
2829
4074
|
// src/mcp/server.ts
|
|
2830
4075
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -2833,9 +4078,9 @@ import {
|
|
|
2833
4078
|
CallToolRequestSchema,
|
|
2834
4079
|
ListToolsRequestSchema
|
|
2835
4080
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2836
|
-
import { createRequire as
|
|
2837
|
-
import { fileURLToPath as
|
|
2838
|
-
import { dirname as
|
|
4081
|
+
import { createRequire as createRequire3 } from "module";
|
|
4082
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4083
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2839
4084
|
|
|
2840
4085
|
// src/mcp/tools.ts
|
|
2841
4086
|
var tools = [
|
|
@@ -2918,247 +4163,11 @@ var tools = [
|
|
|
2918
4163
|
// src/mcp/server.ts
|
|
2919
4164
|
init_lancedb();
|
|
2920
4165
|
init_local();
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
import fs11 from "fs/promises";
|
|
2924
|
-
import path11 from "path";
|
|
2925
|
-
var GitStateTracker = class {
|
|
2926
|
-
stateFile;
|
|
2927
|
-
rootDir;
|
|
2928
|
-
currentState = null;
|
|
2929
|
-
constructor(rootDir, indexPath) {
|
|
2930
|
-
this.rootDir = rootDir;
|
|
2931
|
-
this.stateFile = path11.join(indexPath, ".git-state.json");
|
|
2932
|
-
}
|
|
2933
|
-
/**
|
|
2934
|
-
* Loads the last known git state from disk.
|
|
2935
|
-
* Returns null if no state file exists (first run).
|
|
2936
|
-
*/
|
|
2937
|
-
async loadState() {
|
|
2938
|
-
try {
|
|
2939
|
-
const content = await fs11.readFile(this.stateFile, "utf-8");
|
|
2940
|
-
return JSON.parse(content);
|
|
2941
|
-
} catch {
|
|
2942
|
-
return null;
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
/**
|
|
2946
|
-
* Saves the current git state to disk.
|
|
2947
|
-
*/
|
|
2948
|
-
async saveState(state) {
|
|
2949
|
-
try {
|
|
2950
|
-
const content = JSON.stringify(state, null, 2);
|
|
2951
|
-
await fs11.writeFile(this.stateFile, content, "utf-8");
|
|
2952
|
-
} catch (error) {
|
|
2953
|
-
console.error(`[Lien] Warning: Failed to save git state: ${error}`);
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
/**
|
|
2957
|
-
* Gets the current git state from the repository.
|
|
2958
|
-
*
|
|
2959
|
-
* @returns Current git state
|
|
2960
|
-
* @throws Error if git commands fail
|
|
2961
|
-
*/
|
|
2962
|
-
async getCurrentGitState() {
|
|
2963
|
-
const branch = await getCurrentBranch(this.rootDir);
|
|
2964
|
-
const commit = await getCurrentCommit(this.rootDir);
|
|
2965
|
-
return {
|
|
2966
|
-
branch,
|
|
2967
|
-
commit,
|
|
2968
|
-
timestamp: Date.now()
|
|
2969
|
-
};
|
|
2970
|
-
}
|
|
2971
|
-
/**
|
|
2972
|
-
* Initializes the tracker by loading saved state and checking current state.
|
|
2973
|
-
* Should be called once when MCP server starts.
|
|
2974
|
-
*
|
|
2975
|
-
* @returns Array of changed files if state changed, null if no changes or first run
|
|
2976
|
-
*/
|
|
2977
|
-
async initialize() {
|
|
2978
|
-
const isRepo = await isGitRepo(this.rootDir);
|
|
2979
|
-
if (!isRepo) {
|
|
2980
|
-
return null;
|
|
2981
|
-
}
|
|
2982
|
-
try {
|
|
2983
|
-
this.currentState = await this.getCurrentGitState();
|
|
2984
|
-
const previousState = await this.loadState();
|
|
2985
|
-
if (!previousState) {
|
|
2986
|
-
await this.saveState(this.currentState);
|
|
2987
|
-
return null;
|
|
2988
|
-
}
|
|
2989
|
-
const branchChanged = previousState.branch !== this.currentState.branch;
|
|
2990
|
-
const commitChanged = previousState.commit !== this.currentState.commit;
|
|
2991
|
-
if (!branchChanged && !commitChanged) {
|
|
2992
|
-
return null;
|
|
2993
|
-
}
|
|
2994
|
-
let changedFiles = [];
|
|
2995
|
-
if (branchChanged) {
|
|
2996
|
-
try {
|
|
2997
|
-
changedFiles = await getChangedFiles(
|
|
2998
|
-
this.rootDir,
|
|
2999
|
-
previousState.branch,
|
|
3000
|
-
this.currentState.branch
|
|
3001
|
-
);
|
|
3002
|
-
} catch (error) {
|
|
3003
|
-
console.error(`[Lien] Branch diff failed, using commit diff: ${error}`);
|
|
3004
|
-
changedFiles = await getChangedFilesBetweenCommits(
|
|
3005
|
-
this.rootDir,
|
|
3006
|
-
previousState.commit,
|
|
3007
|
-
this.currentState.commit
|
|
3008
|
-
);
|
|
3009
|
-
}
|
|
3010
|
-
} else if (commitChanged) {
|
|
3011
|
-
changedFiles = await getChangedFilesBetweenCommits(
|
|
3012
|
-
this.rootDir,
|
|
3013
|
-
previousState.commit,
|
|
3014
|
-
this.currentState.commit
|
|
3015
|
-
);
|
|
3016
|
-
}
|
|
3017
|
-
await this.saveState(this.currentState);
|
|
3018
|
-
return changedFiles;
|
|
3019
|
-
} catch (error) {
|
|
3020
|
-
console.error(`[Lien] Failed to initialize git tracker: ${error}`);
|
|
3021
|
-
return null;
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
/**
|
|
3025
|
-
* Checks for git state changes since last check.
|
|
3026
|
-
* This is called periodically by the MCP server.
|
|
3027
|
-
*
|
|
3028
|
-
* @returns Array of changed files if state changed, null if no changes
|
|
3029
|
-
*/
|
|
3030
|
-
async detectChanges() {
|
|
3031
|
-
const isRepo = await isGitRepo(this.rootDir);
|
|
3032
|
-
if (!isRepo) {
|
|
3033
|
-
return null;
|
|
3034
|
-
}
|
|
3035
|
-
try {
|
|
3036
|
-
const newState = await this.getCurrentGitState();
|
|
3037
|
-
if (!this.currentState) {
|
|
3038
|
-
this.currentState = newState;
|
|
3039
|
-
await this.saveState(newState);
|
|
3040
|
-
return null;
|
|
3041
|
-
}
|
|
3042
|
-
const branchChanged = this.currentState.branch !== newState.branch;
|
|
3043
|
-
const commitChanged = this.currentState.commit !== newState.commit;
|
|
3044
|
-
if (!branchChanged && !commitChanged) {
|
|
3045
|
-
return null;
|
|
3046
|
-
}
|
|
3047
|
-
let changedFiles = [];
|
|
3048
|
-
if (branchChanged) {
|
|
3049
|
-
try {
|
|
3050
|
-
changedFiles = await getChangedFiles(
|
|
3051
|
-
this.rootDir,
|
|
3052
|
-
this.currentState.branch,
|
|
3053
|
-
newState.branch
|
|
3054
|
-
);
|
|
3055
|
-
} catch (error) {
|
|
3056
|
-
console.error(`[Lien] Branch diff failed, using commit diff: ${error}`);
|
|
3057
|
-
changedFiles = await getChangedFilesBetweenCommits(
|
|
3058
|
-
this.rootDir,
|
|
3059
|
-
this.currentState.commit,
|
|
3060
|
-
newState.commit
|
|
3061
|
-
);
|
|
3062
|
-
}
|
|
3063
|
-
} else if (commitChanged) {
|
|
3064
|
-
changedFiles = await getChangedFilesBetweenCommits(
|
|
3065
|
-
this.rootDir,
|
|
3066
|
-
this.currentState.commit,
|
|
3067
|
-
newState.commit
|
|
3068
|
-
);
|
|
3069
|
-
}
|
|
3070
|
-
this.currentState = newState;
|
|
3071
|
-
await this.saveState(newState);
|
|
3072
|
-
return changedFiles;
|
|
3073
|
-
} catch (error) {
|
|
3074
|
-
console.error(`[Lien] Failed to detect git changes: ${error}`);
|
|
3075
|
-
return null;
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
/**
|
|
3079
|
-
* Gets the current git state.
|
|
3080
|
-
* Useful for status display.
|
|
3081
|
-
*/
|
|
3082
|
-
getState() {
|
|
3083
|
-
return this.currentState;
|
|
3084
|
-
}
|
|
3085
|
-
/**
|
|
3086
|
-
* Manually updates the saved state.
|
|
3087
|
-
* Useful after manual reindexing to sync state.
|
|
3088
|
-
*/
|
|
3089
|
-
async updateState() {
|
|
3090
|
-
try {
|
|
3091
|
-
this.currentState = await this.getCurrentGitState();
|
|
3092
|
-
await this.saveState(this.currentState);
|
|
3093
|
-
} catch (error) {
|
|
3094
|
-
console.error(`[Lien] Failed to update git state: ${error}`);
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
};
|
|
3098
|
-
|
|
3099
|
-
// src/indexer/incremental.ts
|
|
3100
|
-
init_chunker();
|
|
3101
|
-
init_schema();
|
|
3102
|
-
import fs12 from "fs/promises";
|
|
3103
|
-
async function indexSingleFile(filepath, vectorDB, embeddings, config, options = {}) {
|
|
3104
|
-
const { verbose } = options;
|
|
3105
|
-
try {
|
|
3106
|
-
try {
|
|
3107
|
-
await fs12.access(filepath);
|
|
3108
|
-
} catch {
|
|
3109
|
-
if (verbose) {
|
|
3110
|
-
console.error(`[Lien] File deleted: ${filepath}`);
|
|
3111
|
-
}
|
|
3112
|
-
await vectorDB.deleteByFile(filepath);
|
|
3113
|
-
return;
|
|
3114
|
-
}
|
|
3115
|
-
const content = await fs12.readFile(filepath, "utf-8");
|
|
3116
|
-
const chunkSize = isModernConfig(config) ? config.core.chunkSize : isLegacyConfig(config) ? config.indexing.chunkSize : 75;
|
|
3117
|
-
const chunkOverlap = isModernConfig(config) ? config.core.chunkOverlap : isLegacyConfig(config) ? config.indexing.chunkOverlap : 10;
|
|
3118
|
-
const chunks = chunkFile(filepath, content, {
|
|
3119
|
-
chunkSize,
|
|
3120
|
-
chunkOverlap
|
|
3121
|
-
});
|
|
3122
|
-
if (chunks.length === 0) {
|
|
3123
|
-
if (verbose) {
|
|
3124
|
-
console.error(`[Lien] Empty file: ${filepath}`);
|
|
3125
|
-
}
|
|
3126
|
-
await vectorDB.deleteByFile(filepath);
|
|
3127
|
-
return;
|
|
3128
|
-
}
|
|
3129
|
-
const texts = chunks.map((c) => c.content);
|
|
3130
|
-
const vectors = await embeddings.embedBatch(texts);
|
|
3131
|
-
await vectorDB.updateFile(
|
|
3132
|
-
filepath,
|
|
3133
|
-
vectors,
|
|
3134
|
-
chunks.map((c) => c.metadata),
|
|
3135
|
-
texts
|
|
3136
|
-
);
|
|
3137
|
-
if (verbose) {
|
|
3138
|
-
console.error(`[Lien] \u2713 Updated ${filepath} (${chunks.length} chunks)`);
|
|
3139
|
-
}
|
|
3140
|
-
} catch (error) {
|
|
3141
|
-
console.error(`[Lien] \u26A0\uFE0F Failed to index ${filepath}: ${error}`);
|
|
3142
|
-
}
|
|
3143
|
-
}
|
|
3144
|
-
async function indexMultipleFiles(filepaths, vectorDB, embeddings, config, options = {}) {
|
|
3145
|
-
const { verbose } = options;
|
|
3146
|
-
let successCount = 0;
|
|
3147
|
-
for (const filepath of filepaths) {
|
|
3148
|
-
try {
|
|
3149
|
-
await indexSingleFile(filepath, vectorDB, embeddings, config, options);
|
|
3150
|
-
successCount++;
|
|
3151
|
-
} catch (error) {
|
|
3152
|
-
if (verbose) {
|
|
3153
|
-
console.error(`[Lien] Failed to process ${filepath}`);
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
}
|
|
3157
|
-
return successCount;
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
// src/mcp/server.ts
|
|
4166
|
+
init_tracker();
|
|
4167
|
+
init_incremental();
|
|
3161
4168
|
init_service();
|
|
4169
|
+
init_manifest();
|
|
4170
|
+
init_utils();
|
|
3162
4171
|
|
|
3163
4172
|
// src/watcher/index.ts
|
|
3164
4173
|
init_schema();
|
|
@@ -3291,14 +4300,14 @@ var FileWatcher = class {
|
|
|
3291
4300
|
|
|
3292
4301
|
// src/mcp/server.ts
|
|
3293
4302
|
init_constants();
|
|
3294
|
-
var
|
|
3295
|
-
var
|
|
3296
|
-
var
|
|
3297
|
-
var
|
|
4303
|
+
var __filename4 = fileURLToPath4(import.meta.url);
|
|
4304
|
+
var __dirname4 = dirname3(__filename4);
|
|
4305
|
+
var require4 = createRequire3(import.meta.url);
|
|
4306
|
+
var packageJson3;
|
|
3298
4307
|
try {
|
|
3299
|
-
|
|
4308
|
+
packageJson3 = require4(join3(__dirname4, "../package.json"));
|
|
3300
4309
|
} catch {
|
|
3301
|
-
|
|
4310
|
+
packageJson3 = require4(join3(__dirname4, "../../package.json"));
|
|
3302
4311
|
}
|
|
3303
4312
|
async function startMCPServer(options) {
|
|
3304
4313
|
const { rootDir, verbose, watch } = options;
|
|
@@ -3323,7 +4332,7 @@ async function startMCPServer(options) {
|
|
|
3323
4332
|
const server = new Server(
|
|
3324
4333
|
{
|
|
3325
4334
|
name: "lien",
|
|
3326
|
-
version:
|
|
4335
|
+
version: packageJson3.version
|
|
3327
4336
|
},
|
|
3328
4337
|
{
|
|
3329
4338
|
capabilities: {
|
|
@@ -3353,14 +4362,6 @@ async function startMCPServer(options) {
|
|
|
3353
4362
|
const versionCheckInterval = setInterval(async () => {
|
|
3354
4363
|
await checkAndReconnect();
|
|
3355
4364
|
}, VERSION_CHECK_INTERVAL_MS);
|
|
3356
|
-
process.on("SIGINT", () => {
|
|
3357
|
-
clearInterval(versionCheckInterval);
|
|
3358
|
-
process.exit(0);
|
|
3359
|
-
});
|
|
3360
|
-
process.on("SIGTERM", () => {
|
|
3361
|
-
clearInterval(versionCheckInterval);
|
|
3362
|
-
process.exit(0);
|
|
3363
|
-
});
|
|
3364
4365
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3365
4366
|
const { name, arguments: args } = request.params;
|
|
3366
4367
|
try {
|
|
@@ -3587,7 +4588,7 @@ async function startMCPServer(options) {
|
|
|
3587
4588
|
} else {
|
|
3588
4589
|
log("Git detection disabled by configuration");
|
|
3589
4590
|
}
|
|
3590
|
-
const fileWatchingEnabled = watch
|
|
4591
|
+
const fileWatchingEnabled = watch !== void 0 ? watch : config.fileWatching.enabled;
|
|
3591
4592
|
if (fileWatchingEnabled) {
|
|
3592
4593
|
log("\u{1F440} Starting file watcher...");
|
|
3593
4594
|
fileWatcher = new FileWatcher(rootDir, config);
|
|
@@ -3598,6 +4599,8 @@ async function startMCPServer(options) {
|
|
|
3598
4599
|
log(`\u{1F5D1}\uFE0F File deleted: ${filepath}`);
|
|
3599
4600
|
try {
|
|
3600
4601
|
await vectorDB.deleteByFile(filepath);
|
|
4602
|
+
const manifest = new ManifestManager(vectorDB.dbPath);
|
|
4603
|
+
await manifest.removeFile(filepath);
|
|
3601
4604
|
log(`\u2713 Removed ${filepath} from index`);
|
|
3602
4605
|
} catch (error) {
|
|
3603
4606
|
log(`Warning: Failed to remove ${filepath}: ${error}`);
|
|
@@ -3619,6 +4622,7 @@ async function startMCPServer(options) {
|
|
|
3619
4622
|
}
|
|
3620
4623
|
const cleanup = async () => {
|
|
3621
4624
|
log("Shutting down MCP server...");
|
|
4625
|
+
clearInterval(versionCheckInterval);
|
|
3622
4626
|
if (gitPollInterval) {
|
|
3623
4627
|
clearInterval(gitPollInterval);
|
|
3624
4628
|
}
|
|
@@ -3630,21 +4634,55 @@ async function startMCPServer(options) {
|
|
|
3630
4634
|
process.on("SIGINT", cleanup);
|
|
3631
4635
|
process.on("SIGTERM", cleanup);
|
|
3632
4636
|
const transport = new StdioServerTransport();
|
|
4637
|
+
transport.onclose = () => {
|
|
4638
|
+
log("Transport closed, parent process likely terminated");
|
|
4639
|
+
cleanup().catch(() => process.exit(0));
|
|
4640
|
+
};
|
|
4641
|
+
transport.onerror = (error) => {
|
|
4642
|
+
log(`Transport error: ${error}`);
|
|
4643
|
+
};
|
|
3633
4644
|
await server.connect(transport);
|
|
3634
4645
|
log("MCP server started and listening on stdio");
|
|
3635
4646
|
}
|
|
3636
4647
|
|
|
3637
4648
|
// src/cli/serve.ts
|
|
3638
|
-
init_banner();
|
|
3639
4649
|
async function serveCommand(options) {
|
|
3640
|
-
const rootDir = process.cwd();
|
|
4650
|
+
const rootDir = options.root ? path14.resolve(options.root) : process.cwd();
|
|
3641
4651
|
try {
|
|
4652
|
+
if (options.root) {
|
|
4653
|
+
try {
|
|
4654
|
+
const stats = await fs16.stat(rootDir);
|
|
4655
|
+
if (!stats.isDirectory()) {
|
|
4656
|
+
console.error(chalk6.red(`Error: --root path is not a directory: ${rootDir}`));
|
|
4657
|
+
process.exit(1);
|
|
4658
|
+
}
|
|
4659
|
+
} catch (error) {
|
|
4660
|
+
if (error.code === "ENOENT") {
|
|
4661
|
+
console.error(chalk6.red(`Error: --root directory does not exist: ${rootDir}`));
|
|
4662
|
+
} else if (error.code === "EACCES") {
|
|
4663
|
+
console.error(chalk6.red(`Error: --root directory is not accessible: ${rootDir}`));
|
|
4664
|
+
} else {
|
|
4665
|
+
console.error(chalk6.red(`Error: Failed to access --root directory: ${rootDir}`));
|
|
4666
|
+
console.error(chalk6.dim(error.message));
|
|
4667
|
+
}
|
|
4668
|
+
process.exit(1);
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
3642
4671
|
showBanner();
|
|
3643
4672
|
console.error(chalk6.bold("Starting MCP server...\n"));
|
|
4673
|
+
if (options.root) {
|
|
4674
|
+
console.error(chalk6.dim(`Serving from: ${rootDir}
|
|
4675
|
+
`));
|
|
4676
|
+
}
|
|
4677
|
+
if (options.watch) {
|
|
4678
|
+
console.error(chalk6.yellow("\u26A0\uFE0F --watch flag is deprecated (file watching is now default)"));
|
|
4679
|
+
console.error(chalk6.dim(" Use --no-watch to disable file watching\n"));
|
|
4680
|
+
}
|
|
4681
|
+
const watch = options.noWatch ? false : options.watch ? true : void 0;
|
|
3644
4682
|
await startMCPServer({
|
|
3645
4683
|
rootDir,
|
|
3646
4684
|
verbose: true,
|
|
3647
|
-
watch
|
|
4685
|
+
watch
|
|
3648
4686
|
});
|
|
3649
4687
|
} catch (error) {
|
|
3650
4688
|
console.error(chalk6.red("Failed to start MCP server:"), error);
|
|
@@ -3653,42 +4691,21 @@ async function serveCommand(options) {
|
|
|
3653
4691
|
}
|
|
3654
4692
|
|
|
3655
4693
|
// src/cli/index.ts
|
|
3656
|
-
var
|
|
3657
|
-
var
|
|
3658
|
-
var
|
|
3659
|
-
var
|
|
4694
|
+
var __filename5 = fileURLToPath5(import.meta.url);
|
|
4695
|
+
var __dirname5 = dirname4(__filename5);
|
|
4696
|
+
var require5 = createRequire4(import.meta.url);
|
|
4697
|
+
var packageJson4;
|
|
3660
4698
|
try {
|
|
3661
|
-
|
|
4699
|
+
packageJson4 = require5(join4(__dirname5, "../package.json"));
|
|
3662
4700
|
} catch {
|
|
3663
|
-
|
|
4701
|
+
packageJson4 = require5(join4(__dirname5, "../../package.json"));
|
|
3664
4702
|
}
|
|
3665
4703
|
var program = new Command();
|
|
3666
|
-
program.name("lien").description("Local semantic code search for AI assistants via MCP").version(
|
|
4704
|
+
program.name("lien").description("Local semantic code search for AI assistants via MCP").version(packageJson4.version);
|
|
3667
4705
|
program.command("init").description("Initialize Lien in the current directory").option("-u, --upgrade", "Upgrade existing config with new options").option("-y, --yes", "Skip interactive prompts and use defaults").option("-p, --path <path>", "Path to initialize (defaults to current directory)").action(initCommand);
|
|
3668
|
-
program.command("index").description("Index the codebase for semantic search").option("-w, --watch", "Watch for changes and re-index automatically").option("-v, --verbose", "Show detailed logging during indexing").action(indexCommand);
|
|
3669
|
-
program.command("serve").description("Start the MCP server for Cursor integration").option("-p, --port <port>", "Port number (for future use)", "7133").option("-w, --watch", "
|
|
4706
|
+
program.command("index").description("Index the codebase for semantic search").option("-f, --force", "Force full reindex (skip incremental)").option("-w, --watch", "Watch for changes and re-index automatically").option("-v, --verbose", "Show detailed logging during indexing").action(indexCommand);
|
|
4707
|
+
program.command("serve").description("Start the MCP server for Cursor integration").option("-p, --port <port>", "Port number (for future use)", "7133").option("--no-watch", "Disable file watching for this session").option("-w, --watch", "[DEPRECATED] File watching is now enabled by default").option("-r, --root <path>", "Root directory to serve (defaults to current directory)").action(serveCommand);
|
|
3670
4708
|
program.command("status").description("Show indexing status and statistics").action(statusCommand);
|
|
3671
|
-
program.command("reindex").description("Clear index and re-index the entire codebase").option("-v, --verbose", "Show detailed logging during indexing").action(async (options) => {
|
|
3672
|
-
const { showCompactBanner: showCompactBanner2 } = await Promise.resolve().then(() => (init_banner(), banner_exports));
|
|
3673
|
-
const chalk7 = (await import("chalk")).default;
|
|
3674
|
-
const { VectorDB: VectorDB2 } = await Promise.resolve().then(() => (init_lancedb(), lancedb_exports));
|
|
3675
|
-
const { indexCodebase: indexCodebase2 } = await Promise.resolve().then(() => (init_indexer(), indexer_exports));
|
|
3676
|
-
showCompactBanner2();
|
|
3677
|
-
try {
|
|
3678
|
-
console.log(chalk7.yellow("Clearing existing index..."));
|
|
3679
|
-
const vectorDB = new VectorDB2(process.cwd());
|
|
3680
|
-
await vectorDB.initialize();
|
|
3681
|
-
await vectorDB.clear();
|
|
3682
|
-
console.log(chalk7.green("\u2713 Index cleared\n"));
|
|
3683
|
-
await indexCodebase2({
|
|
3684
|
-
rootDir: process.cwd(),
|
|
3685
|
-
verbose: options.verbose || false
|
|
3686
|
-
});
|
|
3687
|
-
} catch (error) {
|
|
3688
|
-
console.error(chalk7.red("Error during re-indexing:"), error);
|
|
3689
|
-
process.exit(1);
|
|
3690
|
-
}
|
|
3691
|
-
});
|
|
3692
4709
|
|
|
3693
4710
|
// src/index.ts
|
|
3694
4711
|
program.parse();
|