@liendev/lien 0.8.1
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/CURSOR_RULES_TEMPLATE.md +283 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3695 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3695 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
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;
|
|
14
|
+
var init_constants = __esm({
|
|
15
|
+
"src/constants.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
DEFAULT_CHUNK_SIZE = 75;
|
|
18
|
+
DEFAULT_CHUNK_OVERLAP = 10;
|
|
19
|
+
DEFAULT_CONCURRENCY = 4;
|
|
20
|
+
DEFAULT_EMBEDDING_BATCH_SIZE = 50;
|
|
21
|
+
EMBEDDING_DIMENSIONS = 384;
|
|
22
|
+
DEFAULT_EMBEDDING_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
23
|
+
DEFAULT_PORT = 7133;
|
|
24
|
+
VERSION_CHECK_INTERVAL_MS = 2e3;
|
|
25
|
+
DEFAULT_GIT_POLL_INTERVAL_MS = 1e4;
|
|
26
|
+
DEFAULT_DEBOUNCE_MS = 1e3;
|
|
27
|
+
CURRENT_CONFIG_VERSION = "0.3.0";
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// src/config/schema.ts
|
|
32
|
+
function isLegacyConfig(config) {
|
|
33
|
+
return "indexing" in config && !("frameworks" in config);
|
|
34
|
+
}
|
|
35
|
+
function isModernConfig(config) {
|
|
36
|
+
return "frameworks" in config;
|
|
37
|
+
}
|
|
38
|
+
var defaultConfig;
|
|
39
|
+
var init_schema = __esm({
|
|
40
|
+
"src/config/schema.ts"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
init_constants();
|
|
43
|
+
defaultConfig = {
|
|
44
|
+
version: CURRENT_CONFIG_VERSION,
|
|
45
|
+
core: {
|
|
46
|
+
chunkSize: DEFAULT_CHUNK_SIZE,
|
|
47
|
+
chunkOverlap: DEFAULT_CHUNK_OVERLAP,
|
|
48
|
+
concurrency: DEFAULT_CONCURRENCY,
|
|
49
|
+
embeddingBatchSize: DEFAULT_EMBEDDING_BATCH_SIZE
|
|
50
|
+
},
|
|
51
|
+
mcp: {
|
|
52
|
+
port: DEFAULT_PORT,
|
|
53
|
+
transport: "stdio",
|
|
54
|
+
autoIndexOnFirstRun: true
|
|
55
|
+
},
|
|
56
|
+
gitDetection: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
pollIntervalMs: DEFAULT_GIT_POLL_INTERVAL_MS
|
|
59
|
+
},
|
|
60
|
+
fileWatching: {
|
|
61
|
+
enabled: false,
|
|
62
|
+
// Opt-in feature
|
|
63
|
+
debounceMs: DEFAULT_DEBOUNCE_MS
|
|
64
|
+
},
|
|
65
|
+
frameworks: []
|
|
66
|
+
// Will be populated by lien init via framework detection
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// src/config/merge.ts
|
|
72
|
+
function deepMergeConfig(defaults, user) {
|
|
73
|
+
return {
|
|
74
|
+
version: user.version ?? defaults.version,
|
|
75
|
+
core: {
|
|
76
|
+
...defaults.core,
|
|
77
|
+
...user.core
|
|
78
|
+
},
|
|
79
|
+
mcp: {
|
|
80
|
+
...defaults.mcp,
|
|
81
|
+
...user.mcp
|
|
82
|
+
},
|
|
83
|
+
gitDetection: {
|
|
84
|
+
...defaults.gitDetection,
|
|
85
|
+
...user.gitDetection
|
|
86
|
+
},
|
|
87
|
+
fileWatching: {
|
|
88
|
+
...defaults.fileWatching,
|
|
89
|
+
...user.fileWatching
|
|
90
|
+
},
|
|
91
|
+
frameworks: user.frameworks ?? defaults.frameworks
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function detectNewFields(before, after) {
|
|
95
|
+
const newFields = [];
|
|
96
|
+
for (const key of Object.keys(after)) {
|
|
97
|
+
if (!(key in before)) {
|
|
98
|
+
newFields.push(key);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (typeof after[key] === "object" && after[key] !== null && !Array.isArray(after[key])) {
|
|
102
|
+
const beforeSection = before[key] || {};
|
|
103
|
+
const afterSection = after[key];
|
|
104
|
+
for (const nestedKey of Object.keys(afterSection)) {
|
|
105
|
+
if (!(nestedKey in beforeSection)) {
|
|
106
|
+
newFields.push(`${key}.${nestedKey}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return newFields;
|
|
112
|
+
}
|
|
113
|
+
var init_merge = __esm({
|
|
114
|
+
"src/config/merge.ts"() {
|
|
115
|
+
"use strict";
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
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
|
+
// src/config/migration.ts
|
|
189
|
+
function needsMigration(config) {
|
|
190
|
+
if (!config) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (config.frameworks !== void 0) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (config.indexing !== void 0) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (config.version && config.version.startsWith("0.2")) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
function migrateConfig(oldConfig) {
|
|
205
|
+
const newConfig = {
|
|
206
|
+
version: "0.3.0",
|
|
207
|
+
core: {
|
|
208
|
+
chunkSize: oldConfig.indexing?.chunkSize ?? defaultConfig.core.chunkSize,
|
|
209
|
+
chunkOverlap: oldConfig.indexing?.chunkOverlap ?? defaultConfig.core.chunkOverlap,
|
|
210
|
+
concurrency: oldConfig.indexing?.concurrency ?? defaultConfig.core.concurrency,
|
|
211
|
+
embeddingBatchSize: oldConfig.indexing?.embeddingBatchSize ?? defaultConfig.core.embeddingBatchSize
|
|
212
|
+
},
|
|
213
|
+
mcp: {
|
|
214
|
+
port: oldConfig.mcp?.port ?? defaultConfig.mcp.port,
|
|
215
|
+
transport: oldConfig.mcp?.transport ?? defaultConfig.mcp.transport,
|
|
216
|
+
autoIndexOnFirstRun: oldConfig.mcp?.autoIndexOnFirstRun ?? defaultConfig.mcp.autoIndexOnFirstRun
|
|
217
|
+
},
|
|
218
|
+
gitDetection: {
|
|
219
|
+
enabled: oldConfig.gitDetection?.enabled ?? defaultConfig.gitDetection.enabled,
|
|
220
|
+
pollIntervalMs: oldConfig.gitDetection?.pollIntervalMs ?? defaultConfig.gitDetection.pollIntervalMs
|
|
221
|
+
},
|
|
222
|
+
fileWatching: {
|
|
223
|
+
enabled: oldConfig.fileWatching?.enabled ?? defaultConfig.fileWatching.enabled,
|
|
224
|
+
debounceMs: oldConfig.fileWatching?.debounceMs ?? defaultConfig.fileWatching.debounceMs
|
|
225
|
+
},
|
|
226
|
+
frameworks: []
|
|
227
|
+
};
|
|
228
|
+
if (oldConfig.indexing) {
|
|
229
|
+
const genericFramework = {
|
|
230
|
+
name: "generic",
|
|
231
|
+
path: ".",
|
|
232
|
+
enabled: true,
|
|
233
|
+
config: {
|
|
234
|
+
include: oldConfig.indexing.include ?? ["**/*.{ts,tsx,js,jsx,py,go,rs,java,c,cpp,cs}"],
|
|
235
|
+
exclude: oldConfig.indexing.exclude ?? [
|
|
236
|
+
"**/node_modules/**",
|
|
237
|
+
"**/dist/**",
|
|
238
|
+
"**/build/**",
|
|
239
|
+
"**/.git/**",
|
|
240
|
+
"**/coverage/**",
|
|
241
|
+
"**/.next/**",
|
|
242
|
+
"**/.nuxt/**",
|
|
243
|
+
"**/vendor/**"
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
newConfig.frameworks.push(genericFramework);
|
|
248
|
+
} else {
|
|
249
|
+
const genericFramework = {
|
|
250
|
+
name: "generic",
|
|
251
|
+
path: ".",
|
|
252
|
+
enabled: true,
|
|
253
|
+
config: {
|
|
254
|
+
include: ["**/*.{ts,tsx,js,jsx,py,go,rs,java,c,cpp,cs}"],
|
|
255
|
+
exclude: [
|
|
256
|
+
"**/node_modules/**",
|
|
257
|
+
"**/dist/**",
|
|
258
|
+
"**/build/**",
|
|
259
|
+
"**/.git/**",
|
|
260
|
+
"**/coverage/**",
|
|
261
|
+
"**/.next/**",
|
|
262
|
+
"**/.nuxt/**",
|
|
263
|
+
"**/vendor/**"
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
newConfig.frameworks.push(genericFramework);
|
|
268
|
+
}
|
|
269
|
+
return newConfig;
|
|
270
|
+
}
|
|
271
|
+
var init_migration = __esm({
|
|
272
|
+
"src/config/migration.ts"() {
|
|
273
|
+
"use strict";
|
|
274
|
+
init_schema();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// src/errors/index.ts
|
|
279
|
+
function wrapError(error, context, additionalContext) {
|
|
280
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
281
|
+
const stack = error instanceof Error ? error.stack : void 0;
|
|
282
|
+
const wrappedError = new LienError(
|
|
283
|
+
`${context}: ${message}`,
|
|
284
|
+
"WRAPPED_ERROR",
|
|
285
|
+
additionalContext
|
|
286
|
+
);
|
|
287
|
+
if (stack) {
|
|
288
|
+
wrappedError.stack = `${wrappedError.stack}
|
|
289
|
+
|
|
290
|
+
Caused by:
|
|
291
|
+
${stack}`;
|
|
292
|
+
}
|
|
293
|
+
return wrappedError;
|
|
294
|
+
}
|
|
295
|
+
var LienError, ConfigError, EmbeddingError, DatabaseError;
|
|
296
|
+
var init_errors = __esm({
|
|
297
|
+
"src/errors/index.ts"() {
|
|
298
|
+
"use strict";
|
|
299
|
+
LienError = class extends Error {
|
|
300
|
+
constructor(message, code, context) {
|
|
301
|
+
super(message);
|
|
302
|
+
this.code = code;
|
|
303
|
+
this.context = context;
|
|
304
|
+
this.name = "LienError";
|
|
305
|
+
if (Error.captureStackTrace) {
|
|
306
|
+
Error.captureStackTrace(this, this.constructor);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
ConfigError = class extends LienError {
|
|
311
|
+
constructor(message, context) {
|
|
312
|
+
super(message, "CONFIG_ERROR", context);
|
|
313
|
+
this.name = "ConfigError";
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
EmbeddingError = class extends LienError {
|
|
317
|
+
constructor(message, context) {
|
|
318
|
+
super(message, "EMBEDDING_ERROR", context);
|
|
319
|
+
this.name = "EmbeddingError";
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
DatabaseError = class extends LienError {
|
|
323
|
+
constructor(message, context) {
|
|
324
|
+
super(message, "DATABASE_ERROR", context);
|
|
325
|
+
this.name = "DatabaseError";
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// src/config/service.ts
|
|
332
|
+
import fs5 from "fs/promises";
|
|
333
|
+
import path5 from "path";
|
|
334
|
+
var ConfigService, configService;
|
|
335
|
+
var init_service = __esm({
|
|
336
|
+
"src/config/service.ts"() {
|
|
337
|
+
"use strict";
|
|
338
|
+
init_schema();
|
|
339
|
+
init_merge();
|
|
340
|
+
init_migration();
|
|
341
|
+
init_errors();
|
|
342
|
+
ConfigService = class _ConfigService {
|
|
343
|
+
static CONFIG_FILENAME = ".lien.config.json";
|
|
344
|
+
/**
|
|
345
|
+
* Load configuration from the specified directory.
|
|
346
|
+
* Automatically handles migration if needed.
|
|
347
|
+
*
|
|
348
|
+
* @param rootDir - Root directory containing the config file
|
|
349
|
+
* @returns Loaded and validated configuration
|
|
350
|
+
* @throws {ConfigError} If config is invalid or cannot be loaded
|
|
351
|
+
*/
|
|
352
|
+
async load(rootDir = process.cwd()) {
|
|
353
|
+
const configPath = this.getConfigPath(rootDir);
|
|
354
|
+
try {
|
|
355
|
+
const configContent = await fs5.readFile(configPath, "utf-8");
|
|
356
|
+
const userConfig = JSON.parse(configContent);
|
|
357
|
+
if (this.needsMigration(userConfig)) {
|
|
358
|
+
console.log("\u{1F504} Migrating config from v0.2.0 to v0.3.0...");
|
|
359
|
+
const result = await this.migrate(rootDir);
|
|
360
|
+
if (result.migrated && result.backupPath) {
|
|
361
|
+
const backupFilename = path5.basename(result.backupPath);
|
|
362
|
+
console.log(`\u2705 Migration complete! Backup saved as ${backupFilename}`);
|
|
363
|
+
console.log("\u{1F4DD} Your config now uses the framework-based structure.");
|
|
364
|
+
}
|
|
365
|
+
return result.config;
|
|
366
|
+
}
|
|
367
|
+
const mergedConfig = deepMergeConfig(defaultConfig, userConfig);
|
|
368
|
+
const validation = this.validate(mergedConfig);
|
|
369
|
+
if (!validation.valid) {
|
|
370
|
+
throw new ConfigError(
|
|
371
|
+
`Invalid configuration:
|
|
372
|
+
${validation.errors.join("\n")}`,
|
|
373
|
+
{ errors: validation.errors, warnings: validation.warnings }
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (validation.warnings.length > 0) {
|
|
377
|
+
console.warn("\u26A0\uFE0F Configuration warnings:");
|
|
378
|
+
validation.warnings.forEach((warning) => console.warn(` ${warning}`));
|
|
379
|
+
}
|
|
380
|
+
return mergedConfig;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (error.code === "ENOENT") {
|
|
383
|
+
return defaultConfig;
|
|
384
|
+
}
|
|
385
|
+
if (error instanceof ConfigError) {
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
if (error instanceof SyntaxError) {
|
|
389
|
+
throw new ConfigError(
|
|
390
|
+
"Failed to parse config file: Invalid JSON syntax",
|
|
391
|
+
{ path: configPath, originalError: error.message }
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
throw wrapError(error, "Failed to load configuration", { path: configPath });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Save configuration to the specified directory.
|
|
399
|
+
* Validates the config before saving.
|
|
400
|
+
*
|
|
401
|
+
* @param rootDir - Root directory to save the config file
|
|
402
|
+
* @param config - Configuration to save
|
|
403
|
+
* @throws {ConfigError} If config is invalid or cannot be saved
|
|
404
|
+
*/
|
|
405
|
+
async save(rootDir, config) {
|
|
406
|
+
const configPath = this.getConfigPath(rootDir);
|
|
407
|
+
const validation = this.validate(config);
|
|
408
|
+
if (!validation.valid) {
|
|
409
|
+
throw new ConfigError(
|
|
410
|
+
`Cannot save invalid configuration:
|
|
411
|
+
${validation.errors.join("\n")}`,
|
|
412
|
+
{ errors: validation.errors }
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
const configJson = JSON.stringify(config, null, 2) + "\n";
|
|
417
|
+
await fs5.writeFile(configPath, configJson, "utf-8");
|
|
418
|
+
} catch (error) {
|
|
419
|
+
throw wrapError(error, "Failed to save configuration", { path: configPath });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Check if a configuration file exists in the specified directory.
|
|
424
|
+
*
|
|
425
|
+
* @param rootDir - Root directory to check
|
|
426
|
+
* @returns True if config file exists
|
|
427
|
+
*/
|
|
428
|
+
async exists(rootDir = process.cwd()) {
|
|
429
|
+
const configPath = this.getConfigPath(rootDir);
|
|
430
|
+
try {
|
|
431
|
+
await fs5.access(configPath);
|
|
432
|
+
return true;
|
|
433
|
+
} catch {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Migrate configuration from v0.2.0 to v0.3.0 format.
|
|
439
|
+
* Creates a backup of the original config file.
|
|
440
|
+
*
|
|
441
|
+
* @param rootDir - Root directory containing the config file
|
|
442
|
+
* @returns Migration result with status and new config
|
|
443
|
+
* @throws {ConfigError} If migration fails
|
|
444
|
+
*/
|
|
445
|
+
async migrate(rootDir = process.cwd()) {
|
|
446
|
+
const configPath = this.getConfigPath(rootDir);
|
|
447
|
+
try {
|
|
448
|
+
const configContent = await fs5.readFile(configPath, "utf-8");
|
|
449
|
+
const oldConfig = JSON.parse(configContent);
|
|
450
|
+
if (!this.needsMigration(oldConfig)) {
|
|
451
|
+
return {
|
|
452
|
+
migrated: false,
|
|
453
|
+
config: oldConfig
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const newConfig = migrateConfig(oldConfig);
|
|
457
|
+
const validation = this.validate(newConfig);
|
|
458
|
+
if (!validation.valid) {
|
|
459
|
+
throw new ConfigError(
|
|
460
|
+
`Migration produced invalid configuration:
|
|
461
|
+
${validation.errors.join("\n")}`,
|
|
462
|
+
{ errors: validation.errors }
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const backupPath = `${configPath}.v0.2.0.backup`;
|
|
466
|
+
await fs5.copyFile(configPath, backupPath);
|
|
467
|
+
await this.save(rootDir, newConfig);
|
|
468
|
+
return {
|
|
469
|
+
migrated: true,
|
|
470
|
+
backupPath,
|
|
471
|
+
config: newConfig
|
|
472
|
+
};
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (error.code === "ENOENT") {
|
|
475
|
+
return {
|
|
476
|
+
migrated: false,
|
|
477
|
+
config: defaultConfig
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (error instanceof ConfigError) {
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
throw wrapError(error, "Configuration migration failed", { path: configPath });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Check if a config object needs migration from v0.2.0 to v0.3.0.
|
|
488
|
+
*
|
|
489
|
+
* @param config - Config object to check
|
|
490
|
+
* @returns True if migration is needed
|
|
491
|
+
*/
|
|
492
|
+
needsMigration(config) {
|
|
493
|
+
return needsMigration(config);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Validate a configuration object.
|
|
497
|
+
* Checks all constraints and returns detailed validation results.
|
|
498
|
+
*
|
|
499
|
+
* @param config - Configuration to validate
|
|
500
|
+
* @returns Validation result with errors and warnings
|
|
501
|
+
*/
|
|
502
|
+
validate(config) {
|
|
503
|
+
const errors = [];
|
|
504
|
+
const warnings = [];
|
|
505
|
+
if (!config || typeof config !== "object") {
|
|
506
|
+
return {
|
|
507
|
+
valid: false,
|
|
508
|
+
errors: ["Configuration must be an object"],
|
|
509
|
+
warnings: []
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const cfg = config;
|
|
513
|
+
if (!cfg.version) {
|
|
514
|
+
errors.push("Missing required field: version");
|
|
515
|
+
}
|
|
516
|
+
if (isModernConfig(cfg)) {
|
|
517
|
+
this.validateModernConfig(cfg, errors, warnings);
|
|
518
|
+
} else if (isLegacyConfig(cfg)) {
|
|
519
|
+
this.validateLegacyConfig(cfg, errors, warnings);
|
|
520
|
+
} else {
|
|
521
|
+
errors.push('Configuration format not recognized. Must have either "frameworks" or "indexing" field');
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
valid: errors.length === 0,
|
|
525
|
+
errors,
|
|
526
|
+
warnings
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Validate a partial configuration object.
|
|
531
|
+
* Useful for validating user input before merging with defaults.
|
|
532
|
+
*
|
|
533
|
+
* @param config - Partial configuration to validate
|
|
534
|
+
* @returns Validation result with errors and warnings
|
|
535
|
+
*/
|
|
536
|
+
validatePartial(config) {
|
|
537
|
+
const errors = [];
|
|
538
|
+
const warnings = [];
|
|
539
|
+
if (config.core) {
|
|
540
|
+
this.validateCoreConfig(config.core, errors, warnings);
|
|
541
|
+
}
|
|
542
|
+
if (config.mcp) {
|
|
543
|
+
this.validateMCPConfig(config.mcp, errors, warnings);
|
|
544
|
+
}
|
|
545
|
+
if (config.gitDetection) {
|
|
546
|
+
this.validateGitDetectionConfig(config.gitDetection, errors, warnings);
|
|
547
|
+
}
|
|
548
|
+
if (config.fileWatching) {
|
|
549
|
+
this.validateFileWatchingConfig(config.fileWatching, errors, warnings);
|
|
550
|
+
}
|
|
551
|
+
if (config.frameworks) {
|
|
552
|
+
this.validateFrameworks(config.frameworks, errors, warnings);
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
valid: errors.length === 0,
|
|
556
|
+
errors,
|
|
557
|
+
warnings
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Get the full path to the config file
|
|
562
|
+
*/
|
|
563
|
+
getConfigPath(rootDir) {
|
|
564
|
+
return path5.join(rootDir, _ConfigService.CONFIG_FILENAME);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Validate modern (v0.3.0+) configuration
|
|
568
|
+
*/
|
|
569
|
+
validateModernConfig(config, errors, warnings) {
|
|
570
|
+
if (!config.core) {
|
|
571
|
+
errors.push("Missing required field: core");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
this.validateCoreConfig(config.core, errors, warnings);
|
|
575
|
+
if (!config.mcp) {
|
|
576
|
+
errors.push("Missing required field: mcp");
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
this.validateMCPConfig(config.mcp, errors, warnings);
|
|
580
|
+
if (!config.gitDetection) {
|
|
581
|
+
errors.push("Missing required field: gitDetection");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
this.validateGitDetectionConfig(config.gitDetection, errors, warnings);
|
|
585
|
+
if (!config.fileWatching) {
|
|
586
|
+
errors.push("Missing required field: fileWatching");
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this.validateFileWatchingConfig(config.fileWatching, errors, warnings);
|
|
590
|
+
if (!config.frameworks) {
|
|
591
|
+
errors.push("Missing required field: frameworks");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
this.validateFrameworks(config.frameworks, errors, warnings);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Validate legacy (v0.2.0) configuration
|
|
598
|
+
*/
|
|
599
|
+
validateLegacyConfig(config, errors, warnings) {
|
|
600
|
+
warnings.push('Using legacy configuration format. Consider running "lien init" to migrate to v0.3.0');
|
|
601
|
+
if (!config.indexing) {
|
|
602
|
+
errors.push("Missing required field: indexing");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const { indexing } = config;
|
|
606
|
+
if (typeof indexing.chunkSize !== "number" || indexing.chunkSize <= 0) {
|
|
607
|
+
errors.push("indexing.chunkSize must be a positive number");
|
|
608
|
+
}
|
|
609
|
+
if (typeof indexing.chunkOverlap !== "number" || indexing.chunkOverlap < 0) {
|
|
610
|
+
errors.push("indexing.chunkOverlap must be a non-negative number");
|
|
611
|
+
}
|
|
612
|
+
if (typeof indexing.concurrency !== "number" || indexing.concurrency < 1 || indexing.concurrency > 16) {
|
|
613
|
+
errors.push("indexing.concurrency must be between 1 and 16");
|
|
614
|
+
}
|
|
615
|
+
if (typeof indexing.embeddingBatchSize !== "number" || indexing.embeddingBatchSize <= 0) {
|
|
616
|
+
errors.push("indexing.embeddingBatchSize must be a positive number");
|
|
617
|
+
}
|
|
618
|
+
if (config.mcp) {
|
|
619
|
+
this.validateMCPConfig(config.mcp, errors, warnings);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Validate core configuration settings
|
|
624
|
+
*/
|
|
625
|
+
validateCoreConfig(core, errors, warnings) {
|
|
626
|
+
if (core.chunkSize !== void 0) {
|
|
627
|
+
if (typeof core.chunkSize !== "number" || core.chunkSize <= 0) {
|
|
628
|
+
errors.push("core.chunkSize must be a positive number");
|
|
629
|
+
} else if (core.chunkSize < 50) {
|
|
630
|
+
warnings.push("core.chunkSize is very small (<50 lines). This may result in poor search quality");
|
|
631
|
+
} else if (core.chunkSize > 500) {
|
|
632
|
+
warnings.push("core.chunkSize is very large (>500 lines). This may impact performance");
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (core.chunkOverlap !== void 0) {
|
|
636
|
+
if (typeof core.chunkOverlap !== "number" || core.chunkOverlap < 0) {
|
|
637
|
+
errors.push("core.chunkOverlap must be a non-negative number");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (core.concurrency !== void 0) {
|
|
641
|
+
if (typeof core.concurrency !== "number" || core.concurrency < 1 || core.concurrency > 16) {
|
|
642
|
+
errors.push("core.concurrency must be between 1 and 16");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (core.embeddingBatchSize !== void 0) {
|
|
646
|
+
if (typeof core.embeddingBatchSize !== "number" || core.embeddingBatchSize <= 0) {
|
|
647
|
+
errors.push("core.embeddingBatchSize must be a positive number");
|
|
648
|
+
} else if (core.embeddingBatchSize > 100) {
|
|
649
|
+
warnings.push("core.embeddingBatchSize is very large (>100). This may cause memory issues");
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Validate MCP configuration settings
|
|
655
|
+
*/
|
|
656
|
+
validateMCPConfig(mcp, errors, _warnings) {
|
|
657
|
+
if (mcp.port !== void 0) {
|
|
658
|
+
if (typeof mcp.port !== "number" || mcp.port < 1024 || mcp.port > 65535) {
|
|
659
|
+
errors.push("mcp.port must be between 1024 and 65535");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (mcp.transport !== void 0) {
|
|
663
|
+
if (mcp.transport !== "stdio" && mcp.transport !== "socket") {
|
|
664
|
+
errors.push('mcp.transport must be either "stdio" or "socket"');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (mcp.autoIndexOnFirstRun !== void 0) {
|
|
668
|
+
if (typeof mcp.autoIndexOnFirstRun !== "boolean") {
|
|
669
|
+
errors.push("mcp.autoIndexOnFirstRun must be a boolean");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Validate git detection configuration settings
|
|
675
|
+
*/
|
|
676
|
+
validateGitDetectionConfig(gitDetection, errors, _warnings) {
|
|
677
|
+
if (gitDetection.enabled !== void 0) {
|
|
678
|
+
if (typeof gitDetection.enabled !== "boolean") {
|
|
679
|
+
errors.push("gitDetection.enabled must be a boolean");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (gitDetection.pollIntervalMs !== void 0) {
|
|
683
|
+
if (typeof gitDetection.pollIntervalMs !== "number" || gitDetection.pollIntervalMs < 100) {
|
|
684
|
+
errors.push("gitDetection.pollIntervalMs must be at least 100ms");
|
|
685
|
+
} else if (gitDetection.pollIntervalMs < 1e3) {
|
|
686
|
+
_warnings.push("gitDetection.pollIntervalMs is very short (<1s). This may impact performance");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Validate file watching configuration settings
|
|
692
|
+
*/
|
|
693
|
+
validateFileWatchingConfig(fileWatching, errors, warnings) {
|
|
694
|
+
if (fileWatching.enabled !== void 0) {
|
|
695
|
+
if (typeof fileWatching.enabled !== "boolean") {
|
|
696
|
+
errors.push("fileWatching.enabled must be a boolean");
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (fileWatching.debounceMs !== void 0) {
|
|
700
|
+
if (typeof fileWatching.debounceMs !== "number" || fileWatching.debounceMs < 0) {
|
|
701
|
+
errors.push("fileWatching.debounceMs must be a non-negative number");
|
|
702
|
+
} else if (fileWatching.debounceMs < 100) {
|
|
703
|
+
warnings.push("fileWatching.debounceMs is very short (<100ms). This may cause excessive reindexing");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Validate frameworks configuration
|
|
709
|
+
*/
|
|
710
|
+
validateFrameworks(frameworks, errors, warnings) {
|
|
711
|
+
if (!Array.isArray(frameworks)) {
|
|
712
|
+
errors.push("frameworks must be an array");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
frameworks.forEach((framework, index) => {
|
|
716
|
+
if (!framework || typeof framework !== "object") {
|
|
717
|
+
errors.push(`frameworks[${index}] must be an object`);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const fw = framework;
|
|
721
|
+
if (!fw.name) {
|
|
722
|
+
errors.push(`frameworks[${index}] missing required field: name`);
|
|
723
|
+
}
|
|
724
|
+
if (fw.path === void 0) {
|
|
725
|
+
errors.push(`frameworks[${index}] missing required field: path`);
|
|
726
|
+
} else if (typeof fw.path !== "string") {
|
|
727
|
+
errors.push(`frameworks[${index}].path must be a string`);
|
|
728
|
+
} else if (path5.isAbsolute(fw.path)) {
|
|
729
|
+
errors.push(`frameworks[${index}].path must be relative, got: ${fw.path}`);
|
|
730
|
+
}
|
|
731
|
+
if (fw.enabled === void 0) {
|
|
732
|
+
errors.push(`frameworks[${index}] missing required field: enabled`);
|
|
733
|
+
} else if (typeof fw.enabled !== "boolean") {
|
|
734
|
+
errors.push(`frameworks[${index}].enabled must be a boolean`);
|
|
735
|
+
}
|
|
736
|
+
if (!fw.config) {
|
|
737
|
+
errors.push(`frameworks[${index}] missing required field: config`);
|
|
738
|
+
} else {
|
|
739
|
+
this.validateFrameworkConfig(fw.config, `frameworks[${index}].config`, errors, warnings);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Validate framework-specific configuration
|
|
745
|
+
*/
|
|
746
|
+
validateFrameworkConfig(config, prefix, errors, _warnings) {
|
|
747
|
+
if (!config || typeof config !== "object") {
|
|
748
|
+
errors.push(`${prefix} must be an object`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (!Array.isArray(config.include)) {
|
|
752
|
+
errors.push(`${prefix}.include must be an array`);
|
|
753
|
+
} else {
|
|
754
|
+
config.include.forEach((pattern, i) => {
|
|
755
|
+
if (typeof pattern !== "string") {
|
|
756
|
+
errors.push(`${prefix}.include[${i}] must be a string`);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (!Array.isArray(config.exclude)) {
|
|
761
|
+
errors.push(`${prefix}.exclude must be an array`);
|
|
762
|
+
} else {
|
|
763
|
+
config.exclude.forEach((pattern, i) => {
|
|
764
|
+
if (typeof pattern !== "string") {
|
|
765
|
+
errors.push(`${prefix}.exclude[${i}] must be a string`);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
configService = new ConfigService();
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// src/vectordb/version.ts
|
|
776
|
+
import fs7 from "fs/promises";
|
|
777
|
+
import path7 from "path";
|
|
778
|
+
async function writeVersionFile(indexPath) {
|
|
779
|
+
try {
|
|
780
|
+
const versionFilePath = path7.join(indexPath, VERSION_FILE);
|
|
781
|
+
const timestamp = Date.now().toString();
|
|
782
|
+
await fs7.writeFile(versionFilePath, timestamp, "utf-8");
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.error(`Warning: Failed to write version file: ${error}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async function readVersionFile(indexPath) {
|
|
788
|
+
try {
|
|
789
|
+
const versionFilePath = path7.join(indexPath, VERSION_FILE);
|
|
790
|
+
const content = await fs7.readFile(versionFilePath, "utf-8");
|
|
791
|
+
const timestamp = parseInt(content.trim(), 10);
|
|
792
|
+
return isNaN(timestamp) ? 0 : timestamp;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
var VERSION_FILE;
|
|
798
|
+
var init_version = __esm({
|
|
799
|
+
"src/vectordb/version.ts"() {
|
|
800
|
+
"use strict";
|
|
801
|
+
VERSION_FILE = ".lien-index-version";
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// src/indexer/scanner.ts
|
|
806
|
+
import { glob } from "glob";
|
|
807
|
+
import ignore from "ignore";
|
|
808
|
+
import fs9 from "fs/promises";
|
|
809
|
+
import path9 from "path";
|
|
810
|
+
async function scanCodebaseWithFrameworks(rootDir, config) {
|
|
811
|
+
const allFiles = [];
|
|
812
|
+
for (const framework of config.frameworks) {
|
|
813
|
+
if (!framework.enabled) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const frameworkFiles = await scanFramework(rootDir, framework);
|
|
817
|
+
allFiles.push(...frameworkFiles);
|
|
818
|
+
}
|
|
819
|
+
return allFiles;
|
|
820
|
+
}
|
|
821
|
+
async function scanFramework(rootDir, framework) {
|
|
822
|
+
const frameworkPath = path9.join(rootDir, framework.path);
|
|
823
|
+
const gitignorePath = path9.join(frameworkPath, ".gitignore");
|
|
824
|
+
let ig = ignore();
|
|
825
|
+
try {
|
|
826
|
+
const gitignoreContent = await fs9.readFile(gitignorePath, "utf-8");
|
|
827
|
+
ig = ignore().add(gitignoreContent);
|
|
828
|
+
} catch (e) {
|
|
829
|
+
const rootGitignorePath = path9.join(rootDir, ".gitignore");
|
|
830
|
+
try {
|
|
831
|
+
const gitignoreContent = await fs9.readFile(rootGitignorePath, "utf-8");
|
|
832
|
+
ig = ignore().add(gitignoreContent);
|
|
833
|
+
} catch (e2) {
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
ig.add([
|
|
837
|
+
...framework.config.exclude,
|
|
838
|
+
".lien/**"
|
|
839
|
+
]);
|
|
840
|
+
const allFiles = [];
|
|
841
|
+
for (const pattern of framework.config.include) {
|
|
842
|
+
const files = await glob(pattern, {
|
|
843
|
+
cwd: frameworkPath,
|
|
844
|
+
absolute: false,
|
|
845
|
+
// Get paths relative to framework path
|
|
846
|
+
nodir: true,
|
|
847
|
+
ignore: framework.config.exclude
|
|
848
|
+
});
|
|
849
|
+
allFiles.push(...files);
|
|
850
|
+
}
|
|
851
|
+
const uniqueFiles = Array.from(new Set(allFiles));
|
|
852
|
+
return uniqueFiles.filter((file) => !ig.ignores(file)).map((file) => {
|
|
853
|
+
return framework.path === "." ? file : path9.join(framework.path, file);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
async function scanCodebase(options) {
|
|
857
|
+
const { rootDir, includePatterns = [], excludePatterns = [] } = options;
|
|
858
|
+
const gitignorePath = path9.join(rootDir, ".gitignore");
|
|
859
|
+
let ig = ignore();
|
|
860
|
+
try {
|
|
861
|
+
const gitignoreContent = await fs9.readFile(gitignorePath, "utf-8");
|
|
862
|
+
ig = ignore().add(gitignoreContent);
|
|
863
|
+
} catch (e) {
|
|
864
|
+
}
|
|
865
|
+
ig.add([
|
|
866
|
+
"node_modules/**",
|
|
867
|
+
".git/**",
|
|
868
|
+
"dist/**",
|
|
869
|
+
"build/**",
|
|
870
|
+
"*.min.js",
|
|
871
|
+
"*.min.css",
|
|
872
|
+
".lien/**",
|
|
873
|
+
...excludePatterns
|
|
874
|
+
]);
|
|
875
|
+
const patterns = includePatterns.length > 0 ? includePatterns : ["**/*.{ts,tsx,js,jsx,py,go,rs,java,cpp,c,h,md,mdx}"];
|
|
876
|
+
const allFiles = [];
|
|
877
|
+
for (const pattern of patterns) {
|
|
878
|
+
const files = await glob(pattern, {
|
|
879
|
+
cwd: rootDir,
|
|
880
|
+
absolute: true,
|
|
881
|
+
nodir: true,
|
|
882
|
+
ignore: ["node_modules/**", ".git/**"]
|
|
883
|
+
});
|
|
884
|
+
allFiles.push(...files);
|
|
885
|
+
}
|
|
886
|
+
const uniqueFiles = Array.from(new Set(allFiles));
|
|
887
|
+
return uniqueFiles.filter((file) => {
|
|
888
|
+
const relativePath = path9.relative(rootDir, file);
|
|
889
|
+
return !ig.ignores(relativePath);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
function detectLanguage(filepath) {
|
|
893
|
+
const ext = path9.extname(filepath).toLowerCase();
|
|
894
|
+
const languageMap = {
|
|
895
|
+
".ts": "typescript",
|
|
896
|
+
".tsx": "typescript",
|
|
897
|
+
".js": "javascript",
|
|
898
|
+
".jsx": "javascript",
|
|
899
|
+
".mjs": "javascript",
|
|
900
|
+
".cjs": "javascript",
|
|
901
|
+
".vue": "vue",
|
|
902
|
+
".py": "python",
|
|
903
|
+
".go": "go",
|
|
904
|
+
".rs": "rust",
|
|
905
|
+
".java": "java",
|
|
906
|
+
".cpp": "cpp",
|
|
907
|
+
".cc": "cpp",
|
|
908
|
+
".cxx": "cpp",
|
|
909
|
+
".c": "c",
|
|
910
|
+
".h": "c",
|
|
911
|
+
".hpp": "cpp",
|
|
912
|
+
".php": "php",
|
|
913
|
+
".rb": "ruby",
|
|
914
|
+
".swift": "swift",
|
|
915
|
+
".kt": "kotlin",
|
|
916
|
+
".cs": "csharp",
|
|
917
|
+
".scala": "scala",
|
|
918
|
+
".md": "markdown",
|
|
919
|
+
".mdx": "markdown",
|
|
920
|
+
".markdown": "markdown"
|
|
921
|
+
};
|
|
922
|
+
return languageMap[ext] || "unknown";
|
|
923
|
+
}
|
|
924
|
+
var init_scanner = __esm({
|
|
925
|
+
"src/indexer/scanner.ts"() {
|
|
926
|
+
"use strict";
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// src/indexer/symbol-extractor.ts
|
|
931
|
+
function extractSymbols(content, language) {
|
|
932
|
+
const symbols = {
|
|
933
|
+
functions: [],
|
|
934
|
+
classes: [],
|
|
935
|
+
interfaces: []
|
|
936
|
+
};
|
|
937
|
+
const normalizedLang = language.toLowerCase();
|
|
938
|
+
switch (normalizedLang) {
|
|
939
|
+
case "typescript":
|
|
940
|
+
case "tsx":
|
|
941
|
+
symbols.functions = extractTSFunctions(content);
|
|
942
|
+
symbols.classes = extractTSClasses(content);
|
|
943
|
+
symbols.interfaces = extractTSInterfaces(content);
|
|
944
|
+
break;
|
|
945
|
+
case "javascript":
|
|
946
|
+
case "jsx":
|
|
947
|
+
symbols.functions = extractJSFunctions(content);
|
|
948
|
+
symbols.classes = extractJSClasses(content);
|
|
949
|
+
break;
|
|
950
|
+
case "python":
|
|
951
|
+
case "py":
|
|
952
|
+
symbols.functions = extractPythonFunctions(content);
|
|
953
|
+
symbols.classes = extractPythonClasses(content);
|
|
954
|
+
break;
|
|
955
|
+
case "php":
|
|
956
|
+
symbols.functions = extractPHPFunctions(content);
|
|
957
|
+
symbols.classes = extractPHPClasses(content);
|
|
958
|
+
symbols.interfaces = extractPHPInterfaces(content);
|
|
959
|
+
break;
|
|
960
|
+
case "vue":
|
|
961
|
+
symbols.functions = extractVueFunctions(content);
|
|
962
|
+
symbols.classes = extractVueComponents(content);
|
|
963
|
+
break;
|
|
964
|
+
case "go":
|
|
965
|
+
symbols.functions = extractGoFunctions(content);
|
|
966
|
+
symbols.interfaces = extractGoInterfaces(content);
|
|
967
|
+
break;
|
|
968
|
+
case "java":
|
|
969
|
+
symbols.functions = extractJavaFunctions(content);
|
|
970
|
+
symbols.classes = extractJavaClasses(content);
|
|
971
|
+
symbols.interfaces = extractJavaInterfaces(content);
|
|
972
|
+
break;
|
|
973
|
+
case "csharp":
|
|
974
|
+
case "cs":
|
|
975
|
+
symbols.functions = extractCSharpFunctions(content);
|
|
976
|
+
symbols.classes = extractCSharpClasses(content);
|
|
977
|
+
symbols.interfaces = extractCSharpInterfaces(content);
|
|
978
|
+
break;
|
|
979
|
+
case "ruby":
|
|
980
|
+
case "rb":
|
|
981
|
+
symbols.functions = extractRubyFunctions(content);
|
|
982
|
+
symbols.classes = extractRubyClasses(content);
|
|
983
|
+
break;
|
|
984
|
+
case "rust":
|
|
985
|
+
case "rs":
|
|
986
|
+
symbols.functions = extractRustFunctions(content);
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
return symbols;
|
|
990
|
+
}
|
|
991
|
+
function extractTSFunctions(content) {
|
|
992
|
+
const names = /* @__PURE__ */ new Set();
|
|
993
|
+
const functionMatches = content.matchAll(/(?:async\s+)?function\s+(\w+)\s*\(/g);
|
|
994
|
+
for (const match of functionMatches) {
|
|
995
|
+
names.add(match[1]);
|
|
996
|
+
}
|
|
997
|
+
const arrowMatches = content.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g);
|
|
998
|
+
for (const match of arrowMatches) {
|
|
999
|
+
names.add(match[1]);
|
|
1000
|
+
}
|
|
1001
|
+
const methodMatches = content.matchAll(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/g);
|
|
1002
|
+
for (const match of methodMatches) {
|
|
1003
|
+
if (!["if", "for", "while", "switch", "catch"].includes(match[1])) {
|
|
1004
|
+
names.add(match[1]);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const exportMatches = content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)\s*\(/g);
|
|
1008
|
+
for (const match of exportMatches) {
|
|
1009
|
+
names.add(match[1]);
|
|
1010
|
+
}
|
|
1011
|
+
return Array.from(names);
|
|
1012
|
+
}
|
|
1013
|
+
function extractJSFunctions(content) {
|
|
1014
|
+
return extractTSFunctions(content);
|
|
1015
|
+
}
|
|
1016
|
+
function extractTSClasses(content) {
|
|
1017
|
+
const names = /* @__PURE__ */ new Set();
|
|
1018
|
+
const classMatches = content.matchAll(/(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/g);
|
|
1019
|
+
for (const match of classMatches) {
|
|
1020
|
+
names.add(match[1]);
|
|
1021
|
+
}
|
|
1022
|
+
return Array.from(names);
|
|
1023
|
+
}
|
|
1024
|
+
function extractJSClasses(content) {
|
|
1025
|
+
return extractTSClasses(content);
|
|
1026
|
+
}
|
|
1027
|
+
function extractTSInterfaces(content) {
|
|
1028
|
+
const names = /* @__PURE__ */ new Set();
|
|
1029
|
+
const interfaceMatches = content.matchAll(/(?:export\s+)?interface\s+(\w+)/g);
|
|
1030
|
+
for (const match of interfaceMatches) {
|
|
1031
|
+
names.add(match[1]);
|
|
1032
|
+
}
|
|
1033
|
+
const typeMatches = content.matchAll(/(?:export\s+)?type\s+(\w+)\s*=/g);
|
|
1034
|
+
for (const match of typeMatches) {
|
|
1035
|
+
names.add(match[1]);
|
|
1036
|
+
}
|
|
1037
|
+
return Array.from(names);
|
|
1038
|
+
}
|
|
1039
|
+
function extractPythonFunctions(content) {
|
|
1040
|
+
const names = /* @__PURE__ */ new Set();
|
|
1041
|
+
const functionMatches = content.matchAll(/def\s+(\w+)\s*\(/g);
|
|
1042
|
+
for (const match of functionMatches) {
|
|
1043
|
+
names.add(match[1]);
|
|
1044
|
+
}
|
|
1045
|
+
const asyncMatches = content.matchAll(/async\s+def\s+(\w+)\s*\(/g);
|
|
1046
|
+
for (const match of asyncMatches) {
|
|
1047
|
+
names.add(match[1]);
|
|
1048
|
+
}
|
|
1049
|
+
return Array.from(names);
|
|
1050
|
+
}
|
|
1051
|
+
function extractPythonClasses(content) {
|
|
1052
|
+
const names = /* @__PURE__ */ new Set();
|
|
1053
|
+
const classMatches = content.matchAll(/class\s+(\w+)(?:\s*\(|:)/g);
|
|
1054
|
+
for (const match of classMatches) {
|
|
1055
|
+
names.add(match[1]);
|
|
1056
|
+
}
|
|
1057
|
+
return Array.from(names);
|
|
1058
|
+
}
|
|
1059
|
+
function extractPHPFunctions(content) {
|
|
1060
|
+
const names = /* @__PURE__ */ new Set();
|
|
1061
|
+
const functionMatches = content.matchAll(/(?:public|private|protected)?\s*function\s+(\w+)\s*\(/g);
|
|
1062
|
+
for (const match of functionMatches) {
|
|
1063
|
+
names.add(match[1]);
|
|
1064
|
+
}
|
|
1065
|
+
return Array.from(names);
|
|
1066
|
+
}
|
|
1067
|
+
function extractPHPClasses(content) {
|
|
1068
|
+
const names = /* @__PURE__ */ new Set();
|
|
1069
|
+
const classMatches = content.matchAll(/(?:abstract\s+)?class\s+(\w+)/g);
|
|
1070
|
+
for (const match of classMatches) {
|
|
1071
|
+
names.add(match[1]);
|
|
1072
|
+
}
|
|
1073
|
+
return Array.from(names);
|
|
1074
|
+
}
|
|
1075
|
+
function extractPHPInterfaces(content) {
|
|
1076
|
+
const names = /* @__PURE__ */ new Set();
|
|
1077
|
+
const interfaceMatches = content.matchAll(/interface\s+(\w+)/g);
|
|
1078
|
+
for (const match of interfaceMatches) {
|
|
1079
|
+
names.add(match[1]);
|
|
1080
|
+
}
|
|
1081
|
+
const traitMatches = content.matchAll(/trait\s+(\w+)/g);
|
|
1082
|
+
for (const match of traitMatches) {
|
|
1083
|
+
names.add(match[1]);
|
|
1084
|
+
}
|
|
1085
|
+
return Array.from(names);
|
|
1086
|
+
}
|
|
1087
|
+
function extractGoFunctions(content) {
|
|
1088
|
+
const names = /* @__PURE__ */ new Set();
|
|
1089
|
+
const functionMatches = content.matchAll(/func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/g);
|
|
1090
|
+
for (const match of functionMatches) {
|
|
1091
|
+
names.add(match[1]);
|
|
1092
|
+
}
|
|
1093
|
+
return Array.from(names);
|
|
1094
|
+
}
|
|
1095
|
+
function extractGoInterfaces(content) {
|
|
1096
|
+
const names = /* @__PURE__ */ new Set();
|
|
1097
|
+
const interfaceMatches = content.matchAll(/type\s+(\w+)\s+interface\s*\{/g);
|
|
1098
|
+
for (const match of interfaceMatches) {
|
|
1099
|
+
names.add(match[1]);
|
|
1100
|
+
}
|
|
1101
|
+
const structMatches = content.matchAll(/type\s+(\w+)\s+struct\s*\{/g);
|
|
1102
|
+
for (const match of structMatches) {
|
|
1103
|
+
names.add(match[1]);
|
|
1104
|
+
}
|
|
1105
|
+
return Array.from(names);
|
|
1106
|
+
}
|
|
1107
|
+
function extractJavaFunctions(content) {
|
|
1108
|
+
const names = /* @__PURE__ */ new Set();
|
|
1109
|
+
const methodMatches = content.matchAll(/(?:public|private|protected)\s+(?:static\s+)?(?:\w+(?:<[^>]+>)?)\s+(\w+)\s*\(/g);
|
|
1110
|
+
for (const match of methodMatches) {
|
|
1111
|
+
names.add(match[1]);
|
|
1112
|
+
}
|
|
1113
|
+
return Array.from(names);
|
|
1114
|
+
}
|
|
1115
|
+
function extractJavaClasses(content) {
|
|
1116
|
+
const names = /* @__PURE__ */ new Set();
|
|
1117
|
+
const classMatches = content.matchAll(/(?:public\s+)?(?:abstract\s+)?class\s+(\w+)/g);
|
|
1118
|
+
for (const match of classMatches) {
|
|
1119
|
+
names.add(match[1]);
|
|
1120
|
+
}
|
|
1121
|
+
return Array.from(names);
|
|
1122
|
+
}
|
|
1123
|
+
function extractJavaInterfaces(content) {
|
|
1124
|
+
const names = /* @__PURE__ */ new Set();
|
|
1125
|
+
const interfaceMatches = content.matchAll(/(?:public\s+)?interface\s+(\w+)/g);
|
|
1126
|
+
for (const match of interfaceMatches) {
|
|
1127
|
+
names.add(match[1]);
|
|
1128
|
+
}
|
|
1129
|
+
return Array.from(names);
|
|
1130
|
+
}
|
|
1131
|
+
function extractCSharpFunctions(content) {
|
|
1132
|
+
const names = /* @__PURE__ */ new Set();
|
|
1133
|
+
const methodMatches = content.matchAll(/(?:public|private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:\w+(?:<[^>]+>)?)\s+(\w+)\s*\(/g);
|
|
1134
|
+
for (const match of methodMatches) {
|
|
1135
|
+
names.add(match[1]);
|
|
1136
|
+
}
|
|
1137
|
+
return Array.from(names);
|
|
1138
|
+
}
|
|
1139
|
+
function extractCSharpClasses(content) {
|
|
1140
|
+
const names = /* @__PURE__ */ new Set();
|
|
1141
|
+
const classMatches = content.matchAll(/(?:public|internal)?\s*(?:abstract\s+)?class\s+(\w+)/g);
|
|
1142
|
+
for (const match of classMatches) {
|
|
1143
|
+
names.add(match[1]);
|
|
1144
|
+
}
|
|
1145
|
+
return Array.from(names);
|
|
1146
|
+
}
|
|
1147
|
+
function extractCSharpInterfaces(content) {
|
|
1148
|
+
const names = /* @__PURE__ */ new Set();
|
|
1149
|
+
const interfaceMatches = content.matchAll(/(?:public|internal)?\s*interface\s+(\w+)/g);
|
|
1150
|
+
for (const match of interfaceMatches) {
|
|
1151
|
+
names.add(match[1]);
|
|
1152
|
+
}
|
|
1153
|
+
return Array.from(names);
|
|
1154
|
+
}
|
|
1155
|
+
function extractRubyFunctions(content) {
|
|
1156
|
+
const names = /* @__PURE__ */ new Set();
|
|
1157
|
+
const methodMatches = content.matchAll(/def\s+(?:self\.)?(\w+)/g);
|
|
1158
|
+
for (const match of methodMatches) {
|
|
1159
|
+
names.add(match[1]);
|
|
1160
|
+
}
|
|
1161
|
+
return Array.from(names);
|
|
1162
|
+
}
|
|
1163
|
+
function extractRubyClasses(content) {
|
|
1164
|
+
const names = /* @__PURE__ */ new Set();
|
|
1165
|
+
const classMatches = content.matchAll(/class\s+(\w+)/g);
|
|
1166
|
+
for (const match of classMatches) {
|
|
1167
|
+
names.add(match[1]);
|
|
1168
|
+
}
|
|
1169
|
+
const moduleMatches = content.matchAll(/module\s+(\w+)/g);
|
|
1170
|
+
for (const match of moduleMatches) {
|
|
1171
|
+
names.add(match[1]);
|
|
1172
|
+
}
|
|
1173
|
+
return Array.from(names);
|
|
1174
|
+
}
|
|
1175
|
+
function extractRustFunctions(content) {
|
|
1176
|
+
const names = /* @__PURE__ */ new Set();
|
|
1177
|
+
const functionMatches = content.matchAll(/(?:pub\s+)?fn\s+(\w+)\s*\(/g);
|
|
1178
|
+
for (const match of functionMatches) {
|
|
1179
|
+
names.add(match[1]);
|
|
1180
|
+
}
|
|
1181
|
+
const structMatches = content.matchAll(/(?:pub\s+)?struct\s+(\w+)/g);
|
|
1182
|
+
for (const match of structMatches) {
|
|
1183
|
+
names.add(match[1]);
|
|
1184
|
+
}
|
|
1185
|
+
const traitMatches = content.matchAll(/(?:pub\s+)?trait\s+(\w+)/g);
|
|
1186
|
+
for (const match of traitMatches) {
|
|
1187
|
+
names.add(match[1]);
|
|
1188
|
+
}
|
|
1189
|
+
return Array.from(names);
|
|
1190
|
+
}
|
|
1191
|
+
function extractVueFunctions(content) {
|
|
1192
|
+
const names = /* @__PURE__ */ new Set();
|
|
1193
|
+
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
1194
|
+
if (!scriptMatch) return [];
|
|
1195
|
+
const scriptContent = scriptMatch[1];
|
|
1196
|
+
const compositionMatches = scriptContent.matchAll(/(?:const|function)\s+(\w+)\s*=/g);
|
|
1197
|
+
for (const match of compositionMatches) {
|
|
1198
|
+
names.add(match[1]);
|
|
1199
|
+
}
|
|
1200
|
+
const methodMatches = scriptContent.matchAll(/(\w+)\s*\([^)]*\)\s*{/g);
|
|
1201
|
+
for (const match of methodMatches) {
|
|
1202
|
+
names.add(match[1]);
|
|
1203
|
+
}
|
|
1204
|
+
return Array.from(names);
|
|
1205
|
+
}
|
|
1206
|
+
function extractVueComponents(content) {
|
|
1207
|
+
const names = /* @__PURE__ */ new Set();
|
|
1208
|
+
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
1209
|
+
if (!scriptMatch) return [];
|
|
1210
|
+
const scriptContent = scriptMatch[1];
|
|
1211
|
+
const nameMatch = scriptContent.match(/name:\s*['"](\w+)['"]/);
|
|
1212
|
+
if (nameMatch) {
|
|
1213
|
+
names.add(nameMatch[1]);
|
|
1214
|
+
}
|
|
1215
|
+
const defineComponentMatch = scriptContent.match(/defineComponent\s*\(/);
|
|
1216
|
+
if (defineComponentMatch) {
|
|
1217
|
+
names.add("VueComponent");
|
|
1218
|
+
}
|
|
1219
|
+
return Array.from(names);
|
|
1220
|
+
}
|
|
1221
|
+
var init_symbol_extractor = __esm({
|
|
1222
|
+
"src/indexer/symbol-extractor.ts"() {
|
|
1223
|
+
"use strict";
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
// src/indexer/chunker.ts
|
|
1228
|
+
function chunkFile(filepath, content, options = {}) {
|
|
1229
|
+
const { chunkSize = 75, chunkOverlap = 10 } = options;
|
|
1230
|
+
const lines = content.split("\n");
|
|
1231
|
+
const chunks = [];
|
|
1232
|
+
const language = detectLanguage(filepath);
|
|
1233
|
+
if (lines.length === 0 || lines.length === 1 && lines[0].trim() === "") {
|
|
1234
|
+
return chunks;
|
|
1235
|
+
}
|
|
1236
|
+
for (let i = 0; i < lines.length; i += chunkSize - chunkOverlap) {
|
|
1237
|
+
const endLine = Math.min(i + chunkSize, lines.length);
|
|
1238
|
+
const chunkLines = lines.slice(i, endLine);
|
|
1239
|
+
const chunkContent = chunkLines.join("\n");
|
|
1240
|
+
if (chunkContent.trim().length === 0) {
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
const symbols = extractSymbols(chunkContent, language);
|
|
1244
|
+
chunks.push({
|
|
1245
|
+
content: chunkContent,
|
|
1246
|
+
metadata: {
|
|
1247
|
+
file: filepath,
|
|
1248
|
+
startLine: i + 1,
|
|
1249
|
+
endLine,
|
|
1250
|
+
type: "block",
|
|
1251
|
+
// MVP: all chunks are 'block' type
|
|
1252
|
+
language,
|
|
1253
|
+
symbols
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
if (endLine >= lines.length) {
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return chunks;
|
|
1261
|
+
}
|
|
1262
|
+
var init_chunker = __esm({
|
|
1263
|
+
"src/indexer/chunker.ts"() {
|
|
1264
|
+
"use strict";
|
|
1265
|
+
init_scanner();
|
|
1266
|
+
init_symbol_extractor();
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// src/embeddings/local.ts
|
|
1271
|
+
import { pipeline, env } from "@xenova/transformers";
|
|
1272
|
+
var LocalEmbeddings;
|
|
1273
|
+
var init_local = __esm({
|
|
1274
|
+
"src/embeddings/local.ts"() {
|
|
1275
|
+
"use strict";
|
|
1276
|
+
init_errors();
|
|
1277
|
+
init_constants();
|
|
1278
|
+
env.allowRemoteModels = true;
|
|
1279
|
+
env.allowLocalModels = true;
|
|
1280
|
+
LocalEmbeddings = class {
|
|
1281
|
+
extractor = null;
|
|
1282
|
+
modelName = DEFAULT_EMBEDDING_MODEL;
|
|
1283
|
+
initPromise = null;
|
|
1284
|
+
async initialize() {
|
|
1285
|
+
if (this.initPromise) {
|
|
1286
|
+
return this.initPromise;
|
|
1287
|
+
}
|
|
1288
|
+
if (this.extractor) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
this.initPromise = (async () => {
|
|
1292
|
+
try {
|
|
1293
|
+
this.extractor = await pipeline("feature-extraction", this.modelName);
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
this.initPromise = null;
|
|
1296
|
+
throw wrapError(error, "Failed to initialize embedding model");
|
|
1297
|
+
}
|
|
1298
|
+
})();
|
|
1299
|
+
return this.initPromise;
|
|
1300
|
+
}
|
|
1301
|
+
async embed(text) {
|
|
1302
|
+
await this.initialize();
|
|
1303
|
+
if (!this.extractor) {
|
|
1304
|
+
throw new EmbeddingError("Embedding model not initialized");
|
|
1305
|
+
}
|
|
1306
|
+
try {
|
|
1307
|
+
const output = await this.extractor(text, {
|
|
1308
|
+
pooling: "mean",
|
|
1309
|
+
normalize: true
|
|
1310
|
+
});
|
|
1311
|
+
return output.data;
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
throw wrapError(error, "Failed to generate embedding", { textLength: text.length });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async embedBatch(texts) {
|
|
1317
|
+
await this.initialize();
|
|
1318
|
+
if (!this.extractor) {
|
|
1319
|
+
throw new EmbeddingError("Embedding model not initialized");
|
|
1320
|
+
}
|
|
1321
|
+
try {
|
|
1322
|
+
const results = await Promise.all(
|
|
1323
|
+
texts.map((text) => this.embed(text))
|
|
1324
|
+
);
|
|
1325
|
+
return results;
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
throw wrapError(error, "Failed to generate batch embeddings", { batchSize: texts.length });
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
// src/embeddings/types.ts
|
|
1335
|
+
var EMBEDDING_DIMENSION;
|
|
1336
|
+
var init_types = __esm({
|
|
1337
|
+
"src/embeddings/types.ts"() {
|
|
1338
|
+
"use strict";
|
|
1339
|
+
init_constants();
|
|
1340
|
+
EMBEDDING_DIMENSION = EMBEDDING_DIMENSIONS;
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
// src/vectordb/relevance.ts
|
|
1345
|
+
function calculateRelevance(score) {
|
|
1346
|
+
if (score < 1) return "highly_relevant";
|
|
1347
|
+
if (score < 1.3) return "relevant";
|
|
1348
|
+
if (score < 1.5) return "loosely_related";
|
|
1349
|
+
return "not_relevant";
|
|
1350
|
+
}
|
|
1351
|
+
var init_relevance = __esm({
|
|
1352
|
+
"src/vectordb/relevance.ts"() {
|
|
1353
|
+
"use strict";
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
// src/vectordb/intent-classifier.ts
|
|
1358
|
+
function classifyQueryIntent(query) {
|
|
1359
|
+
const lower = query.toLowerCase().trim();
|
|
1360
|
+
if (lower.match(/where\s+(is|are|does|can\s+i\s+find)/) || lower.match(/find\s+the\s+/) || lower.match(/locate\s+/)) {
|
|
1361
|
+
return "location" /* LOCATION */;
|
|
1362
|
+
}
|
|
1363
|
+
if (lower.match(/how\s+does\s+.*\s+work/) || lower.match(/what\s+(is|are|does)/) || lower.match(/explain\s+/) || lower.match(/understand\s+/) || lower.match(/\b(process|workflow|architecture)\b/)) {
|
|
1364
|
+
return "conceptual" /* CONCEPTUAL */;
|
|
1365
|
+
}
|
|
1366
|
+
if (lower.match(/how\s+(is|are)\s+.*\s+(implemented|built|coded)/) || lower.match(/implementation\s+of/) || lower.match(/source\s+code\s+for/)) {
|
|
1367
|
+
return "implementation" /* IMPLEMENTATION */;
|
|
1368
|
+
}
|
|
1369
|
+
return "implementation" /* IMPLEMENTATION */;
|
|
1370
|
+
}
|
|
1371
|
+
var init_intent_classifier = __esm({
|
|
1372
|
+
"src/vectordb/intent-classifier.ts"() {
|
|
1373
|
+
"use strict";
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
// src/vectordb/lancedb.ts
|
|
1378
|
+
var lancedb_exports = {};
|
|
1379
|
+
__export(lancedb_exports, {
|
|
1380
|
+
VectorDB: () => VectorDB
|
|
1381
|
+
});
|
|
1382
|
+
import * as lancedb from "vectordb";
|
|
1383
|
+
import path10 from "path";
|
|
1384
|
+
import os2 from "os";
|
|
1385
|
+
import crypto2 from "crypto";
|
|
1386
|
+
function isDocumentationFile(filepath) {
|
|
1387
|
+
const lower = filepath.toLowerCase();
|
|
1388
|
+
const filename = path10.basename(filepath).toLowerCase();
|
|
1389
|
+
if (filename.startsWith("readme")) return true;
|
|
1390
|
+
if (filename.startsWith("changelog")) return true;
|
|
1391
|
+
if (filename.endsWith(".md") || filename.endsWith(".mdx") || filename.endsWith(".markdown")) {
|
|
1392
|
+
return true;
|
|
1393
|
+
}
|
|
1394
|
+
if (lower.includes("/docs/") || lower.includes("/documentation/") || lower.includes("/wiki/") || lower.includes("/.github/")) {
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
if (lower.includes("architecture") || lower.includes("workflow") || lower.includes("/flow/")) {
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
function isTestFile(filepath) {
|
|
1403
|
+
const lower = filepath.toLowerCase();
|
|
1404
|
+
if (lower.includes("/test/") || lower.includes("/tests/") || lower.includes("/__tests__/")) {
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1407
|
+
if (lower.includes(".test.") || lower.includes(".spec.") || lower.includes("_test.") || lower.includes("_spec.")) {
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1412
|
+
function isUtilityFile(filepath) {
|
|
1413
|
+
const lower = filepath.toLowerCase();
|
|
1414
|
+
if (lower.includes("/utils/") || lower.includes("/utilities/") || lower.includes("/helpers/") || lower.includes("/lib/")) {
|
|
1415
|
+
return true;
|
|
1416
|
+
}
|
|
1417
|
+
if (lower.includes(".util.") || lower.includes(".helper.") || lower.includes("-util.") || lower.includes("-helper.")) {
|
|
1418
|
+
return true;
|
|
1419
|
+
}
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
function boostPathRelevance(query, filepath, baseScore) {
|
|
1423
|
+
const queryTokens = query.toLowerCase().split(/\s+/);
|
|
1424
|
+
const pathSegments = filepath.toLowerCase().split("/");
|
|
1425
|
+
let boostFactor = 1;
|
|
1426
|
+
for (const token of queryTokens) {
|
|
1427
|
+
if (token.length <= 2) continue;
|
|
1428
|
+
if (pathSegments.some((seg) => seg.includes(token))) {
|
|
1429
|
+
boostFactor *= 0.9;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return baseScore * boostFactor;
|
|
1433
|
+
}
|
|
1434
|
+
function boostFilenameRelevance(query, filepath, baseScore) {
|
|
1435
|
+
const filename = path10.basename(filepath, path10.extname(filepath)).toLowerCase();
|
|
1436
|
+
const queryTokens = query.toLowerCase().split(/\s+/);
|
|
1437
|
+
let boostFactor = 1;
|
|
1438
|
+
for (const token of queryTokens) {
|
|
1439
|
+
if (token.length <= 2) continue;
|
|
1440
|
+
if (filename === token) {
|
|
1441
|
+
boostFactor *= 0.7;
|
|
1442
|
+
} else if (filename.includes(token)) {
|
|
1443
|
+
boostFactor *= 0.8;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return baseScore * boostFactor;
|
|
1447
|
+
}
|
|
1448
|
+
function boostForLocationIntent(query, filepath, baseScore) {
|
|
1449
|
+
let score = baseScore;
|
|
1450
|
+
const filename = path10.basename(filepath, path10.extname(filepath)).toLowerCase();
|
|
1451
|
+
const queryTokens = query.toLowerCase().split(/\s+/);
|
|
1452
|
+
for (const token of queryTokens) {
|
|
1453
|
+
if (token.length <= 2) continue;
|
|
1454
|
+
if (filename === token) {
|
|
1455
|
+
score *= 0.6;
|
|
1456
|
+
} else if (filename.includes(token)) {
|
|
1457
|
+
score *= 0.7;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
score = boostPathRelevance(query, filepath, score);
|
|
1461
|
+
if (isTestFile(filepath)) {
|
|
1462
|
+
score *= 1.1;
|
|
1463
|
+
}
|
|
1464
|
+
return score;
|
|
1465
|
+
}
|
|
1466
|
+
function boostForConceptualIntent(query, filepath, baseScore) {
|
|
1467
|
+
let score = baseScore;
|
|
1468
|
+
if (isDocumentationFile(filepath)) {
|
|
1469
|
+
score *= 0.65;
|
|
1470
|
+
const lower = filepath.toLowerCase();
|
|
1471
|
+
if (lower.includes("architecture") || lower.includes("workflow") || lower.includes("flow")) {
|
|
1472
|
+
score *= 0.9;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (isUtilityFile(filepath)) {
|
|
1476
|
+
score *= 1.05;
|
|
1477
|
+
}
|
|
1478
|
+
const filename = path10.basename(filepath, path10.extname(filepath)).toLowerCase();
|
|
1479
|
+
const queryTokens = query.toLowerCase().split(/\s+/);
|
|
1480
|
+
for (const token of queryTokens) {
|
|
1481
|
+
if (token.length <= 2) continue;
|
|
1482
|
+
if (filename.includes(token)) {
|
|
1483
|
+
score *= 0.9;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const pathSegments = filepath.toLowerCase().split(path10.sep);
|
|
1487
|
+
for (const token of queryTokens) {
|
|
1488
|
+
if (token.length <= 2) continue;
|
|
1489
|
+
for (const segment of pathSegments) {
|
|
1490
|
+
if (segment.includes(token)) {
|
|
1491
|
+
score *= 0.95;
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return score;
|
|
1497
|
+
}
|
|
1498
|
+
function boostForImplementationIntent(query, filepath, baseScore) {
|
|
1499
|
+
let score = baseScore;
|
|
1500
|
+
score = boostFilenameRelevance(query, filepath, score);
|
|
1501
|
+
score = boostPathRelevance(query, filepath, score);
|
|
1502
|
+
if (isTestFile(filepath)) {
|
|
1503
|
+
score *= 0.9;
|
|
1504
|
+
}
|
|
1505
|
+
return score;
|
|
1506
|
+
}
|
|
1507
|
+
function applyRelevanceBoosting(query, filepath, baseScore) {
|
|
1508
|
+
if (!query) {
|
|
1509
|
+
return baseScore;
|
|
1510
|
+
}
|
|
1511
|
+
const intent = classifyQueryIntent(query);
|
|
1512
|
+
switch (intent) {
|
|
1513
|
+
case "location" /* LOCATION */:
|
|
1514
|
+
return boostForLocationIntent(query, filepath, baseScore);
|
|
1515
|
+
case "conceptual" /* CONCEPTUAL */:
|
|
1516
|
+
return boostForConceptualIntent(query, filepath, baseScore);
|
|
1517
|
+
case "implementation" /* IMPLEMENTATION */:
|
|
1518
|
+
return boostForImplementationIntent(query, filepath, baseScore);
|
|
1519
|
+
default:
|
|
1520
|
+
return boostForImplementationIntent(query, filepath, baseScore);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
var VectorDB;
|
|
1524
|
+
var init_lancedb = __esm({
|
|
1525
|
+
"src/vectordb/lancedb.ts"() {
|
|
1526
|
+
"use strict";
|
|
1527
|
+
init_types();
|
|
1528
|
+
init_version();
|
|
1529
|
+
init_errors();
|
|
1530
|
+
init_relevance();
|
|
1531
|
+
init_intent_classifier();
|
|
1532
|
+
VectorDB = class _VectorDB {
|
|
1533
|
+
db = null;
|
|
1534
|
+
table = null;
|
|
1535
|
+
dbPath;
|
|
1536
|
+
tableName = "code_chunks";
|
|
1537
|
+
lastVersionCheck = 0;
|
|
1538
|
+
currentVersion = 0;
|
|
1539
|
+
constructor(projectRoot) {
|
|
1540
|
+
const projectName = path10.basename(projectRoot);
|
|
1541
|
+
const pathHash = crypto2.createHash("md5").update(projectRoot).digest("hex").substring(0, 8);
|
|
1542
|
+
this.dbPath = path10.join(
|
|
1543
|
+
os2.homedir(),
|
|
1544
|
+
".lien",
|
|
1545
|
+
"indices",
|
|
1546
|
+
`${projectName}-${pathHash}`
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
async initialize() {
|
|
1550
|
+
try {
|
|
1551
|
+
this.db = await lancedb.connect(this.dbPath);
|
|
1552
|
+
try {
|
|
1553
|
+
this.table = await this.db.openTable(this.tableName);
|
|
1554
|
+
} catch {
|
|
1555
|
+
this.table = null;
|
|
1556
|
+
}
|
|
1557
|
+
try {
|
|
1558
|
+
this.currentVersion = await readVersionFile(this.dbPath);
|
|
1559
|
+
} catch {
|
|
1560
|
+
this.currentVersion = 0;
|
|
1561
|
+
}
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
throw wrapError(error, "Failed to initialize vector database", { dbPath: this.dbPath });
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
async insertBatch(vectors, metadatas, contents) {
|
|
1567
|
+
if (!this.db) {
|
|
1568
|
+
throw new DatabaseError("Vector database not initialized");
|
|
1569
|
+
}
|
|
1570
|
+
if (vectors.length !== metadatas.length || vectors.length !== contents.length) {
|
|
1571
|
+
throw new DatabaseError("Vectors, metadatas, and contents arrays must have the same length", {
|
|
1572
|
+
vectorsLength: vectors.length,
|
|
1573
|
+
metadatasLength: metadatas.length,
|
|
1574
|
+
contentsLength: contents.length
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
try {
|
|
1578
|
+
const records = vectors.map((vector, i) => ({
|
|
1579
|
+
vector: Array.from(vector),
|
|
1580
|
+
content: contents[i],
|
|
1581
|
+
file: metadatas[i].file,
|
|
1582
|
+
startLine: metadatas[i].startLine,
|
|
1583
|
+
endLine: metadatas[i].endLine,
|
|
1584
|
+
type: metadatas[i].type,
|
|
1585
|
+
language: metadatas[i].language,
|
|
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);
|
|
1595
|
+
}
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
throw wrapError(error, "Failed to insert batch into vector database");
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
async search(queryVector, limit = 5, query) {
|
|
1601
|
+
if (!this.table) {
|
|
1602
|
+
throw new DatabaseError("Vector database not initialized");
|
|
1603
|
+
}
|
|
1604
|
+
try {
|
|
1605
|
+
const results = await this.table.search(Array.from(queryVector)).limit(limit + 20).execute();
|
|
1606
|
+
const filtered = results.filter(
|
|
1607
|
+
(r) => r.content && r.content.trim().length > 0 && r.file && r.file.length > 0
|
|
1608
|
+
).map((r) => {
|
|
1609
|
+
const baseScore = r._distance ?? 0;
|
|
1610
|
+
const boostedScore = applyRelevanceBoosting(query, r.file, baseScore);
|
|
1611
|
+
return {
|
|
1612
|
+
content: r.content,
|
|
1613
|
+
metadata: {
|
|
1614
|
+
file: r.file,
|
|
1615
|
+
startLine: r.startLine,
|
|
1616
|
+
endLine: r.endLine,
|
|
1617
|
+
type: r.type,
|
|
1618
|
+
language: r.language
|
|
1619
|
+
},
|
|
1620
|
+
score: boostedScore,
|
|
1621
|
+
relevance: calculateRelevance(boostedScore)
|
|
1622
|
+
};
|
|
1623
|
+
}).sort((a, b) => a.score - b.score).slice(0, limit);
|
|
1624
|
+
return filtered;
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
const errorMsg = String(error);
|
|
1627
|
+
if (errorMsg.includes("Not found:") || errorMsg.includes(".lance")) {
|
|
1628
|
+
try {
|
|
1629
|
+
await this.initialize();
|
|
1630
|
+
const results = await this.table.search(Array.from(queryVector)).limit(limit + 20).execute();
|
|
1631
|
+
return results.filter(
|
|
1632
|
+
(r) => r.content && r.content.trim().length > 0 && r.file && r.file.length > 0
|
|
1633
|
+
).map((r) => {
|
|
1634
|
+
const baseScore = r._distance ?? 0;
|
|
1635
|
+
const boostedScore = applyRelevanceBoosting(query, r.file, baseScore);
|
|
1636
|
+
return {
|
|
1637
|
+
content: r.content,
|
|
1638
|
+
metadata: {
|
|
1639
|
+
file: r.file,
|
|
1640
|
+
startLine: r.startLine,
|
|
1641
|
+
endLine: r.endLine,
|
|
1642
|
+
type: r.type,
|
|
1643
|
+
language: r.language
|
|
1644
|
+
},
|
|
1645
|
+
score: boostedScore,
|
|
1646
|
+
relevance: calculateRelevance(boostedScore)
|
|
1647
|
+
};
|
|
1648
|
+
}).sort((a, b) => a.score - b.score).slice(0, limit);
|
|
1649
|
+
} catch (retryError) {
|
|
1650
|
+
throw new DatabaseError(
|
|
1651
|
+
`Index appears corrupted or outdated. Please restart the MCP server or run 'lien reindex' in the project directory.`,
|
|
1652
|
+
{ originalError: retryError }
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
throw wrapError(error, "Failed to search vector database");
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async scanWithFilter(options) {
|
|
1660
|
+
if (!this.table) {
|
|
1661
|
+
throw new DatabaseError("Vector database not initialized");
|
|
1662
|
+
}
|
|
1663
|
+
const { language, pattern, limit = 100 } = options;
|
|
1664
|
+
try {
|
|
1665
|
+
const zeroVector = Array(EMBEDDING_DIMENSION).fill(0);
|
|
1666
|
+
const query = this.table.search(zeroVector).where('file != ""').limit(Math.max(limit * 5, 200));
|
|
1667
|
+
const results = await query.execute();
|
|
1668
|
+
let filtered = results.filter(
|
|
1669
|
+
(r) => r.content && r.content.trim().length > 0 && r.file && r.file.length > 0
|
|
1670
|
+
);
|
|
1671
|
+
if (language) {
|
|
1672
|
+
filtered = filtered.filter(
|
|
1673
|
+
(r) => r.language && r.language.toLowerCase() === language.toLowerCase()
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
if (pattern) {
|
|
1677
|
+
const regex = new RegExp(pattern, "i");
|
|
1678
|
+
filtered = filtered.filter(
|
|
1679
|
+
(r) => regex.test(r.content) || regex.test(r.file)
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
return filtered.slice(0, limit).map((r) => {
|
|
1683
|
+
const score = 0;
|
|
1684
|
+
return {
|
|
1685
|
+
content: r.content,
|
|
1686
|
+
metadata: {
|
|
1687
|
+
file: r.file,
|
|
1688
|
+
startLine: r.startLine,
|
|
1689
|
+
endLine: r.endLine,
|
|
1690
|
+
type: r.type,
|
|
1691
|
+
language: r.language
|
|
1692
|
+
},
|
|
1693
|
+
score,
|
|
1694
|
+
relevance: calculateRelevance(score)
|
|
1695
|
+
};
|
|
1696
|
+
});
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
throw wrapError(error, "Failed to scan with filter");
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
async querySymbols(options) {
|
|
1702
|
+
if (!this.table) {
|
|
1703
|
+
throw new DatabaseError("Vector database not initialized");
|
|
1704
|
+
}
|
|
1705
|
+
const { language, pattern, symbolType, limit = 50 } = options;
|
|
1706
|
+
try {
|
|
1707
|
+
const zeroVector = Array(EMBEDDING_DIMENSION).fill(0);
|
|
1708
|
+
const query = this.table.search(zeroVector).where('file != ""').limit(Math.max(limit * 10, 500));
|
|
1709
|
+
const results = await query.execute();
|
|
1710
|
+
let filtered = results.filter((r) => {
|
|
1711
|
+
if (!r.content || r.content.trim().length === 0) {
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
if (!r.file || r.file.length === 0) {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
if (language && (!r.language || r.language.toLowerCase() !== language.toLowerCase())) {
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
const symbols = symbolType === "function" ? r.functionNames || [] : symbolType === "class" ? r.classNames || [] : symbolType === "interface" ? r.interfaceNames || [] : [...r.functionNames || [], ...r.classNames || [], ...r.interfaceNames || []];
|
|
1721
|
+
if (symbols.length === 0) {
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
if (pattern) {
|
|
1725
|
+
const regex = new RegExp(pattern, "i");
|
|
1726
|
+
return symbols.some((s) => regex.test(s));
|
|
1727
|
+
}
|
|
1728
|
+
return true;
|
|
1729
|
+
});
|
|
1730
|
+
return filtered.slice(0, limit).map((r) => {
|
|
1731
|
+
const score = 0;
|
|
1732
|
+
return {
|
|
1733
|
+
content: r.content,
|
|
1734
|
+
metadata: {
|
|
1735
|
+
file: r.file,
|
|
1736
|
+
startLine: r.startLine,
|
|
1737
|
+
endLine: r.endLine,
|
|
1738
|
+
type: r.type,
|
|
1739
|
+
language: r.language,
|
|
1740
|
+
symbols: {
|
|
1741
|
+
functions: r.functionNames || [],
|
|
1742
|
+
classes: r.classNames || [],
|
|
1743
|
+
interfaces: r.interfaceNames || []
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
score,
|
|
1747
|
+
relevance: calculateRelevance(score)
|
|
1748
|
+
};
|
|
1749
|
+
});
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
throw wrapError(error, "Failed to query symbols");
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
async clear() {
|
|
1755
|
+
if (!this.db) {
|
|
1756
|
+
throw new DatabaseError("Vector database not initialized");
|
|
1757
|
+
}
|
|
1758
|
+
try {
|
|
1759
|
+
if (this.table) {
|
|
1760
|
+
await this.db.dropTable(this.tableName);
|
|
1761
|
+
}
|
|
1762
|
+
this.table = null;
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
throw wrapError(error, "Failed to clear vector database");
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Deletes all chunks from a specific file.
|
|
1769
|
+
* Used for incremental reindexing when a file is deleted or needs to be re-indexed.
|
|
1770
|
+
*
|
|
1771
|
+
* @param filepath - Path to the file whose chunks should be deleted
|
|
1772
|
+
*/
|
|
1773
|
+
async deleteByFile(filepath) {
|
|
1774
|
+
if (!this.table) {
|
|
1775
|
+
throw new DatabaseError("Vector database not initialized");
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
await this.table.delete(`file = "${filepath}"`);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
throw wrapError(error, "Failed to delete file from vector database");
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Updates a file in the index by atomically deleting old chunks and inserting new ones.
|
|
1785
|
+
* This is the primary method for incremental reindexing.
|
|
1786
|
+
*
|
|
1787
|
+
* @param filepath - Path to the file being updated
|
|
1788
|
+
* @param vectors - New embedding vectors
|
|
1789
|
+
* @param metadatas - New chunk metadata
|
|
1790
|
+
* @param contents - New chunk contents
|
|
1791
|
+
*/
|
|
1792
|
+
async updateFile(filepath, vectors, metadatas, contents) {
|
|
1793
|
+
if (!this.table) {
|
|
1794
|
+
throw new DatabaseError("Vector database not initialized");
|
|
1795
|
+
}
|
|
1796
|
+
try {
|
|
1797
|
+
await this.deleteByFile(filepath);
|
|
1798
|
+
if (vectors.length > 0) {
|
|
1799
|
+
await this.insertBatch(vectors, metadatas, contents);
|
|
1800
|
+
}
|
|
1801
|
+
await writeVersionFile(this.dbPath);
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
throw wrapError(error, "Failed to update file in vector database");
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Checks if the index version has changed since last check.
|
|
1808
|
+
* Uses caching to minimize I/O overhead (checks at most once per second).
|
|
1809
|
+
*
|
|
1810
|
+
* @returns true if version has changed, false otherwise
|
|
1811
|
+
*/
|
|
1812
|
+
async checkVersion() {
|
|
1813
|
+
const now = Date.now();
|
|
1814
|
+
if (now - this.lastVersionCheck < 1e3) {
|
|
1815
|
+
return false;
|
|
1816
|
+
}
|
|
1817
|
+
this.lastVersionCheck = now;
|
|
1818
|
+
try {
|
|
1819
|
+
const version = await readVersionFile(this.dbPath);
|
|
1820
|
+
if (version > this.currentVersion) {
|
|
1821
|
+
this.currentVersion = version;
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
return false;
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
return false;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Reconnects to the database by reinitializing the connection.
|
|
1831
|
+
* Used when the index has been rebuilt/reindexed.
|
|
1832
|
+
* Forces a complete reload from disk by closing existing connections first.
|
|
1833
|
+
*/
|
|
1834
|
+
async reconnect() {
|
|
1835
|
+
try {
|
|
1836
|
+
this.table = null;
|
|
1837
|
+
this.db = null;
|
|
1838
|
+
await this.initialize();
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
throw wrapError(error, "Failed to reconnect to vector database");
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Gets the current index version (timestamp of last reindex).
|
|
1845
|
+
*
|
|
1846
|
+
* @returns Version timestamp, or 0 if unknown
|
|
1847
|
+
*/
|
|
1848
|
+
getCurrentVersion() {
|
|
1849
|
+
return this.currentVersion;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Gets the current index version as a human-readable date string.
|
|
1853
|
+
*
|
|
1854
|
+
* @returns Formatted date string, or 'Unknown' if no version
|
|
1855
|
+
*/
|
|
1856
|
+
getVersionDate() {
|
|
1857
|
+
if (this.currentVersion === 0) {
|
|
1858
|
+
return "Unknown";
|
|
1859
|
+
}
|
|
1860
|
+
return new Date(this.currentVersion).toLocaleString();
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Checks if the database contains real indexed data.
|
|
1864
|
+
* Used to detect first run and trigger auto-indexing.
|
|
1865
|
+
*
|
|
1866
|
+
* @returns true if database has real code chunks, false if empty or only schema rows
|
|
1867
|
+
*/
|
|
1868
|
+
async hasData() {
|
|
1869
|
+
if (!this.table) {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
const count = await this.table.countRows();
|
|
1874
|
+
if (count === 0) {
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1877
|
+
const sample = await this.table.search(Array(EMBEDDING_DIMENSION).fill(0)).limit(Math.min(count, 5)).execute();
|
|
1878
|
+
const hasRealData = sample.some(
|
|
1879
|
+
(r) => r.content && r.content.trim().length > 0
|
|
1880
|
+
);
|
|
1881
|
+
return hasRealData;
|
|
1882
|
+
} catch {
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
static async load(projectRoot) {
|
|
1887
|
+
const db = new _VectorDB(projectRoot);
|
|
1888
|
+
await db.initialize();
|
|
1889
|
+
return db;
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
// src/indexer/index.ts
|
|
1896
|
+
var indexer_exports = {};
|
|
1897
|
+
__export(indexer_exports, {
|
|
1898
|
+
indexCodebase: () => indexCodebase
|
|
1899
|
+
});
|
|
1900
|
+
import fs10 from "fs/promises";
|
|
1901
|
+
import ora from "ora";
|
|
1902
|
+
import chalk4 from "chalk";
|
|
1903
|
+
import pLimit from "p-limit";
|
|
1904
|
+
async function indexCodebase(options = {}) {
|
|
1905
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
1906
|
+
const spinner = ora("Starting indexing process...").start();
|
|
1907
|
+
try {
|
|
1908
|
+
spinner.text = "Loading configuration...";
|
|
1909
|
+
const config = await configService.load(rootDir);
|
|
1910
|
+
spinner.text = "Scanning codebase...";
|
|
1911
|
+
let files;
|
|
1912
|
+
if (isModernConfig(config) && config.frameworks.length > 0) {
|
|
1913
|
+
files = await scanCodebaseWithFrameworks(rootDir, config);
|
|
1914
|
+
} else if (isLegacyConfig(config)) {
|
|
1915
|
+
files = await scanCodebase({
|
|
1916
|
+
rootDir,
|
|
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;
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
const filePromises = files.map(
|
|
1964
|
+
(file) => limit(async () => {
|
|
1965
|
+
try {
|
|
1966
|
+
const content = await fs10.readFile(file, "utf-8");
|
|
1967
|
+
const chunkSize = isModernConfig(config) ? config.core.chunkSize : 75;
|
|
1968
|
+
const chunkOverlap = isModernConfig(config) ? config.core.chunkOverlap : 10;
|
|
1969
|
+
const chunks = chunkFile(file, content, {
|
|
1970
|
+
chunkSize,
|
|
1971
|
+
chunkOverlap
|
|
1972
|
+
});
|
|
1973
|
+
if (chunks.length === 0) {
|
|
1974
|
+
processedFiles++;
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
for (const chunk of chunks) {
|
|
1978
|
+
chunkAccumulator.push({
|
|
1979
|
+
chunk,
|
|
1980
|
+
content: chunk.content
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
if (chunkAccumulator.length >= batchSize) {
|
|
1984
|
+
await processAccumulatedChunks();
|
|
1985
|
+
}
|
|
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
|
+
} catch (error) {
|
|
1992
|
+
if (options.verbose) {
|
|
1993
|
+
console.error(chalk4.yellow(`
|
|
1994
|
+
\u26A0\uFE0F Skipping ${file}: ${error}`));
|
|
1995
|
+
}
|
|
1996
|
+
processedFiles++;
|
|
1997
|
+
}
|
|
1998
|
+
})
|
|
1999
|
+
);
|
|
2000
|
+
await Promise.all(filePromises);
|
|
2001
|
+
await processAccumulatedChunks();
|
|
2002
|
+
await writeVersionFile(vectorDB.dbPath);
|
|
2003
|
+
const totalTime = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2004
|
+
spinner.succeed(
|
|
2005
|
+
`Indexed ${processedFiles} files (${processedChunks} chunks) in ${totalTime}s using ${concurrency}x concurrency`
|
|
2006
|
+
);
|
|
2007
|
+
console.log(chalk4.dim("\nNext step: Run"), chalk4.bold("lien serve"), chalk4.dim("to start the MCP server"));
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
spinner.fail(`Indexing failed: ${error}`);
|
|
2010
|
+
throw error;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
var init_indexer = __esm({
|
|
2014
|
+
"src/indexer/index.ts"() {
|
|
2015
|
+
"use strict";
|
|
2016
|
+
init_scanner();
|
|
2017
|
+
init_chunker();
|
|
2018
|
+
init_local();
|
|
2019
|
+
init_lancedb();
|
|
2020
|
+
init_service();
|
|
2021
|
+
init_version();
|
|
2022
|
+
init_schema();
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
// src/cli/index.ts
|
|
2027
|
+
import { Command } from "commander";
|
|
2028
|
+
import { createRequire as createRequire3 } from "module";
|
|
2029
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2030
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2031
|
+
|
|
2032
|
+
// src/cli/init.ts
|
|
2033
|
+
init_schema();
|
|
2034
|
+
init_merge();
|
|
2035
|
+
init_banner();
|
|
2036
|
+
init_migration();
|
|
2037
|
+
import fs4 from "fs/promises";
|
|
2038
|
+
import path4 from "path";
|
|
2039
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2040
|
+
import chalk2 from "chalk";
|
|
2041
|
+
import inquirer from "inquirer";
|
|
2042
|
+
|
|
2043
|
+
// src/frameworks/detector-service.ts
|
|
2044
|
+
import fs3 from "fs/promises";
|
|
2045
|
+
import path3 from "path";
|
|
2046
|
+
|
|
2047
|
+
// src/frameworks/types.ts
|
|
2048
|
+
var defaultDetectionOptions = {
|
|
2049
|
+
maxDepth: 3,
|
|
2050
|
+
skipDirs: [
|
|
2051
|
+
"node_modules",
|
|
2052
|
+
"vendor",
|
|
2053
|
+
"dist",
|
|
2054
|
+
"build",
|
|
2055
|
+
".next",
|
|
2056
|
+
".nuxt",
|
|
2057
|
+
"coverage",
|
|
2058
|
+
".git",
|
|
2059
|
+
".idea",
|
|
2060
|
+
".vscode",
|
|
2061
|
+
"tmp",
|
|
2062
|
+
"temp"
|
|
2063
|
+
]
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/frameworks/nodejs/detector.ts
|
|
2067
|
+
import fs from "fs/promises";
|
|
2068
|
+
import path from "path";
|
|
2069
|
+
|
|
2070
|
+
// src/frameworks/nodejs/config.ts
|
|
2071
|
+
async function generateNodeJsConfig(_rootDir, _relativePath) {
|
|
2072
|
+
return {
|
|
2073
|
+
include: [
|
|
2074
|
+
"src/**/*.ts",
|
|
2075
|
+
"src/**/*.tsx",
|
|
2076
|
+
"src/**/*.js",
|
|
2077
|
+
"src/**/*.jsx",
|
|
2078
|
+
"src/**/*.mjs",
|
|
2079
|
+
"src/**/*.cjs",
|
|
2080
|
+
"lib/**/*.ts",
|
|
2081
|
+
"lib/**/*.js",
|
|
2082
|
+
"*.ts",
|
|
2083
|
+
"*.js",
|
|
2084
|
+
"*.mjs",
|
|
2085
|
+
"*.cjs",
|
|
2086
|
+
"**/*.md",
|
|
2087
|
+
"**/*.mdx",
|
|
2088
|
+
"docs/**/*.md",
|
|
2089
|
+
"README.md",
|
|
2090
|
+
"CHANGELOG.md",
|
|
2091
|
+
"CONTRIBUTING.md"
|
|
2092
|
+
],
|
|
2093
|
+
exclude: [
|
|
2094
|
+
"node_modules/**",
|
|
2095
|
+
"dist/**",
|
|
2096
|
+
"build/**",
|
|
2097
|
+
"coverage/**",
|
|
2098
|
+
".next/**",
|
|
2099
|
+
".nuxt/**",
|
|
2100
|
+
"out/**",
|
|
2101
|
+
"*.min.js",
|
|
2102
|
+
"*.min.css",
|
|
2103
|
+
"*.bundle.js"
|
|
2104
|
+
]
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/frameworks/nodejs/detector.ts
|
|
2109
|
+
var nodejsDetector = {
|
|
2110
|
+
name: "nodejs",
|
|
2111
|
+
priority: 50,
|
|
2112
|
+
// Generic, yields to specific frameworks like Laravel
|
|
2113
|
+
async detect(rootDir, relativePath) {
|
|
2114
|
+
const fullPath = path.join(rootDir, relativePath);
|
|
2115
|
+
const result = {
|
|
2116
|
+
detected: false,
|
|
2117
|
+
name: "nodejs",
|
|
2118
|
+
path: relativePath,
|
|
2119
|
+
confidence: "low",
|
|
2120
|
+
evidence: []
|
|
2121
|
+
};
|
|
2122
|
+
const packageJsonPath = path.join(fullPath, "package.json");
|
|
2123
|
+
let packageJson4 = null;
|
|
2124
|
+
try {
|
|
2125
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
2126
|
+
packageJson4 = JSON.parse(content);
|
|
2127
|
+
result.evidence.push("Found package.json");
|
|
2128
|
+
} catch {
|
|
2129
|
+
return result;
|
|
2130
|
+
}
|
|
2131
|
+
result.detected = true;
|
|
2132
|
+
result.confidence = "high";
|
|
2133
|
+
if (packageJson4.devDependencies?.typescript || packageJson4.dependencies?.typescript) {
|
|
2134
|
+
result.evidence.push("TypeScript detected");
|
|
2135
|
+
}
|
|
2136
|
+
const testFrameworks = [
|
|
2137
|
+
{ name: "jest", display: "Jest" },
|
|
2138
|
+
{ name: "vitest", display: "Vitest" },
|
|
2139
|
+
{ name: "mocha", display: "Mocha" },
|
|
2140
|
+
{ name: "ava", display: "AVA" },
|
|
2141
|
+
{ name: "@playwright/test", display: "Playwright" }
|
|
2142
|
+
];
|
|
2143
|
+
for (const framework of testFrameworks) {
|
|
2144
|
+
if (packageJson4.devDependencies?.[framework.name] || packageJson4.dependencies?.[framework.name]) {
|
|
2145
|
+
result.evidence.push(`${framework.display} test framework detected`);
|
|
2146
|
+
break;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
const frameworks = [
|
|
2150
|
+
{ name: "next", display: "Next.js" },
|
|
2151
|
+
{ name: "react", display: "React" },
|
|
2152
|
+
{ name: "vue", display: "Vue" },
|
|
2153
|
+
{ name: "express", display: "Express" },
|
|
2154
|
+
{ name: "@nestjs/core", display: "NestJS" }
|
|
2155
|
+
];
|
|
2156
|
+
for (const fw of frameworks) {
|
|
2157
|
+
if (packageJson4.dependencies?.[fw.name]) {
|
|
2158
|
+
result.evidence.push(`${fw.display} detected`);
|
|
2159
|
+
break;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
if (packageJson4.engines?.node) {
|
|
2163
|
+
result.version = packageJson4.engines.node;
|
|
2164
|
+
}
|
|
2165
|
+
return result;
|
|
2166
|
+
},
|
|
2167
|
+
async generateConfig(rootDir, relativePath) {
|
|
2168
|
+
return generateNodeJsConfig(rootDir, relativePath);
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
|
|
2172
|
+
// src/frameworks/laravel/detector.ts
|
|
2173
|
+
import fs2 from "fs/promises";
|
|
2174
|
+
import path2 from "path";
|
|
2175
|
+
|
|
2176
|
+
// src/frameworks/laravel/config.ts
|
|
2177
|
+
async function generateLaravelConfig(_rootDir, _relativePath) {
|
|
2178
|
+
return {
|
|
2179
|
+
include: [
|
|
2180
|
+
// PHP backend
|
|
2181
|
+
"app/**/*.php",
|
|
2182
|
+
"routes/**/*.php",
|
|
2183
|
+
"config/**/*.php",
|
|
2184
|
+
"database/**/*.php",
|
|
2185
|
+
"resources/**/*.php",
|
|
2186
|
+
"tests/**/*.php",
|
|
2187
|
+
"*.php",
|
|
2188
|
+
// Frontend assets (Vue/React/Inertia)
|
|
2189
|
+
"resources/js/**/*.js",
|
|
2190
|
+
"resources/js/**/*.ts",
|
|
2191
|
+
"resources/js/**/*.jsx",
|
|
2192
|
+
"resources/js/**/*.tsx",
|
|
2193
|
+
"resources/js/**/*.vue",
|
|
2194
|
+
// Blade templates
|
|
2195
|
+
"resources/views/**/*.blade.php",
|
|
2196
|
+
// Documentation
|
|
2197
|
+
"**/*.md",
|
|
2198
|
+
"**/*.mdx",
|
|
2199
|
+
"docs/**/*.md",
|
|
2200
|
+
"README.md",
|
|
2201
|
+
"CHANGELOG.md"
|
|
2202
|
+
],
|
|
2203
|
+
exclude: [
|
|
2204
|
+
"vendor/**",
|
|
2205
|
+
"storage/**",
|
|
2206
|
+
"bootstrap/cache/**",
|
|
2207
|
+
"public/**",
|
|
2208
|
+
"node_modules/**"
|
|
2209
|
+
]
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/frameworks/laravel/detector.ts
|
|
2214
|
+
var laravelDetector = {
|
|
2215
|
+
name: "laravel",
|
|
2216
|
+
priority: 100,
|
|
2217
|
+
// Laravel takes precedence over Node.js
|
|
2218
|
+
async detect(rootDir, relativePath) {
|
|
2219
|
+
const fullPath = path2.join(rootDir, relativePath);
|
|
2220
|
+
const result = {
|
|
2221
|
+
detected: false,
|
|
2222
|
+
name: "laravel",
|
|
2223
|
+
path: relativePath,
|
|
2224
|
+
confidence: "low",
|
|
2225
|
+
evidence: []
|
|
2226
|
+
};
|
|
2227
|
+
const composerJsonPath = path2.join(fullPath, "composer.json");
|
|
2228
|
+
let composerJson = null;
|
|
2229
|
+
try {
|
|
2230
|
+
const content = await fs2.readFile(composerJsonPath, "utf-8");
|
|
2231
|
+
composerJson = JSON.parse(content);
|
|
2232
|
+
result.evidence.push("Found composer.json");
|
|
2233
|
+
} catch {
|
|
2234
|
+
return result;
|
|
2235
|
+
}
|
|
2236
|
+
const hasLaravel = composerJson.require?.["laravel/framework"] || composerJson["require-dev"]?.["laravel/framework"];
|
|
2237
|
+
if (!hasLaravel) {
|
|
2238
|
+
return result;
|
|
2239
|
+
}
|
|
2240
|
+
result.evidence.push("Laravel framework detected in composer.json");
|
|
2241
|
+
const artisanPath = path2.join(fullPath, "artisan");
|
|
2242
|
+
try {
|
|
2243
|
+
await fs2.access(artisanPath);
|
|
2244
|
+
result.evidence.push("Found artisan file");
|
|
2245
|
+
result.confidence = "high";
|
|
2246
|
+
} catch {
|
|
2247
|
+
result.confidence = "medium";
|
|
2248
|
+
}
|
|
2249
|
+
const laravelDirs = ["app", "routes", "config", "database"];
|
|
2250
|
+
let foundDirs = 0;
|
|
2251
|
+
for (const dir of laravelDirs) {
|
|
2252
|
+
try {
|
|
2253
|
+
const dirPath = path2.join(fullPath, dir);
|
|
2254
|
+
const stats = await fs2.stat(dirPath);
|
|
2255
|
+
if (stats.isDirectory()) {
|
|
2256
|
+
foundDirs++;
|
|
2257
|
+
}
|
|
2258
|
+
} catch {
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (foundDirs >= 2) {
|
|
2262
|
+
result.evidence.push(`Laravel directory structure detected (${foundDirs}/${laravelDirs.length} dirs)`);
|
|
2263
|
+
result.confidence = "high";
|
|
2264
|
+
}
|
|
2265
|
+
const testDirsToCheck = [
|
|
2266
|
+
path2.join(fullPath, "tests", "Feature"),
|
|
2267
|
+
path2.join(fullPath, "tests", "Unit")
|
|
2268
|
+
];
|
|
2269
|
+
for (const testDir of testDirsToCheck) {
|
|
2270
|
+
try {
|
|
2271
|
+
const stats = await fs2.stat(testDir);
|
|
2272
|
+
if (stats.isDirectory()) {
|
|
2273
|
+
const dirName = path2.basename(path2.dirname(testDir)) + "/" + path2.basename(testDir);
|
|
2274
|
+
result.evidence.push(`Found ${dirName} test directory`);
|
|
2275
|
+
}
|
|
2276
|
+
} catch {
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
if (composerJson.require?.["laravel/framework"]) {
|
|
2280
|
+
result.version = composerJson.require["laravel/framework"];
|
|
2281
|
+
}
|
|
2282
|
+
result.detected = true;
|
|
2283
|
+
return result;
|
|
2284
|
+
},
|
|
2285
|
+
async generateConfig(rootDir, relativePath) {
|
|
2286
|
+
return generateLaravelConfig(rootDir, relativePath);
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
// src/frameworks/registry.ts
|
|
2291
|
+
var frameworkDetectors = [
|
|
2292
|
+
nodejsDetector,
|
|
2293
|
+
laravelDetector
|
|
2294
|
+
];
|
|
2295
|
+
function getFrameworkDetector(name) {
|
|
2296
|
+
return frameworkDetectors.find((d) => d.name === name);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// src/frameworks/detector-service.ts
|
|
2300
|
+
async function detectAllFrameworks(rootDir, options = {}) {
|
|
2301
|
+
const opts = { ...defaultDetectionOptions, ...options };
|
|
2302
|
+
const results = [];
|
|
2303
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2304
|
+
await detectAtPath(rootDir, ".", results, visited);
|
|
2305
|
+
await scanSubdirectories(rootDir, ".", results, visited, 0, opts);
|
|
2306
|
+
return results;
|
|
2307
|
+
}
|
|
2308
|
+
async function detectAtPath(rootDir, relativePath, results, visited) {
|
|
2309
|
+
const fullPath = path3.join(rootDir, relativePath);
|
|
2310
|
+
if (visited.has(fullPath)) {
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
visited.add(fullPath);
|
|
2314
|
+
const detectedAtPath = [];
|
|
2315
|
+
for (const detector of frameworkDetectors) {
|
|
2316
|
+
try {
|
|
2317
|
+
const result = await detector.detect(rootDir, relativePath);
|
|
2318
|
+
if (result.detected) {
|
|
2319
|
+
detectedAtPath.push({
|
|
2320
|
+
...result,
|
|
2321
|
+
priority: detector.priority ?? 0
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
console.error(`Error running detector '${detector.name}' at ${relativePath}:`, error);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
if (detectedAtPath.length > 1) {
|
|
2329
|
+
detectedAtPath.sort((a, b) => b.priority - a.priority);
|
|
2330
|
+
const winner = detectedAtPath[0];
|
|
2331
|
+
results.push(winner);
|
|
2332
|
+
const skipped = detectedAtPath.slice(1);
|
|
2333
|
+
if (skipped.length > 0) {
|
|
2334
|
+
const skippedNames = skipped.map((d) => d.name).join(", ");
|
|
2335
|
+
console.log(` \u2192 Skipping ${skippedNames} at ${relativePath} (${winner.name} takes precedence)`);
|
|
2336
|
+
}
|
|
2337
|
+
} else if (detectedAtPath.length === 1) {
|
|
2338
|
+
results.push(detectedAtPath[0]);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
async function scanSubdirectories(rootDir, relativePath, results, visited, depth, options) {
|
|
2342
|
+
if (depth >= options.maxDepth) {
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
const fullPath = path3.join(rootDir, relativePath);
|
|
2346
|
+
try {
|
|
2347
|
+
const entries = await fs3.readdir(fullPath, { withFileTypes: true });
|
|
2348
|
+
const dirs = entries.filter((e) => e.isDirectory());
|
|
2349
|
+
for (const dir of dirs) {
|
|
2350
|
+
if (options.skipDirs.includes(dir.name)) {
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
if (dir.name.startsWith(".")) {
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
const subPath = relativePath === "." ? dir.name : path3.join(relativePath, dir.name);
|
|
2357
|
+
await detectAtPath(rootDir, subPath, results, visited);
|
|
2358
|
+
await scanSubdirectories(rootDir, subPath, results, visited, depth + 1, options);
|
|
2359
|
+
}
|
|
2360
|
+
} catch (error) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// src/cli/init.ts
|
|
2366
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2367
|
+
var __dirname2 = path4.dirname(__filename2);
|
|
2368
|
+
async function initCommand(options = {}) {
|
|
2369
|
+
const rootDir = options.path || process.cwd();
|
|
2370
|
+
const configPath = path4.join(rootDir, ".lien.config.json");
|
|
2371
|
+
try {
|
|
2372
|
+
let configExists = false;
|
|
2373
|
+
try {
|
|
2374
|
+
await fs4.access(configPath);
|
|
2375
|
+
configExists = true;
|
|
2376
|
+
} catch {
|
|
2377
|
+
}
|
|
2378
|
+
if (configExists && options.upgrade) {
|
|
2379
|
+
await upgradeConfig(configPath);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (configExists && !options.upgrade) {
|
|
2383
|
+
console.log(chalk2.yellow("\u26A0\uFE0F .lien.config.json already exists"));
|
|
2384
|
+
console.log(chalk2.dim("Run"), chalk2.bold("lien init --upgrade"), chalk2.dim("to merge new config options"));
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
if (!configExists) {
|
|
2388
|
+
await createNewConfig(rootDir, options);
|
|
2389
|
+
}
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
console.error(chalk2.red("Error creating config file:"), error);
|
|
2392
|
+
process.exit(1);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
async function createNewConfig(rootDir, options) {
|
|
2396
|
+
showCompactBanner();
|
|
2397
|
+
console.log(chalk2.bold("Initializing Lien...\n"));
|
|
2398
|
+
console.log(chalk2.dim("\u{1F50D} Detecting frameworks in"), chalk2.bold(rootDir));
|
|
2399
|
+
const detections = await detectAllFrameworks(rootDir);
|
|
2400
|
+
let frameworks = [];
|
|
2401
|
+
if (detections.length === 0) {
|
|
2402
|
+
console.log(chalk2.yellow("\n\u26A0\uFE0F No frameworks detected"));
|
|
2403
|
+
if (!options.yes) {
|
|
2404
|
+
const { useGeneric } = await inquirer.prompt([
|
|
2405
|
+
{
|
|
2406
|
+
type: "confirm",
|
|
2407
|
+
name: "useGeneric",
|
|
2408
|
+
message: "Create a generic config (index all supported file types)?",
|
|
2409
|
+
default: true
|
|
2410
|
+
}
|
|
2411
|
+
]);
|
|
2412
|
+
if (!useGeneric) {
|
|
2413
|
+
console.log(chalk2.dim("Aborted."));
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
frameworks.push({
|
|
2418
|
+
name: "generic",
|
|
2419
|
+
path: ".",
|
|
2420
|
+
enabled: true,
|
|
2421
|
+
config: {
|
|
2422
|
+
include: ["**/*.{ts,tsx,js,jsx,py,go,rs,java,c,cpp,cs}"],
|
|
2423
|
+
exclude: [
|
|
2424
|
+
"**/node_modules/**",
|
|
2425
|
+
"**/dist/**",
|
|
2426
|
+
"**/build/**",
|
|
2427
|
+
"**/.git/**",
|
|
2428
|
+
"**/coverage/**",
|
|
2429
|
+
"**/.next/**",
|
|
2430
|
+
"**/.nuxt/**",
|
|
2431
|
+
"**/vendor/**"
|
|
2432
|
+
]
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
} else {
|
|
2436
|
+
console.log(chalk2.green(`
|
|
2437
|
+
\u2713 Found ${detections.length} framework(s):
|
|
2438
|
+
`));
|
|
2439
|
+
for (const det of detections) {
|
|
2440
|
+
const pathDisplay = det.path === "." ? "root" : det.path;
|
|
2441
|
+
console.log(chalk2.bold(` ${det.name}`), chalk2.dim(`(${det.confidence} confidence)`));
|
|
2442
|
+
console.log(chalk2.dim(` Location: ${pathDisplay}`));
|
|
2443
|
+
if (det.evidence.length > 0) {
|
|
2444
|
+
det.evidence.forEach((e) => {
|
|
2445
|
+
console.log(chalk2.dim(` \u2022 ${e}`));
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
console.log();
|
|
2449
|
+
}
|
|
2450
|
+
if (!options.yes) {
|
|
2451
|
+
const { confirm } = await inquirer.prompt([
|
|
2452
|
+
{
|
|
2453
|
+
type: "confirm",
|
|
2454
|
+
name: "confirm",
|
|
2455
|
+
message: "Configure these frameworks?",
|
|
2456
|
+
default: true
|
|
2457
|
+
}
|
|
2458
|
+
]);
|
|
2459
|
+
if (!confirm) {
|
|
2460
|
+
console.log(chalk2.dim("Aborted."));
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
for (const det of detections) {
|
|
2465
|
+
const detector = getFrameworkDetector(det.name);
|
|
2466
|
+
if (!detector) {
|
|
2467
|
+
console.warn(chalk2.yellow(`\u26A0\uFE0F No detector found for ${det.name}, skipping`));
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
const frameworkConfig = await detector.generateConfig(rootDir, det.path);
|
|
2471
|
+
let shouldCustomize = false;
|
|
2472
|
+
if (!options.yes) {
|
|
2473
|
+
const { customize } = await inquirer.prompt([
|
|
2474
|
+
{
|
|
2475
|
+
type: "confirm",
|
|
2476
|
+
name: "customize",
|
|
2477
|
+
message: `Customize ${det.name} settings?`,
|
|
2478
|
+
default: false
|
|
2479
|
+
}
|
|
2480
|
+
]);
|
|
2481
|
+
shouldCustomize = customize;
|
|
2482
|
+
}
|
|
2483
|
+
let finalConfig = frameworkConfig;
|
|
2484
|
+
if (shouldCustomize) {
|
|
2485
|
+
const customized = await promptForCustomization(det.name, frameworkConfig);
|
|
2486
|
+
finalConfig = { ...frameworkConfig, ...customized };
|
|
2487
|
+
} else {
|
|
2488
|
+
const pathDisplay = det.path === "." ? "root" : det.path;
|
|
2489
|
+
console.log(chalk2.dim(` \u2192 Using defaults for ${det.name} at ${pathDisplay}`));
|
|
2490
|
+
}
|
|
2491
|
+
frameworks.push({
|
|
2492
|
+
name: det.name,
|
|
2493
|
+
path: det.path,
|
|
2494
|
+
enabled: true,
|
|
2495
|
+
config: finalConfig
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
if (!options.yes) {
|
|
2500
|
+
const { installCursorRules } = await inquirer.prompt([
|
|
2501
|
+
{
|
|
2502
|
+
type: "confirm",
|
|
2503
|
+
name: "installCursorRules",
|
|
2504
|
+
message: "Install recommended Cursor rules?",
|
|
2505
|
+
default: true
|
|
2506
|
+
}
|
|
2507
|
+
]);
|
|
2508
|
+
if (installCursorRules) {
|
|
2509
|
+
try {
|
|
2510
|
+
const cursorRulesDir = path4.join(rootDir, ".cursor");
|
|
2511
|
+
await fs4.mkdir(cursorRulesDir, { recursive: true });
|
|
2512
|
+
const templatePath = path4.join(__dirname2, "../CURSOR_RULES_TEMPLATE.md");
|
|
2513
|
+
const rulesPath = path4.join(cursorRulesDir, "rules");
|
|
2514
|
+
let targetPath;
|
|
2515
|
+
let isDirectory = false;
|
|
2516
|
+
let isFile = false;
|
|
2517
|
+
try {
|
|
2518
|
+
const stats = await fs4.stat(rulesPath);
|
|
2519
|
+
isDirectory = stats.isDirectory();
|
|
2520
|
+
isFile = stats.isFile();
|
|
2521
|
+
} catch {
|
|
2522
|
+
}
|
|
2523
|
+
if (isDirectory) {
|
|
2524
|
+
targetPath = path4.join(rulesPath, "lien.mdc");
|
|
2525
|
+
await fs4.copyFile(templatePath, targetPath);
|
|
2526
|
+
console.log(chalk2.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
|
|
2527
|
+
} else if (isFile) {
|
|
2528
|
+
const { convertToDir } = await inquirer.prompt([
|
|
2529
|
+
{
|
|
2530
|
+
type: "confirm",
|
|
2531
|
+
name: "convertToDir",
|
|
2532
|
+
message: "Existing .cursor/rules file found. Convert to directory and preserve your rules?",
|
|
2533
|
+
default: true
|
|
2534
|
+
}
|
|
2535
|
+
]);
|
|
2536
|
+
if (convertToDir) {
|
|
2537
|
+
const existingRules = await fs4.readFile(rulesPath, "utf-8");
|
|
2538
|
+
await fs4.unlink(rulesPath);
|
|
2539
|
+
await fs4.mkdir(rulesPath);
|
|
2540
|
+
await fs4.writeFile(path4.join(rulesPath, "project.mdc"), existingRules);
|
|
2541
|
+
await fs4.copyFile(templatePath, path4.join(rulesPath, "lien.mdc"));
|
|
2542
|
+
console.log(chalk2.green("\u2713 Converted .cursor/rules to directory"));
|
|
2543
|
+
console.log(chalk2.green(" - Your project rules: .cursor/rules/project.mdc"));
|
|
2544
|
+
console.log(chalk2.green(" - Lien rules: .cursor/rules/lien.mdc"));
|
|
2545
|
+
} else {
|
|
2546
|
+
console.log(chalk2.dim("Skipped Cursor rules installation (preserving existing file)"));
|
|
2547
|
+
}
|
|
2548
|
+
} else {
|
|
2549
|
+
await fs4.mkdir(rulesPath, { recursive: true });
|
|
2550
|
+
targetPath = path4.join(rulesPath, "lien.mdc");
|
|
2551
|
+
await fs4.copyFile(templatePath, targetPath);
|
|
2552
|
+
console.log(chalk2.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
|
|
2553
|
+
}
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
console.log(chalk2.yellow("\u26A0\uFE0F Could not install Cursor rules"));
|
|
2556
|
+
console.log(chalk2.dim(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
2557
|
+
console.log(chalk2.dim("You can manually copy CURSOR_RULES_TEMPLATE.md to .cursor/rules/lien.mdc"));
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
const config = {
|
|
2562
|
+
...defaultConfig,
|
|
2563
|
+
frameworks
|
|
2564
|
+
};
|
|
2565
|
+
const configPath = path4.join(rootDir, ".lien.config.json");
|
|
2566
|
+
await fs4.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2567
|
+
console.log(chalk2.green("\n\u2713 Created .lien.config.json"));
|
|
2568
|
+
console.log(chalk2.green(`\u2713 Configured ${frameworks.length} framework(s)`));
|
|
2569
|
+
console.log(chalk2.dim("\nNext steps:"));
|
|
2570
|
+
console.log(chalk2.dim(" 1. Run"), chalk2.bold("lien index"), chalk2.dim("to index your codebase"));
|
|
2571
|
+
console.log(chalk2.dim(" 2. Run"), chalk2.bold("lien serve"), chalk2.dim("to start the MCP server"));
|
|
2572
|
+
console.log(chalk2.dim(" 3. Configure Cursor to use the MCP server (see README.md)"));
|
|
2573
|
+
}
|
|
2574
|
+
async function promptForCustomization(frameworkName, config) {
|
|
2575
|
+
console.log(chalk2.bold(`
|
|
2576
|
+
Customizing ${frameworkName} settings:`));
|
|
2577
|
+
const answers = await inquirer.prompt([
|
|
2578
|
+
{
|
|
2579
|
+
type: "input",
|
|
2580
|
+
name: "include",
|
|
2581
|
+
message: "File patterns to include (comma-separated):",
|
|
2582
|
+
default: config.include.join(", "),
|
|
2583
|
+
filter: (input) => input.split(",").map((s) => s.trim())
|
|
2584
|
+
},
|
|
2585
|
+
{
|
|
2586
|
+
type: "input",
|
|
2587
|
+
name: "exclude",
|
|
2588
|
+
message: "File patterns to exclude (comma-separated):",
|
|
2589
|
+
default: config.exclude.join(", "),
|
|
2590
|
+
filter: (input) => input.split(",").map((s) => s.trim())
|
|
2591
|
+
}
|
|
2592
|
+
]);
|
|
2593
|
+
return {
|
|
2594
|
+
include: answers.include,
|
|
2595
|
+
exclude: answers.exclude
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
async function upgradeConfig(configPath) {
|
|
2599
|
+
try {
|
|
2600
|
+
const backupPath = `${configPath}.backup`;
|
|
2601
|
+
await fs4.copyFile(configPath, backupPath);
|
|
2602
|
+
const existingContent = await fs4.readFile(configPath, "utf-8");
|
|
2603
|
+
const existingConfig = JSON.parse(existingContent);
|
|
2604
|
+
let upgradedConfig;
|
|
2605
|
+
let migrated = false;
|
|
2606
|
+
if (needsMigration(existingConfig)) {
|
|
2607
|
+
console.log(chalk2.blue("\u{1F504} Migrating config from v0.2.0 to v0.3.0..."));
|
|
2608
|
+
upgradedConfig = migrateConfig(existingConfig);
|
|
2609
|
+
migrated = true;
|
|
2610
|
+
} else {
|
|
2611
|
+
const newFields = detectNewFields(existingConfig, defaultConfig);
|
|
2612
|
+
upgradedConfig = deepMergeConfig(defaultConfig, existingConfig);
|
|
2613
|
+
if (newFields.length > 0) {
|
|
2614
|
+
console.log(chalk2.dim("\nNew options added:"));
|
|
2615
|
+
newFields.forEach((field) => console.log(chalk2.dim(" \u2022"), chalk2.bold(field)));
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
await fs4.writeFile(
|
|
2619
|
+
configPath,
|
|
2620
|
+
JSON.stringify(upgradedConfig, null, 2) + "\n",
|
|
2621
|
+
"utf-8"
|
|
2622
|
+
);
|
|
2623
|
+
console.log(chalk2.green("\u2713 Config upgraded successfully"));
|
|
2624
|
+
console.log(chalk2.dim("Backup saved to:"), backupPath);
|
|
2625
|
+
if (migrated) {
|
|
2626
|
+
console.log(chalk2.dim("\n\u{1F4DD} Your config now uses the framework-based structure."));
|
|
2627
|
+
}
|
|
2628
|
+
} catch (error) {
|
|
2629
|
+
console.error(chalk2.red("Error upgrading config:"), error);
|
|
2630
|
+
throw error;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// src/cli/status.ts
|
|
2635
|
+
init_service();
|
|
2636
|
+
import chalk3 from "chalk";
|
|
2637
|
+
import fs8 from "fs/promises";
|
|
2638
|
+
import path8 from "path";
|
|
2639
|
+
import os from "os";
|
|
2640
|
+
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
|
+
init_schema();
|
|
2724
|
+
async function statusCommand() {
|
|
2725
|
+
const rootDir = process.cwd();
|
|
2726
|
+
const projectName = path8.basename(rootDir);
|
|
2727
|
+
const pathHash = crypto.createHash("md5").update(rootDir).digest("hex").substring(0, 8);
|
|
2728
|
+
const indexPath = path8.join(os.homedir(), ".lien", "indices", `${projectName}-${pathHash}`);
|
|
2729
|
+
showCompactBanner();
|
|
2730
|
+
console.log(chalk3.bold("Status\n"));
|
|
2731
|
+
const hasConfig = await configService.exists(rootDir);
|
|
2732
|
+
console.log(chalk3.dim("Configuration:"), hasConfig ? chalk3.green("\u2713 Found") : chalk3.red("\u2717 Not initialized"));
|
|
2733
|
+
if (!hasConfig) {
|
|
2734
|
+
console.log(chalk3.yellow("\nRun"), chalk3.bold("lien init"), chalk3.yellow("to initialize"));
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
try {
|
|
2738
|
+
const stats = await fs8.stat(indexPath);
|
|
2739
|
+
console.log(chalk3.dim("Index location:"), indexPath);
|
|
2740
|
+
console.log(chalk3.dim("Index status:"), chalk3.green("\u2713 Exists"));
|
|
2741
|
+
try {
|
|
2742
|
+
const files = await fs8.readdir(indexPath, { recursive: true });
|
|
2743
|
+
console.log(chalk3.dim("Index files:"), files.length);
|
|
2744
|
+
} catch (e) {
|
|
2745
|
+
}
|
|
2746
|
+
console.log(chalk3.dim("Last modified:"), stats.mtime.toLocaleString());
|
|
2747
|
+
try {
|
|
2748
|
+
const version = await readVersionFile(indexPath);
|
|
2749
|
+
if (version > 0) {
|
|
2750
|
+
const versionDate = new Date(version);
|
|
2751
|
+
console.log(chalk3.dim("Last reindex:"), versionDate.toLocaleString());
|
|
2752
|
+
}
|
|
2753
|
+
} catch {
|
|
2754
|
+
}
|
|
2755
|
+
} catch (error) {
|
|
2756
|
+
console.log(chalk3.dim("Index status:"), chalk3.yellow("\u2717 Not indexed"));
|
|
2757
|
+
console.log(chalk3.yellow("\nRun"), chalk3.bold("lien index"), chalk3.yellow("to index your codebase"));
|
|
2758
|
+
}
|
|
2759
|
+
try {
|
|
2760
|
+
const config = await configService.load(rootDir);
|
|
2761
|
+
console.log(chalk3.bold("\nFeatures:"));
|
|
2762
|
+
const isRepo = await isGitRepo(rootDir);
|
|
2763
|
+
if (config.gitDetection.enabled && isRepo) {
|
|
2764
|
+
console.log(chalk3.dim("Git detection:"), chalk3.green("\u2713 Enabled"));
|
|
2765
|
+
console.log(chalk3.dim(" Poll interval:"), `${config.gitDetection.pollIntervalMs / 1e3}s`);
|
|
2766
|
+
try {
|
|
2767
|
+
const branch = await getCurrentBranch(rootDir);
|
|
2768
|
+
const commit = await getCurrentCommit(rootDir);
|
|
2769
|
+
console.log(chalk3.dim(" Current branch:"), branch);
|
|
2770
|
+
console.log(chalk3.dim(" Current commit:"), commit.substring(0, 8));
|
|
2771
|
+
const gitStateFile = path8.join(indexPath, ".git-state.json");
|
|
2772
|
+
try {
|
|
2773
|
+
const gitStateContent = await fs8.readFile(gitStateFile, "utf-8");
|
|
2774
|
+
const gitState = JSON.parse(gitStateContent);
|
|
2775
|
+
if (gitState.branch !== branch || gitState.commit !== commit) {
|
|
2776
|
+
console.log(chalk3.yellow(" \u26A0\uFE0F Git state changed - will reindex on next serve"));
|
|
2777
|
+
}
|
|
2778
|
+
} catch {
|
|
2779
|
+
}
|
|
2780
|
+
} catch {
|
|
2781
|
+
}
|
|
2782
|
+
} else if (config.gitDetection.enabled && !isRepo) {
|
|
2783
|
+
console.log(chalk3.dim("Git detection:"), chalk3.yellow("Enabled (not a git repo)"));
|
|
2784
|
+
} else {
|
|
2785
|
+
console.log(chalk3.dim("Git detection:"), chalk3.gray("Disabled"));
|
|
2786
|
+
}
|
|
2787
|
+
if (config.fileWatching.enabled) {
|
|
2788
|
+
console.log(chalk3.dim("File watching:"), chalk3.green("\u2713 Enabled"));
|
|
2789
|
+
console.log(chalk3.dim(" Debounce:"), `${config.fileWatching.debounceMs}ms`);
|
|
2790
|
+
} else {
|
|
2791
|
+
console.log(chalk3.dim("File watching:"), chalk3.gray("Disabled"));
|
|
2792
|
+
console.log(chalk3.dim(" Enable with:"), chalk3.bold("lien serve --watch"));
|
|
2793
|
+
}
|
|
2794
|
+
console.log(chalk3.bold("\nIndexing Settings:"));
|
|
2795
|
+
if (isModernConfig(config)) {
|
|
2796
|
+
console.log(chalk3.dim("Concurrency:"), config.core.concurrency);
|
|
2797
|
+
console.log(chalk3.dim("Batch size:"), config.core.embeddingBatchSize);
|
|
2798
|
+
console.log(chalk3.dim("Chunk size:"), config.core.chunkSize);
|
|
2799
|
+
console.log(chalk3.dim("Chunk overlap:"), config.core.chunkOverlap);
|
|
2800
|
+
}
|
|
2801
|
+
} catch (error) {
|
|
2802
|
+
console.log(chalk3.yellow("\nWarning: Could not load configuration"));
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// src/cli/index-cmd.ts
|
|
2807
|
+
init_indexer();
|
|
2808
|
+
init_banner();
|
|
2809
|
+
import chalk5 from "chalk";
|
|
2810
|
+
async function indexCommand(options) {
|
|
2811
|
+
showCompactBanner();
|
|
2812
|
+
try {
|
|
2813
|
+
await indexCodebase({
|
|
2814
|
+
rootDir: process.cwd(),
|
|
2815
|
+
verbose: options.verbose || false
|
|
2816
|
+
});
|
|
2817
|
+
if (options.watch) {
|
|
2818
|
+
console.log(chalk5.yellow("\n\u26A0\uFE0F Watch mode not yet implemented"));
|
|
2819
|
+
}
|
|
2820
|
+
} catch (error) {
|
|
2821
|
+
console.error(chalk5.red("Error during indexing:"), error);
|
|
2822
|
+
process.exit(1);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
// src/cli/serve.ts
|
|
2827
|
+
import chalk6 from "chalk";
|
|
2828
|
+
|
|
2829
|
+
// src/mcp/server.ts
|
|
2830
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2831
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2832
|
+
import {
|
|
2833
|
+
CallToolRequestSchema,
|
|
2834
|
+
ListToolsRequestSchema
|
|
2835
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
2836
|
+
import { createRequire as createRequire2 } from "module";
|
|
2837
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2838
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
2839
|
+
|
|
2840
|
+
// src/mcp/tools.ts
|
|
2841
|
+
var tools = [
|
|
2842
|
+
{
|
|
2843
|
+
name: "semantic_search",
|
|
2844
|
+
description: "Search the codebase semantically for relevant code using natural language. Results include a relevance category (highly_relevant, relevant, loosely_related, not_relevant) based on semantic similarity.",
|
|
2845
|
+
inputSchema: {
|
|
2846
|
+
type: "object",
|
|
2847
|
+
properties: {
|
|
2848
|
+
query: {
|
|
2849
|
+
type: "string",
|
|
2850
|
+
description: 'Natural language search query (e.g., "authentication logic", "database connection handling")'
|
|
2851
|
+
},
|
|
2852
|
+
limit: {
|
|
2853
|
+
type: "number",
|
|
2854
|
+
description: "Maximum number of results to return",
|
|
2855
|
+
default: 5
|
|
2856
|
+
}
|
|
2857
|
+
},
|
|
2858
|
+
required: ["query"]
|
|
2859
|
+
}
|
|
2860
|
+
},
|
|
2861
|
+
{
|
|
2862
|
+
name: "find_similar",
|
|
2863
|
+
description: "Find code similar to a given code snippet. Results include a relevance category (highly_relevant, relevant, loosely_related, not_relevant) based on semantic similarity.",
|
|
2864
|
+
inputSchema: {
|
|
2865
|
+
type: "object",
|
|
2866
|
+
properties: {
|
|
2867
|
+
code: {
|
|
2868
|
+
type: "string",
|
|
2869
|
+
description: "Code snippet to find similar implementations"
|
|
2870
|
+
},
|
|
2871
|
+
limit: {
|
|
2872
|
+
type: "number",
|
|
2873
|
+
description: "Maximum number of results to return",
|
|
2874
|
+
default: 5
|
|
2875
|
+
}
|
|
2876
|
+
},
|
|
2877
|
+
required: ["code"]
|
|
2878
|
+
}
|
|
2879
|
+
},
|
|
2880
|
+
{
|
|
2881
|
+
name: "get_file_context",
|
|
2882
|
+
description: "Get all chunks and related context for a specific file. Results include a relevance category (highly_relevant, relevant, loosely_related, not_relevant) based on semantic similarity.",
|
|
2883
|
+
inputSchema: {
|
|
2884
|
+
type: "object",
|
|
2885
|
+
properties: {
|
|
2886
|
+
filepath: {
|
|
2887
|
+
type: "string",
|
|
2888
|
+
description: "Path to the file (relative to project root)"
|
|
2889
|
+
},
|
|
2890
|
+
includeRelated: {
|
|
2891
|
+
type: "boolean",
|
|
2892
|
+
description: "Include semantically related chunks from other files",
|
|
2893
|
+
default: true
|
|
2894
|
+
}
|
|
2895
|
+
},
|
|
2896
|
+
required: ["filepath"]
|
|
2897
|
+
}
|
|
2898
|
+
},
|
|
2899
|
+
{
|
|
2900
|
+
name: "list_functions",
|
|
2901
|
+
description: "List functions, classes, and interfaces by name pattern and language",
|
|
2902
|
+
inputSchema: {
|
|
2903
|
+
type: "object",
|
|
2904
|
+
properties: {
|
|
2905
|
+
pattern: {
|
|
2906
|
+
type: "string",
|
|
2907
|
+
description: 'Regex pattern to match symbol names (e.g., ".*Service$", "handle.*")'
|
|
2908
|
+
},
|
|
2909
|
+
language: {
|
|
2910
|
+
type: "string",
|
|
2911
|
+
description: 'Language filter (e.g., "typescript", "python", "php")'
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
];
|
|
2917
|
+
|
|
2918
|
+
// src/mcp/server.ts
|
|
2919
|
+
init_lancedb();
|
|
2920
|
+
init_local();
|
|
2921
|
+
|
|
2922
|
+
// src/git/tracker.ts
|
|
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
|
|
3161
|
+
init_service();
|
|
3162
|
+
|
|
3163
|
+
// src/watcher/index.ts
|
|
3164
|
+
init_schema();
|
|
3165
|
+
import chokidar from "chokidar";
|
|
3166
|
+
var FileWatcher = class {
|
|
3167
|
+
watcher = null;
|
|
3168
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
3169
|
+
config;
|
|
3170
|
+
rootDir;
|
|
3171
|
+
onChangeHandler = null;
|
|
3172
|
+
constructor(rootDir, config) {
|
|
3173
|
+
this.rootDir = rootDir;
|
|
3174
|
+
this.config = config;
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Starts watching files for changes.
|
|
3178
|
+
*
|
|
3179
|
+
* @param handler - Callback function called when files change
|
|
3180
|
+
*/
|
|
3181
|
+
async start(handler) {
|
|
3182
|
+
if (this.watcher) {
|
|
3183
|
+
throw new Error("File watcher is already running");
|
|
3184
|
+
}
|
|
3185
|
+
this.onChangeHandler = handler;
|
|
3186
|
+
let includePatterns;
|
|
3187
|
+
let excludePatterns;
|
|
3188
|
+
if (isLegacyConfig(this.config)) {
|
|
3189
|
+
includePatterns = this.config.indexing.include;
|
|
3190
|
+
excludePatterns = this.config.indexing.exclude;
|
|
3191
|
+
} else if (isModernConfig(this.config)) {
|
|
3192
|
+
includePatterns = this.config.frameworks.flatMap((f) => f.config.include);
|
|
3193
|
+
excludePatterns = this.config.frameworks.flatMap((f) => f.config.exclude);
|
|
3194
|
+
} else {
|
|
3195
|
+
includePatterns = ["**/*"];
|
|
3196
|
+
excludePatterns = [];
|
|
3197
|
+
}
|
|
3198
|
+
this.watcher = chokidar.watch(includePatterns, {
|
|
3199
|
+
cwd: this.rootDir,
|
|
3200
|
+
ignored: excludePatterns,
|
|
3201
|
+
persistent: true,
|
|
3202
|
+
ignoreInitial: true,
|
|
3203
|
+
// Don't trigger for existing files
|
|
3204
|
+
awaitWriteFinish: {
|
|
3205
|
+
stabilityThreshold: 500,
|
|
3206
|
+
// Wait 500ms for file to stop changing
|
|
3207
|
+
pollInterval: 100
|
|
3208
|
+
},
|
|
3209
|
+
// Performance optimizations
|
|
3210
|
+
usePolling: false,
|
|
3211
|
+
interval: 100,
|
|
3212
|
+
binaryInterval: 300
|
|
3213
|
+
});
|
|
3214
|
+
this.watcher.on("add", (filepath) => this.handleChange("add", filepath)).on("change", (filepath) => this.handleChange("change", filepath)).on("unlink", (filepath) => this.handleChange("unlink", filepath)).on("error", (error) => {
|
|
3215
|
+
console.error(`[Lien] File watcher error: ${error}`);
|
|
3216
|
+
});
|
|
3217
|
+
await new Promise((resolve) => {
|
|
3218
|
+
this.watcher.on("ready", () => {
|
|
3219
|
+
resolve();
|
|
3220
|
+
});
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
/**
|
|
3224
|
+
* Handles a file change event with debouncing.
|
|
3225
|
+
* Debouncing prevents rapid reindexing when files are saved multiple times quickly.
|
|
3226
|
+
*/
|
|
3227
|
+
handleChange(type, filepath) {
|
|
3228
|
+
const existingTimer = this.debounceTimers.get(filepath);
|
|
3229
|
+
if (existingTimer) {
|
|
3230
|
+
clearTimeout(existingTimer);
|
|
3231
|
+
}
|
|
3232
|
+
const timer = setTimeout(() => {
|
|
3233
|
+
this.debounceTimers.delete(filepath);
|
|
3234
|
+
if (this.onChangeHandler) {
|
|
3235
|
+
const absolutePath = filepath.startsWith("/") ? filepath : `${this.rootDir}/${filepath}`;
|
|
3236
|
+
try {
|
|
3237
|
+
const result = this.onChangeHandler({
|
|
3238
|
+
type,
|
|
3239
|
+
filepath: absolutePath
|
|
3240
|
+
});
|
|
3241
|
+
if (result instanceof Promise) {
|
|
3242
|
+
result.catch((error) => {
|
|
3243
|
+
console.error(`[Lien] Error handling file change: ${error}`);
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
} catch (error) {
|
|
3247
|
+
console.error(`[Lien] Error handling file change: ${error}`);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}, this.config.fileWatching.debounceMs);
|
|
3251
|
+
this.debounceTimers.set(filepath, timer);
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Stops the file watcher and cleans up resources.
|
|
3255
|
+
*/
|
|
3256
|
+
async stop() {
|
|
3257
|
+
if (!this.watcher) {
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
for (const timer of this.debounceTimers.values()) {
|
|
3261
|
+
clearTimeout(timer);
|
|
3262
|
+
}
|
|
3263
|
+
this.debounceTimers.clear();
|
|
3264
|
+
await this.watcher.close();
|
|
3265
|
+
this.watcher = null;
|
|
3266
|
+
this.onChangeHandler = null;
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Gets the list of files currently being watched.
|
|
3270
|
+
*/
|
|
3271
|
+
getWatchedFiles() {
|
|
3272
|
+
if (!this.watcher) {
|
|
3273
|
+
return [];
|
|
3274
|
+
}
|
|
3275
|
+
const watched = this.watcher.getWatched();
|
|
3276
|
+
const files = [];
|
|
3277
|
+
for (const [dir, filenames] of Object.entries(watched)) {
|
|
3278
|
+
for (const filename of filenames) {
|
|
3279
|
+
files.push(`${dir}/${filename}`);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return files;
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Checks if the watcher is currently running.
|
|
3286
|
+
*/
|
|
3287
|
+
isRunning() {
|
|
3288
|
+
return this.watcher !== null;
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
|
|
3292
|
+
// src/mcp/server.ts
|
|
3293
|
+
init_constants();
|
|
3294
|
+
var __filename3 = fileURLToPath3(import.meta.url);
|
|
3295
|
+
var __dirname3 = dirname2(__filename3);
|
|
3296
|
+
var require3 = createRequire2(import.meta.url);
|
|
3297
|
+
var packageJson2;
|
|
3298
|
+
try {
|
|
3299
|
+
packageJson2 = require3(join2(__dirname3, "../package.json"));
|
|
3300
|
+
} catch {
|
|
3301
|
+
packageJson2 = require3(join2(__dirname3, "../../package.json"));
|
|
3302
|
+
}
|
|
3303
|
+
async function startMCPServer(options) {
|
|
3304
|
+
const { rootDir, verbose, watch } = options;
|
|
3305
|
+
const log = (message) => {
|
|
3306
|
+
if (verbose) {
|
|
3307
|
+
console.error(`[Lien MCP] ${message}`);
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
log("Initializing MCP server...");
|
|
3311
|
+
const embeddings = new LocalEmbeddings();
|
|
3312
|
+
const vectorDB = new VectorDB(rootDir);
|
|
3313
|
+
try {
|
|
3314
|
+
log("Loading embedding model...");
|
|
3315
|
+
await embeddings.initialize();
|
|
3316
|
+
log("Loading vector database...");
|
|
3317
|
+
await vectorDB.initialize();
|
|
3318
|
+
log("Embeddings and vector DB ready");
|
|
3319
|
+
} catch (error) {
|
|
3320
|
+
console.error(`Failed to initialize: ${error}`);
|
|
3321
|
+
process.exit(1);
|
|
3322
|
+
}
|
|
3323
|
+
const server = new Server(
|
|
3324
|
+
{
|
|
3325
|
+
name: "lien",
|
|
3326
|
+
version: packageJson2.version
|
|
3327
|
+
},
|
|
3328
|
+
{
|
|
3329
|
+
capabilities: {
|
|
3330
|
+
tools: {}
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
);
|
|
3334
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3335
|
+
tools
|
|
3336
|
+
}));
|
|
3337
|
+
const checkAndReconnect = async () => {
|
|
3338
|
+
try {
|
|
3339
|
+
const versionChanged = await vectorDB.checkVersion();
|
|
3340
|
+
if (versionChanged) {
|
|
3341
|
+
log("Index version changed, reconnecting to database...");
|
|
3342
|
+
await vectorDB.reconnect();
|
|
3343
|
+
log("Reconnected to updated index");
|
|
3344
|
+
}
|
|
3345
|
+
} catch (error) {
|
|
3346
|
+
log(`Version check failed: ${error}`);
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
const getIndexMetadata = () => ({
|
|
3350
|
+
indexVersion: vectorDB.getCurrentVersion(),
|
|
3351
|
+
indexDate: vectorDB.getVersionDate()
|
|
3352
|
+
});
|
|
3353
|
+
const versionCheckInterval = setInterval(async () => {
|
|
3354
|
+
await checkAndReconnect();
|
|
3355
|
+
}, 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
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3365
|
+
const { name, arguments: args } = request.params;
|
|
3366
|
+
try {
|
|
3367
|
+
log(`Handling tool call: ${name}`);
|
|
3368
|
+
switch (name) {
|
|
3369
|
+
case "semantic_search": {
|
|
3370
|
+
const query = args?.query;
|
|
3371
|
+
const limit = args?.limit || 5;
|
|
3372
|
+
log(`Searching for: "${query}"`);
|
|
3373
|
+
await checkAndReconnect();
|
|
3374
|
+
const queryEmbedding = await embeddings.embed(query);
|
|
3375
|
+
const results = await vectorDB.search(queryEmbedding, limit, query);
|
|
3376
|
+
log(`Found ${results.length} results`);
|
|
3377
|
+
const response = {
|
|
3378
|
+
indexInfo: getIndexMetadata(),
|
|
3379
|
+
results
|
|
3380
|
+
};
|
|
3381
|
+
return {
|
|
3382
|
+
content: [
|
|
3383
|
+
{
|
|
3384
|
+
type: "text",
|
|
3385
|
+
text: JSON.stringify(response, null, 2)
|
|
3386
|
+
}
|
|
3387
|
+
]
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
case "find_similar": {
|
|
3391
|
+
const code = args?.code;
|
|
3392
|
+
const limit = args?.limit || 5;
|
|
3393
|
+
log(`Finding similar code...`);
|
|
3394
|
+
await checkAndReconnect();
|
|
3395
|
+
const codeEmbedding = await embeddings.embed(code);
|
|
3396
|
+
const results = await vectorDB.search(codeEmbedding, limit, code);
|
|
3397
|
+
log(`Found ${results.length} similar chunks`);
|
|
3398
|
+
const response = {
|
|
3399
|
+
indexInfo: getIndexMetadata(),
|
|
3400
|
+
results
|
|
3401
|
+
};
|
|
3402
|
+
return {
|
|
3403
|
+
content: [
|
|
3404
|
+
{
|
|
3405
|
+
type: "text",
|
|
3406
|
+
text: JSON.stringify(response, null, 2)
|
|
3407
|
+
}
|
|
3408
|
+
]
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
case "get_file_context": {
|
|
3412
|
+
const filepath = args?.filepath;
|
|
3413
|
+
const includeRelated = args?.includeRelated ?? true;
|
|
3414
|
+
log(`Getting context for: ${filepath}`);
|
|
3415
|
+
await checkAndReconnect();
|
|
3416
|
+
const fileEmbedding = await embeddings.embed(filepath);
|
|
3417
|
+
const allResults = await vectorDB.search(fileEmbedding, 50, filepath);
|
|
3418
|
+
const fileChunks = allResults.filter(
|
|
3419
|
+
(r) => r.metadata.file.includes(filepath) || filepath.includes(r.metadata.file)
|
|
3420
|
+
);
|
|
3421
|
+
let results = fileChunks;
|
|
3422
|
+
if (includeRelated && fileChunks.length > 0) {
|
|
3423
|
+
const relatedEmbedding = await embeddings.embed(fileChunks[0].content);
|
|
3424
|
+
const related = await vectorDB.search(relatedEmbedding, 5, fileChunks[0].content);
|
|
3425
|
+
const relatedOtherFiles = related.filter(
|
|
3426
|
+
(r) => !r.metadata.file.includes(filepath) && !filepath.includes(r.metadata.file)
|
|
3427
|
+
);
|
|
3428
|
+
results = [...fileChunks, ...relatedOtherFiles];
|
|
3429
|
+
}
|
|
3430
|
+
log(`Found ${results.length} chunks`);
|
|
3431
|
+
const response = {
|
|
3432
|
+
indexInfo: getIndexMetadata(),
|
|
3433
|
+
file: filepath,
|
|
3434
|
+
chunks: results
|
|
3435
|
+
};
|
|
3436
|
+
return {
|
|
3437
|
+
content: [
|
|
3438
|
+
{
|
|
3439
|
+
type: "text",
|
|
3440
|
+
text: JSON.stringify(response, null, 2)
|
|
3441
|
+
}
|
|
3442
|
+
]
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
case "list_functions": {
|
|
3446
|
+
const pattern = args?.pattern;
|
|
3447
|
+
const language = args?.language;
|
|
3448
|
+
log("Listing functions with symbol metadata...");
|
|
3449
|
+
await checkAndReconnect();
|
|
3450
|
+
let results;
|
|
3451
|
+
let usedMethod = "symbols";
|
|
3452
|
+
try {
|
|
3453
|
+
results = await vectorDB.querySymbols({
|
|
3454
|
+
language,
|
|
3455
|
+
pattern,
|
|
3456
|
+
limit: 50
|
|
3457
|
+
});
|
|
3458
|
+
if (results.length === 0 && (language || pattern)) {
|
|
3459
|
+
log("No symbol results, falling back to content scan...");
|
|
3460
|
+
results = await vectorDB.scanWithFilter({
|
|
3461
|
+
language,
|
|
3462
|
+
pattern,
|
|
3463
|
+
limit: 50
|
|
3464
|
+
});
|
|
3465
|
+
usedMethod = "content";
|
|
3466
|
+
}
|
|
3467
|
+
} catch (error) {
|
|
3468
|
+
log(`Symbol query failed, falling back to content scan: ${error}`);
|
|
3469
|
+
results = await vectorDB.scanWithFilter({
|
|
3470
|
+
language,
|
|
3471
|
+
pattern,
|
|
3472
|
+
limit: 50
|
|
3473
|
+
});
|
|
3474
|
+
usedMethod = "content";
|
|
3475
|
+
}
|
|
3476
|
+
log(`Found ${results.length} matches using ${usedMethod} method`);
|
|
3477
|
+
const response = {
|
|
3478
|
+
indexInfo: getIndexMetadata(),
|
|
3479
|
+
method: usedMethod,
|
|
3480
|
+
results,
|
|
3481
|
+
note: usedMethod === "content" ? 'Using content search. Run "lien reindex" to enable faster symbol-based queries.' : void 0
|
|
3482
|
+
};
|
|
3483
|
+
return {
|
|
3484
|
+
content: [
|
|
3485
|
+
{
|
|
3486
|
+
type: "text",
|
|
3487
|
+
text: JSON.stringify(response, null, 2)
|
|
3488
|
+
}
|
|
3489
|
+
]
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
default:
|
|
3493
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
3494
|
+
}
|
|
3495
|
+
} catch (error) {
|
|
3496
|
+
console.error(`Error handling tool call ${name}:`, error);
|
|
3497
|
+
return {
|
|
3498
|
+
content: [
|
|
3499
|
+
{
|
|
3500
|
+
type: "text",
|
|
3501
|
+
text: JSON.stringify({
|
|
3502
|
+
error: String(error),
|
|
3503
|
+
tool: name
|
|
3504
|
+
})
|
|
3505
|
+
}
|
|
3506
|
+
],
|
|
3507
|
+
isError: true
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
const config = await configService.load(rootDir);
|
|
3512
|
+
const hasIndex = await vectorDB.hasData();
|
|
3513
|
+
if (!hasIndex && config.mcp.autoIndexOnFirstRun) {
|
|
3514
|
+
log("\u{1F4E6} No index found - running initial indexing...");
|
|
3515
|
+
log("\u23F1\uFE0F This may take 5-20 minutes depending on project size");
|
|
3516
|
+
try {
|
|
3517
|
+
const { indexCodebase: indexCodebase2 } = await Promise.resolve().then(() => (init_indexer(), indexer_exports));
|
|
3518
|
+
await indexCodebase2({ rootDir, verbose: true });
|
|
3519
|
+
log("\u2705 Initial indexing complete!");
|
|
3520
|
+
} catch (error) {
|
|
3521
|
+
log(`\u26A0\uFE0F Initial indexing failed: ${error}`);
|
|
3522
|
+
log("You can manually run: lien index");
|
|
3523
|
+
}
|
|
3524
|
+
} else if (!hasIndex) {
|
|
3525
|
+
log("\u26A0\uFE0F No index found. Auto-indexing is disabled in config.");
|
|
3526
|
+
log('Run "lien index" to index your codebase.');
|
|
3527
|
+
}
|
|
3528
|
+
let gitTracker = null;
|
|
3529
|
+
let gitPollInterval = null;
|
|
3530
|
+
let fileWatcher = null;
|
|
3531
|
+
if (config.gitDetection.enabled) {
|
|
3532
|
+
const gitAvailable = await isGitAvailable();
|
|
3533
|
+
const isRepo = await isGitRepo(rootDir);
|
|
3534
|
+
if (gitAvailable && isRepo) {
|
|
3535
|
+
log("\u2713 Detected git repository");
|
|
3536
|
+
gitTracker = new GitStateTracker(rootDir, vectorDB.dbPath);
|
|
3537
|
+
try {
|
|
3538
|
+
log("Checking for git changes...");
|
|
3539
|
+
const changedFiles = await gitTracker.initialize();
|
|
3540
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
3541
|
+
log(`\u{1F33F} Git changes detected: ${changedFiles.length} files changed`);
|
|
3542
|
+
log("Reindexing changed files...");
|
|
3543
|
+
const count = await indexMultipleFiles(
|
|
3544
|
+
changedFiles,
|
|
3545
|
+
vectorDB,
|
|
3546
|
+
embeddings,
|
|
3547
|
+
config,
|
|
3548
|
+
{ verbose }
|
|
3549
|
+
);
|
|
3550
|
+
log(`\u2713 Reindexed ${count} files`);
|
|
3551
|
+
} else {
|
|
3552
|
+
log("\u2713 Index is up to date with git state");
|
|
3553
|
+
}
|
|
3554
|
+
} catch (error) {
|
|
3555
|
+
log(`Warning: Failed to check git state on startup: ${error}`);
|
|
3556
|
+
}
|
|
3557
|
+
log(`\u2713 Git detection enabled (checking every ${config.gitDetection.pollIntervalMs / 1e3}s)`);
|
|
3558
|
+
gitPollInterval = setInterval(async () => {
|
|
3559
|
+
try {
|
|
3560
|
+
const changedFiles = await gitTracker.detectChanges();
|
|
3561
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
3562
|
+
log(`\u{1F33F} Git change detected: ${changedFiles.length} files changed`);
|
|
3563
|
+
log("Reindexing in background...");
|
|
3564
|
+
indexMultipleFiles(
|
|
3565
|
+
changedFiles,
|
|
3566
|
+
vectorDB,
|
|
3567
|
+
embeddings,
|
|
3568
|
+
config,
|
|
3569
|
+
{ verbose }
|
|
3570
|
+
).then((count) => {
|
|
3571
|
+
log(`\u2713 Background reindex complete: ${count} files`);
|
|
3572
|
+
}).catch((error) => {
|
|
3573
|
+
log(`Warning: Background reindex failed: ${error}`);
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
} catch (error) {
|
|
3577
|
+
log(`Warning: Git detection check failed: ${error}`);
|
|
3578
|
+
}
|
|
3579
|
+
}, config.gitDetection.pollIntervalMs);
|
|
3580
|
+
} else {
|
|
3581
|
+
if (!gitAvailable) {
|
|
3582
|
+
log("Git not available - git detection disabled");
|
|
3583
|
+
} else if (!isRepo) {
|
|
3584
|
+
log("Not a git repository - git detection disabled");
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
} else {
|
|
3588
|
+
log("Git detection disabled by configuration");
|
|
3589
|
+
}
|
|
3590
|
+
const fileWatchingEnabled = watch || config.fileWatching.enabled;
|
|
3591
|
+
if (fileWatchingEnabled) {
|
|
3592
|
+
log("\u{1F440} Starting file watcher...");
|
|
3593
|
+
fileWatcher = new FileWatcher(rootDir, config);
|
|
3594
|
+
try {
|
|
3595
|
+
await fileWatcher.start(async (event) => {
|
|
3596
|
+
const { type, filepath } = event;
|
|
3597
|
+
if (type === "unlink") {
|
|
3598
|
+
log(`\u{1F5D1}\uFE0F File deleted: ${filepath}`);
|
|
3599
|
+
try {
|
|
3600
|
+
await vectorDB.deleteByFile(filepath);
|
|
3601
|
+
log(`\u2713 Removed ${filepath} from index`);
|
|
3602
|
+
} catch (error) {
|
|
3603
|
+
log(`Warning: Failed to remove ${filepath}: ${error}`);
|
|
3604
|
+
}
|
|
3605
|
+
} else {
|
|
3606
|
+
const action = type === "add" ? "added" : "changed";
|
|
3607
|
+
log(`\u{1F4DD} File ${action}: ${filepath}`);
|
|
3608
|
+
indexSingleFile(filepath, vectorDB, embeddings, config, { verbose }).catch((error) => {
|
|
3609
|
+
log(`Warning: Failed to reindex ${filepath}: ${error}`);
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
});
|
|
3613
|
+
const watchedCount = fileWatcher.getWatchedFiles().length;
|
|
3614
|
+
log(`\u2713 File watching enabled (watching ${watchedCount} files)`);
|
|
3615
|
+
} catch (error) {
|
|
3616
|
+
log(`Warning: Failed to start file watcher: ${error}`);
|
|
3617
|
+
fileWatcher = null;
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
const cleanup = async () => {
|
|
3621
|
+
log("Shutting down MCP server...");
|
|
3622
|
+
if (gitPollInterval) {
|
|
3623
|
+
clearInterval(gitPollInterval);
|
|
3624
|
+
}
|
|
3625
|
+
if (fileWatcher) {
|
|
3626
|
+
await fileWatcher.stop();
|
|
3627
|
+
}
|
|
3628
|
+
process.exit(0);
|
|
3629
|
+
};
|
|
3630
|
+
process.on("SIGINT", cleanup);
|
|
3631
|
+
process.on("SIGTERM", cleanup);
|
|
3632
|
+
const transport = new StdioServerTransport();
|
|
3633
|
+
await server.connect(transport);
|
|
3634
|
+
log("MCP server started and listening on stdio");
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// src/cli/serve.ts
|
|
3638
|
+
init_banner();
|
|
3639
|
+
async function serveCommand(options) {
|
|
3640
|
+
const rootDir = process.cwd();
|
|
3641
|
+
try {
|
|
3642
|
+
showBanner();
|
|
3643
|
+
console.error(chalk6.bold("Starting MCP server...\n"));
|
|
3644
|
+
await startMCPServer({
|
|
3645
|
+
rootDir,
|
|
3646
|
+
verbose: true,
|
|
3647
|
+
watch: options.watch
|
|
3648
|
+
});
|
|
3649
|
+
} catch (error) {
|
|
3650
|
+
console.error(chalk6.red("Failed to start MCP server:"), error);
|
|
3651
|
+
process.exit(1);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
// src/cli/index.ts
|
|
3656
|
+
var __filename4 = fileURLToPath4(import.meta.url);
|
|
3657
|
+
var __dirname4 = dirname3(__filename4);
|
|
3658
|
+
var require4 = createRequire3(import.meta.url);
|
|
3659
|
+
var packageJson3;
|
|
3660
|
+
try {
|
|
3661
|
+
packageJson3 = require4(join3(__dirname4, "../package.json"));
|
|
3662
|
+
} catch {
|
|
3663
|
+
packageJson3 = require4(join3(__dirname4, "../../package.json"));
|
|
3664
|
+
}
|
|
3665
|
+
var program = new Command();
|
|
3666
|
+
program.name("lien").description("Local semantic code search for AI assistants via MCP").version(packageJson3.version);
|
|
3667
|
+
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", "Enable file watching for real-time reindexing").action(serveCommand);
|
|
3670
|
+
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
|
+
|
|
3693
|
+
// src/index.ts
|
|
3694
|
+
program.parse();
|
|
3695
|
+
//# sourceMappingURL=index.js.map
|