@lorrylurui/code-intelligence-mcp 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/detect-duplicates.js +28 -6
- package/dist/cli/index-codebase.js +33 -5
- package/dist/config/env.js +36 -13
- package/dist/services/reindex.js +25 -12
- package/package.json +1 -1
|
@@ -5,22 +5,20 @@
|
|
|
5
5
|
* 策略:
|
|
6
6
|
* - 只分析 changed files 中可索引的导出代码块
|
|
7
7
|
* - 与库内同 type 的存量代码块做语义相似度(cosine)匹配
|
|
8
|
-
* - 对 component:要求 newProps 是 oldProps
|
|
8
|
+
* - 对 component:要求 newProps 是 oldProps 的超集(或至少覆盖大部分)才判定为"重复/可合并"
|
|
9
9
|
*
|
|
10
10
|
* 输出:
|
|
11
11
|
* - duplicate-report.json
|
|
12
12
|
* - duplicate-report.md(中文,适合 PR 评论)
|
|
13
13
|
*/
|
|
14
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { resolve } from 'node:path';
|
|
16
|
-
import
|
|
17
|
-
import { env, validateEnv } from '../config/env.js';
|
|
16
|
+
import { env, loadProjectDotenv, validateEnv } from '../config/env.js';
|
|
18
17
|
import { getMySqlPool } from '../db/mysql.js';
|
|
19
18
|
import { indexedRowToEmbedText } from '../indexer/embedText.js';
|
|
20
19
|
import { indexProject } from '../indexer/indexProject.js';
|
|
21
20
|
import { createEmbeddingClient, embedAll, } from '../services/embeddingClient.js';
|
|
22
21
|
import { cosineSimilarity } from '../services/vectorMath.js';
|
|
23
|
-
dotenv.config();
|
|
24
22
|
function parseArgs(argv) {
|
|
25
23
|
const args = new Map();
|
|
26
24
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -89,6 +87,31 @@ function escapeMd(text) {
|
|
|
89
87
|
return text.replace(/\|/g, '\\|');
|
|
90
88
|
}
|
|
91
89
|
async function main() {
|
|
90
|
+
// 1️ 确定项目根目录
|
|
91
|
+
const projectRoot = resolve(process.env.INDEX_ROOT ?? process.cwd());
|
|
92
|
+
// 2️ 加载环境变量:先本地默认值,再第三方覆盖
|
|
93
|
+
const localEnvPath = resolve(process.cwd(), '.env');
|
|
94
|
+
if (existsSync(localEnvPath)) {
|
|
95
|
+
const content = readFileSync(localEnvPath, 'utf-8');
|
|
96
|
+
for (const line of content.split('\n')) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
99
|
+
continue;
|
|
100
|
+
const eqIdx = trimmed.indexOf('=');
|
|
101
|
+
if (eqIdx === -1)
|
|
102
|
+
continue;
|
|
103
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
104
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
105
|
+
value = value.replace(/^["']|["']$/g, '');
|
|
106
|
+
if (key && process.env[key] === undefined) {
|
|
107
|
+
process.env[key] = value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
loadProjectDotenv(projectRoot);
|
|
112
|
+
console.error(`[duplicate-check] projectRoot=${projectRoot}, ` +
|
|
113
|
+
`MYSQL_ENABLED=${process.env.MYSQL_ENABLED}`);
|
|
114
|
+
// 3️ 解析命令行参数
|
|
92
115
|
const args = parseArgs(process.argv.slice(2));
|
|
93
116
|
const changedFilesPath = args.get('changed-files') ?? 'changed_files.txt';
|
|
94
117
|
const outJson = args.get('out-json') ?? 'duplicate-report.json';
|
|
@@ -118,7 +141,6 @@ async function main() {
|
|
|
118
141
|
}
|
|
119
142
|
// Type narrowing for TS (pool is guaranteed non-null after guards above)
|
|
120
143
|
const mysqlPool = isMockMode ? null : getMySqlPool();
|
|
121
|
-
const projectRoot = resolve(process.cwd());
|
|
122
144
|
const changed = readLines(changedFilesPath)
|
|
123
145
|
.filter((p) => p.endsWith('.ts') || p.endsWith('.tsx'))
|
|
124
146
|
.filter((p) => !p.includes('/node_modules/') && !p.includes('/dist/'));
|
|
@@ -1,25 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Phase 2 CLI:扫描代码库并写入 MySQL `symbols`(需 `MYSQL_ENABLED=true`)。
|
|
4
|
+
*
|
|
5
|
+
* 环境变量加载顺序:
|
|
6
|
+
* 1. 命令行参数(最高优先级)
|
|
7
|
+
* 2. INDEX_ROOT 指向的第三方项目 .env(中等优先级,优先使用第三方显式设置的值)
|
|
8
|
+
* 3. 本地的 .env(最低优先级,提供默认值)
|
|
4
9
|
*/
|
|
5
10
|
import { resolve } from 'node:path';
|
|
6
|
-
import
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { loadProjectDotenv } from '../config/env.js';
|
|
7
13
|
import { runReindex } from '../services/reindex.js';
|
|
8
|
-
dotenv.config();
|
|
9
14
|
/**
|
|
10
|
-
*
|
|
11
|
-
* 进度与统计输出到 **stderr**,避免占用 stdout
|
|
15
|
+
* 入口:加载第三方 .env → 校验环境 → 调用 runReindex。
|
|
16
|
+
* 进度与统计输出到 **stderr**,避免占用 stdout。
|
|
12
17
|
* 进程退出码:成功 `0`,无 MySQL 或异常 `1`。
|
|
13
18
|
*/
|
|
14
19
|
async function main() {
|
|
20
|
+
// 1️ 先加载本地 .env(提供默认值)
|
|
21
|
+
const localEnvPath = resolve(process.cwd(), '.env');
|
|
22
|
+
if (existsSync(localEnvPath)) {
|
|
23
|
+
const content = readFileSync(localEnvPath, 'utf-8');
|
|
24
|
+
for (const line of content.split('\n')) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
27
|
+
continue;
|
|
28
|
+
const eqIdx = trimmed.indexOf('=');
|
|
29
|
+
if (eqIdx === -1)
|
|
30
|
+
continue;
|
|
31
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
32
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
33
|
+
value = value.replace(/^["']|["']$/g, '');
|
|
34
|
+
if (key && process.env[key] === undefined) {
|
|
35
|
+
process.env[key] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 2️ 确定项目根目录并加载第三方 .env(仅覆盖未定义的变量)
|
|
15
40
|
const projectRoot = resolve(process.env.INDEX_ROOT ?? process.cwd());
|
|
41
|
+
loadProjectDotenv(projectRoot);
|
|
42
|
+
console.error(`[index] projectRoot=${projectRoot}`);
|
|
43
|
+
console.error(`[index] MYSQL_ENABLED=${process.env.MYSQL_ENABLED}, ` +
|
|
44
|
+
`MYSQL_HOST=${process.env.MYSQL_HOST}`);
|
|
16
45
|
const globPatterns = process.env.INDEX_GLOB
|
|
17
46
|
? process.env.INDEX_GLOB.split(/\s+/).map((s) => s.trim()).filter(Boolean)
|
|
18
47
|
: undefined;
|
|
19
48
|
const ignore = process.env.INDEX_IGNORE
|
|
20
49
|
? process.env.INDEX_IGNORE.split(',').map((s) => s.trim())
|
|
21
50
|
: undefined;
|
|
22
|
-
console.error(`[index] projectRoot=${projectRoot}`);
|
|
23
51
|
const result = await runReindex({
|
|
24
52
|
projectRoot,
|
|
25
53
|
globPatterns,
|
package/dist/config/env.js
CHANGED
|
@@ -16,14 +16,32 @@ dotenv.config({
|
|
|
16
16
|
path: path.resolve(projectRoot, '.env'),
|
|
17
17
|
override: false,
|
|
18
18
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
/**
|
|
20
|
+
* 从指定项目根目录加载 .env 到 process.env
|
|
21
|
+
* 行为:优先使用第三方显式设置的值,否则保留 MCP Server 本地配置
|
|
22
|
+
*/
|
|
23
|
+
export function loadProjectDotenv(projectRoot) {
|
|
24
|
+
const envPath = path.resolve(projectRoot, '.env');
|
|
25
|
+
if (!existsSync(envPath)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
29
|
+
// 第一步:收集第三方 .env 中所有显式定义的 key
|
|
30
|
+
const thirdPartyKeys = new Set();
|
|
31
|
+
for (const line of content.split('\n')) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
34
|
+
continue;
|
|
35
|
+
const eqIdx = trimmed.indexOf('=');
|
|
36
|
+
if (eqIdx === -1)
|
|
37
|
+
continue;
|
|
38
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
39
|
+
if (!key)
|
|
40
|
+
continue;
|
|
41
|
+
thirdPartyKeys.add(key);
|
|
42
|
+
}
|
|
43
|
+
// 第二步:如果某个 key 是第三方显式定义的,则覆盖(不管值是什么)
|
|
44
|
+
for (const line of content.split('\n')) {
|
|
27
45
|
const trimmed = line.trim();
|
|
28
46
|
if (!trimmed || trimmed.startsWith('#'))
|
|
29
47
|
continue;
|
|
@@ -31,14 +49,19 @@ if (existsSync(clientEnvPath)) {
|
|
|
31
49
|
if (eqIdx === -1)
|
|
32
50
|
continue;
|
|
33
51
|
const key = trimmed.slice(0, eqIdx).trim();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
53
|
+
value = value.replace(/^["']|["']$/g, '');
|
|
54
|
+
if (!key)
|
|
55
|
+
continue;
|
|
56
|
+
// 只有当第三方显式定义了这个 key 时才覆盖
|
|
57
|
+
if (thirdPartyKeys.has(key)) {
|
|
58
|
+
process.env[key] = value;
|
|
39
59
|
}
|
|
40
60
|
}
|
|
41
61
|
}
|
|
62
|
+
// 尝试从第三方项目目录加载 .env,按变量维度覆盖(只覆盖第三方明确配置的变量)
|
|
63
|
+
const clientProjectRoot = process.env.INDEX_ROOT || process.cwd();
|
|
64
|
+
loadProjectDotenv(clientProjectRoot);
|
|
42
65
|
// 外部传入的 env 已在上一步保留,这里确保环境变量已正确设置
|
|
43
66
|
for (const arg of process.argv) {
|
|
44
67
|
const match = arg.match(/^--([A-Z_][A-Z0-9_]*)=(.+)$/);
|
package/dist/services/reindex.js
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { loadProjectDotenv, validateEnv } from '../config/env.js';
|
|
3
3
|
import { getMySqlPool } from '../db/mysql.js';
|
|
4
4
|
import { indexedRowToEmbedText } from '../indexer/embedText.js';
|
|
5
5
|
import { indexProject } from '../indexer/indexProject.js';
|
|
6
6
|
import { upsertSymbols } from '../indexer/persistSymbols.js';
|
|
7
7
|
import { createEmbeddingClient, embedAll, } from '../services/embeddingClient.js';
|
|
8
8
|
export async function runReindex(options = {}) {
|
|
9
|
-
validateEnv();
|
|
10
|
-
const pool = getMySqlPool();
|
|
11
|
-
console.error('[reindex] pool', 'options:', JSON.stringify(options));
|
|
12
|
-
if (!pool || !env.mysqlEnabled) {
|
|
13
|
-
console.error('[reindex] pool', pool, env.mysqlEnabled);
|
|
14
|
-
throw new Error('执行 reindex 前必须开启 MYSQL_ENABLED=true。');
|
|
15
|
-
}
|
|
16
|
-
await pool.query('SELECT 1'); // 测试连接,提前捕获常见的连接错误(如拒绝、认证失败、超时等),并给出更友好的提示。
|
|
17
|
-
console.error('[reindex] MySQL connection successful');
|
|
18
9
|
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
10
|
+
const { dryRun = false } = options;
|
|
11
|
+
// 1️ 加载第三方 .env:只覆盖未定义的变量 → 保留 MCP Server 自身配置
|
|
12
|
+
loadProjectDotenv(projectRoot);
|
|
13
|
+
// 2️ 打印生效的环境变量(便于调试)
|
|
14
|
+
console.error(`[reindex] projectRoot=${projectRoot}, dryRun=${dryRun}, ` +
|
|
15
|
+
`MYSQL_ENABLED=${process.env.MYSQL_ENABLED}, ` +
|
|
16
|
+
`MYSQL_HOST=${process.env.MYSQL_HOST}`);
|
|
17
|
+
// 3️⃣ 只有需要写入数据库时才检查 MySQL 并建立连接
|
|
18
|
+
// 注意:直接检查 process.env,因为 env.mysqlEnabled 是模块加载时计算的,不会反映 loadProjectDotenv 的更新
|
|
19
|
+
const mysqlEnabled = process.env.MYSQL_ENABLED === 'true';
|
|
20
|
+
const embeddingServiceUrl = process.env.EMBEDDING_SERVICE_URL;
|
|
21
|
+
let pool = null;
|
|
22
|
+
if (!dryRun) {
|
|
23
|
+
if (!mysqlEnabled) {
|
|
24
|
+
throw new Error('执行 reindex 写入数据库需要 MYSQL_ENABLED=true。' +
|
|
25
|
+
'第三方项目可在 .env 中配置此变量(未配置则使用 MCP Server 本地配置)。');
|
|
26
|
+
}
|
|
27
|
+
validateEnv();
|
|
28
|
+
pool = getMySqlPool();
|
|
29
|
+
await pool.query('SELECT 1'); // 测试连接
|
|
30
|
+
console.error('[reindex] MySQL connection successful');
|
|
31
|
+
}
|
|
19
32
|
const rows = await indexProject({
|
|
20
33
|
projectRoot,
|
|
21
34
|
globPatterns: options.globPatterns,
|
|
@@ -24,9 +37,9 @@ export async function runReindex(options = {}) {
|
|
|
24
37
|
console.error(`[reindex] extracted ${rows.length} symbol(s) from ${projectRoot}`);
|
|
25
38
|
let embeddingsComputed = false;
|
|
26
39
|
let embeddingPayload;
|
|
27
|
-
if (!options.dryRun && rows.length > 0 &&
|
|
40
|
+
if (!options.dryRun && rows.length > 0 && embeddingServiceUrl) {
|
|
28
41
|
try {
|
|
29
|
-
const client = createEmbeddingClient(
|
|
42
|
+
const client = createEmbeddingClient(embeddingServiceUrl);
|
|
30
43
|
const texts = rows.map(indexedRowToEmbedText);
|
|
31
44
|
const vecs = await embedAll(client, texts);
|
|
32
45
|
embeddingPayload = vecs;
|