@kennyzhao/cgm 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.sh ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # CodeGraph Manager 一键安装(Linux / macOS)
3
+ # 自动安装缺失依赖:Node.js(>=18) → git → CodeGraph → cgm,然后启动服务
4
+ # 用法: bash install.sh [cgm 参数透传] 例: bash install.sh --port 9000 --data /srv/cgm-data
5
+ set -euo pipefail
6
+
7
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ SUDO=""
9
+ if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
10
+
11
+ say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
12
+ die() { printf '\033[1;31m错误:\033[0m %s\n' "$*" >&2; exit 1; }
13
+
14
+ # ---------- 1) Node.js >= 18 ----------
15
+ node_major() { node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1 || echo 0; }
16
+
17
+ NM="$(node_major || true)"
18
+ if [ "${NM:-0}" -ge 18 ] 2>/dev/null; then
19
+ say "Node $(node -v) 已就绪"
20
+ else
21
+ [ "${NM:-0}" -gt 0 ] 2>/dev/null && say "Node $(node -v) 版本过低,需要 >= 18,开始升级..."
22
+ say "安装 Node.js 22 ..."
23
+ if command -v apt-get >/dev/null 2>&1; then
24
+ curl -fsSL https://deb.nodesource.com/setup_22.x | $SUDO -E bash -
25
+ $SUDO apt-get install -y nodejs
26
+ elif command -v dnf >/dev/null 2>&1; then
27
+ curl -fsSL https://rpm.nodesource.com/setup_22.x | $SUDO bash -
28
+ $SUDO dnf install -y nodejs
29
+ elif command -v yum >/dev/null 2>&1; then
30
+ curl -fsSL https://rpm.nodesource.com/setup_22.x | $SUDO bash -
31
+ $SUDO yum install -y nodejs
32
+ elif command -v brew >/dev/null 2>&1; then
33
+ brew install node@22 && brew link --overwrite node@22
34
+ else
35
+ die "未识别的包管理器,请手动安装 Node.js >= 18: https://nodejs.org"
36
+ fi
37
+ command -v node >/dev/null 2>&1 || die "Node 安装后仍不可用,可能需要重开终端再试"
38
+ fi
39
+
40
+ # ---------- 2) git(服务端 clone 业务仓库依赖) ----------
41
+ if command -v git >/dev/null 2>&1; then
42
+ say "git $(git --version | awk '{print $3}') 已就绪"
43
+ else
44
+ say "安装 git ..."
45
+ if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update && $SUDO apt-get install -y git
46
+ elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y git
47
+ elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y git
48
+ elif command -v brew >/dev/null 2>&1; then brew install git
49
+ else die "请手动安装 git: https://git-scm.com"; fi
50
+ fi
51
+
52
+ # ---------- 3) CodeGraph ----------
53
+ if command -v codegraph >/dev/null 2>&1; then
54
+ say "codegraph 已就绪"
55
+ else
56
+ say "安装 CodeGraph ..."
57
+ npm i -g @colbymchenry/codegraph
58
+ fi
59
+
60
+ # ---------- 4) cgm(本仓库 CLI,link 到全局) ----------
61
+ say "安装 cgm(npm i -g $REPO_DIR)..."
62
+ npm i -g "$REPO_DIR"
63
+
64
+ # ---------- 5) 启动(参数透传) ----------
65
+ say "启动 CodeGraph Manager ...(Ctrl+C 退出;参数透传给 cgm)"
66
+ exec cgm "$@"
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "cgm": {
4
+ "url": "http://127.0.0.1:8801/mcp",
5
+ "headers": { "Authorization": "Bearer <MCP_TOKEN>" }
6
+ }
7
+ }
8
+ }
package/mcp.mjs ADDED
@@ -0,0 +1,487 @@
1
+ // CodeGraph Manager —— MCP 服务器模块
2
+ // 提供 MCP (Model Context Protocol) 支持,让 AI agent 能查询仓库名到 projectPath 的映射
3
+ //
4
+ // MCP 工具:
5
+ // query_repository - 根据仓库名查询本地路径和状态
6
+ // list_repositories - 列出所有已注册仓库
7
+ // search_repositories - 模糊搜索仓库名
8
+ // search_code - 检索代码(网关:repo→projectPath 映射后调用 codegraph 原生 MCP)
9
+ //
10
+ // 传输:SSE (Server-Sent Events) + HTTP POST
11
+
12
+ import { spawn } from 'node:child_process';
13
+ import readline from 'node:readline';
14
+ import { parseBin, opEnsureWorktree, MAX_BODY_BYTES, BASE_PATH } from './server.mjs';
15
+
16
+ // MCP 工具定义
17
+ const MCP_TOOLS = [
18
+ {
19
+ name: 'query_repository',
20
+ description: '根据仓库名查询本地路径(projectPath)和状态。支持精确匹配和模糊搜索。',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ name: {
25
+ type: 'string',
26
+ description: '仓库名(支持模糊匹配,如 "tineco-nmp" 或 "ibu/aftersales")'
27
+ }
28
+ },
29
+ required: ['name']
30
+ }
31
+ },
32
+ {
33
+ name: 'list_repositories',
34
+ description: '列出所有已注册的仓库,包含名称、本地路径、状态和提供商信息。',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ status: {
39
+ type: 'string',
40
+ description: '按状态过滤(可选):registered, cloning, cloned, indexing, indexed, failed',
41
+ enum: ['registered', 'cloning', 'cloned', 'indexing', 'indexed', 'failed']
42
+ }
43
+ }
44
+ }
45
+ },
46
+ {
47
+ name: 'search_repositories',
48
+ description: '模糊搜索仓库名,返回匹配的仓库列表。',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ query: {
53
+ type: 'string',
54
+ description: '搜索关键词(模糊匹配仓库名)'
55
+ }
56
+ },
57
+ required: ['query']
58
+ }
59
+ },
60
+ {
61
+ name: 'search_code',
62
+ description: '检索代码(网关)。指定仓库名和查询语句,内部调用 codegraph 的知识图谱检索符号/文件/调用关系。支持按分支检索:传入 branch 会在该分支的 worktree 索引上检索(首次会后台异步建索引,返回"稍后重试")。',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ query: {
67
+ type: 'string',
68
+ description: '检索语句:符号名、文件名或短代码术语(如 "AuthService loginUser"、"GraphTraverser impact")'
69
+ },
70
+ repo: {
71
+ type: 'string',
72
+ description: '仓库名(精确或模糊,如 "tineco-nmp" 或 "ibu/aftersales")。省略时自动用第一个已注册仓库。'
73
+ },
74
+ branch: {
75
+ type: 'string',
76
+ description: '分支名(可选)。指定后在该分支的 worktree 索引上检索;若该分支尚未建索引,后台会异步 fetch+切 worktree+分析,首次调用返回"稍后重试",再次调用即得结果。'
77
+ },
78
+ maxFiles: {
79
+ type: 'number',
80
+ description: '最多返回文件数(默认 12)'
81
+ }
82
+ },
83
+ required: ['query']
84
+ }
85
+ }
86
+ ];
87
+
88
+ // SSE 连接管理
89
+ const sseClients = new Set();
90
+
91
+ // 发送 SSE 事件
92
+ function sendSseEvent(res, event, data) {
93
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
94
+ }
95
+
96
+ // 向所有 SSE 客户端广播事件 —— 公共通知专用(如 repos-changed),严禁推送工具结果/请求响应
97
+ // (工具结果只经 HTTP 响应返回给发起方,广播会造成跨客户端数据泄露)
98
+ export function broadcastSse(event, data) {
99
+ for (const client of sseClients) {
100
+ try { sendSseEvent(client, event, data); } catch { sseClients.delete(client); }
101
+ }
102
+ }
103
+
104
+ // 处理 SSE 连接
105
+ export function handleSseConnection(req, res) {
106
+ // 设置 SSE 头
107
+ res.writeHead(200, {
108
+ 'Content-Type': 'text/event-stream',
109
+ 'Cache-Control': 'no-cache',
110
+ 'Connection': 'keep-alive',
111
+ 'Access-Control-Allow-Origin': '*',
112
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
113
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, MCP-Protocol-Version, Mcp-Session-Id, Last-Event-ID'
114
+ });
115
+
116
+ // 发送初始连接事件(带 BASE_PATH 前缀,供反代部署下的 MCP 客户端回投消息)
117
+ sendSseEvent(res, 'endpoint', { endpoint: BASE_PATH + '/mcp/message' });
118
+
119
+ // 添加到客户端列表
120
+ sseClients.add(res);
121
+
122
+ // 心跳
123
+ const heartbeat = setInterval(() => {
124
+ res.write(': heartbeat\n\n');
125
+ }, 30000);
126
+
127
+ // 清理
128
+ req.on('close', () => {
129
+ clearInterval(heartbeat);
130
+ sseClients.delete(res);
131
+ });
132
+ }
133
+
134
+ // 处理 MCP 消息(通过 POST;鉴权与 OPTIONS 预检已由 server.mjs 路由层完成)
135
+ export async function handleMcpMessage(req, res, repos) {
136
+ // 设置 CORS 头(需允许 MCP 协议头,否则浏览器直连预检失败)
137
+ res.setHeader('Access-Control-Allow-Origin', '*');
138
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
139
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, MCP-Protocol-Version, Mcp-Session-Id, Last-Event-ID');
140
+
141
+ if (req.method !== 'POST') {
142
+ res.writeHead(405, { 'Content-Type': 'application/json' });
143
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
144
+ return;
145
+ }
146
+
147
+ // 读取请求体(与 server.mjs readBody 同一大小上限,超限 413,防绕过)
148
+ const chunks = [];
149
+ let bodySize = 0, bodyTooLarge = false;
150
+ for await (const chunk of req) {
151
+ bodySize += chunk.length;
152
+ if (bodySize > MAX_BODY_BYTES) { bodyTooLarge = true; break; }
153
+ chunks.push(chunk);
154
+ }
155
+ if (bodyTooLarge) {
156
+ res.writeHead(413, { 'Content-Type': 'application/json' });
157
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Payload too large' }, id: null }));
158
+ return;
159
+ }
160
+ const body = Buffer.concat(chunks).toString('utf8');
161
+
162
+ let request;
163
+ try {
164
+ request = JSON.parse(body);
165
+ } catch {
166
+ res.writeHead(400, { 'Content-Type': 'application/json' });
167
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null }));
168
+ return;
169
+ }
170
+
171
+ const { jsonrpc, method, params, id } = request;
172
+
173
+ // 验证 JSON-RPC 版本
174
+ if (jsonrpc !== '2.0') {
175
+ res.writeHead(400, { 'Content-Type': 'application/json' });
176
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32600, message: 'Invalid Request' }, id }));
177
+ return;
178
+ }
179
+
180
+ // 通知类消息(notifications/*):按 MCP/JSON-RPC 规范,无 id、不应有响应体,返回 202
181
+ if (method.startsWith('notifications/')) {
182
+ res.writeHead(202, { 'Content-Type': 'application/json' });
183
+ res.end();
184
+ return;
185
+ }
186
+
187
+ let result;
188
+
189
+ switch (method) {
190
+ case 'ping':
191
+ result = {};
192
+ break;
193
+
194
+ case 'initialize':
195
+ result = {
196
+ protocolVersion: '2024-11-05',
197
+ capabilities: { tools: {} },
198
+ serverInfo: {
199
+ name: 'codegraph-manager',
200
+ version: '1.0.0'
201
+ }
202
+ };
203
+ break;
204
+
205
+ case 'tools/list':
206
+ result = { tools: MCP_TOOLS };
207
+ break;
208
+
209
+ case 'tools/call':
210
+ result = await handleToolCall(params, repos);
211
+ break;
212
+
213
+ default:
214
+ res.writeHead(404, { 'Content-Type': 'application/json' });
215
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' }, id }));
216
+ return;
217
+ }
218
+
219
+ // 发送响应(仅返回给发起方;不再经 SSE 广播给其他客户端,防止跨客户端泄露查询结果)
220
+ const response = { jsonrpc: '2.0', result, id };
221
+ res.writeHead(200, { 'Content-Type': 'application/json' });
222
+ res.end(JSON.stringify(response));
223
+ }
224
+
225
+ // ── codegraph MCP 网关客户端(stdio)──────────────────────────────
226
+ // 单例子进程:懒加载 + 崩溃自动重建。search_code 通过它调用 codegraph 原生 MCP。
227
+ // (parseBin 从 server.mjs 共享)
228
+
229
+ let cgProc = null; // { child, rl, pending, id, ready }
230
+
231
+ function getCgProc() {
232
+ if (cgProc && cgProc.child.exitCode === null && !cgProc.closed) return cgProc;
233
+ const [cmd, ...args] = parseBin(process.env.CODEGRAPH_BIN || 'codegraph');
234
+ const child = spawn(cmd, [...args, 'serve', '--mcp', '--no-watch'], {
235
+ env: { ...process.env, CODEGRAPH_MCP_TOOLS: 'explore,node,search,callers,callees,impact,files,status' },
236
+ stdio: ['pipe', 'pipe', 'pipe'],
237
+ });
238
+ const rl = readline.createInterface({ input: child.stdout });
239
+ const cg = { child, rl, pending: new Map(), id: 0, ready: false, closed: false };
240
+ rl.on('line', (line) => {
241
+ let msg; try { msg = JSON.parse(line); } catch { return; }
242
+ if (msg.id && cg.pending.has(msg.id)) {
243
+ const { resolve, reject, timer } = cg.pending.get(msg.id);
244
+ cg.pending.delete(msg.id); clearTimeout(timer);
245
+ msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result);
246
+ }
247
+ });
248
+ child.on('error', (e) => {
249
+ // spawn 失败(如 ENOENT):不监听会以 unhandled 'error' 崩掉整个服务进程
250
+ cg.closed = true;
251
+ console.error('[codegraph-gateway] 子进程启动失败:', e.message, '(下次调用自动重试)');
252
+ for (const { reject, timer } of cg.pending.values()) { clearTimeout(timer); reject(new Error(`codegraph 启动失败: ${e.message}`)); }
253
+ cg.pending.clear();
254
+ });
255
+ child.on('exit', () => {
256
+ cg.closed = true;
257
+ for (const { reject, timer } of cg.pending.values()) { clearTimeout(timer); reject(new Error('codegraph 子进程已退出')); }
258
+ cg.pending.clear();
259
+ });
260
+ child.stderr.on('data', () => {}); // 静默,错误通过 JSON-RPC error 返回
261
+ cgProc = cg;
262
+ return cg;
263
+ }
264
+
265
+ function cgSend(method, params = {}, timeoutMs = 30000) {
266
+ const cg = getCgProc();
267
+ const id = ++cg.id;
268
+ cg.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
269
+ return new Promise((resolve, reject) => {
270
+ const timer = setTimeout(() => {
271
+ cg.pending.delete(id);
272
+ reject(new Error(`codegraph 响应超时(${timeoutMs / 1000}s): ${method}`));
273
+ }, timeoutMs);
274
+ cg.pending.set(id, { resolve, reject, timer });
275
+ });
276
+ }
277
+
278
+ async function cgReady() {
279
+ const cg = getCgProc();
280
+ if (cg.ready) return cg;
281
+ await cgSend('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'codegraph-manager', version: '1.0.0' } });
282
+ cg.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
283
+ cg.ready = true;
284
+ return cg;
285
+ }
286
+
287
+ async function cgCallTool(name, args, timeoutMs = 30000) {
288
+ await cgReady();
289
+ const res = await cgSend('tools/call', { name, arguments: args }, timeoutMs);
290
+ const text = (res.content || []).filter((c) => c.type === 'text').map((c) => c.text).join('\n');
291
+ return { text, isError: !!res.isError };
292
+ }
293
+
294
+ // 仓库匹配:精确 → 模糊 → 多匹配列表
295
+ function findRepo(repos, name) {
296
+ const q = name.toLowerCase();
297
+ const exact = repos.find((r) => r.name.toLowerCase() === q);
298
+ if (exact) return { repo: exact };
299
+ const matches = repos.filter((r) => r.name.toLowerCase().includes(q) || (r.gitUrl || '').toLowerCase().includes(q));
300
+ if (matches.length === 1) return { repo: matches[0] };
301
+ if (matches.length > 1) return { multiple: matches.map((r) => r.name) };
302
+ return null;
303
+ }
304
+
305
+ // 处理工具调用
306
+ async function handleToolCall(params, repos) {
307
+ const { name, arguments: args } = params;
308
+
309
+ switch (name) {
310
+ case 'query_repository': {
311
+ const queryName = (args?.name || '').toLowerCase();
312
+ if (!queryName) {
313
+ return { content: [{ type: 'text', text: '错误:请提供仓库名' }], isError: true };
314
+ }
315
+
316
+ // 精确匹配
317
+ let repo = repos.find(r => r.name.toLowerCase() === queryName);
318
+
319
+ // 模糊匹配
320
+ if (!repo) {
321
+ const matches = repos.filter(r =>
322
+ r.name.toLowerCase().includes(queryName) ||
323
+ (r.gitUrl || '').toLowerCase().includes(queryName)
324
+ );
325
+
326
+ if (matches.length === 0) {
327
+ return {
328
+ content: [{ type: 'text', text: `未找到匹配 "${args.name}" 的仓库` }],
329
+ isError: false
330
+ };
331
+ }
332
+
333
+ if (matches.length === 1) {
334
+ repo = matches[0];
335
+ } else {
336
+ // 多个匹配,返回列表
337
+ const list = matches.map(r =>
338
+ `- ${r.name} → ${r.localPath || '未克隆'} (${r.status})`
339
+ ).join('\n');
340
+ return {
341
+ content: [{ type: 'text', text: `找到 ${matches.length} 个匹配仓库:\n${list}` }],
342
+ isError: false
343
+ };
344
+ }
345
+ }
346
+
347
+ // 返回单个仓库信息
348
+ return {
349
+ content: [{
350
+ type: 'text',
351
+ text: JSON.stringify({
352
+ name: repo.name,
353
+ projectPath: repo.localPath || null,
354
+ status: repo.status,
355
+ provider: repo.providerType || 'unknown',
356
+ gitUrl: repo.gitUrl || null,
357
+ branch: repo.branch || null
358
+ }, null, 2)
359
+ }],
360
+ isError: false
361
+ };
362
+ }
363
+
364
+ case 'list_repositories': {
365
+ const statusFilter = args?.status;
366
+ let filtered = repos;
367
+
368
+ if (statusFilter) {
369
+ filtered = repos.filter(r => r.status === statusFilter);
370
+ }
371
+
372
+ if (filtered.length === 0) {
373
+ return {
374
+ content: [{ type: 'text', text: statusFilter ? `没有状态为 "${statusFilter}" 的仓库` : '没有注册的仓库' }],
375
+ isError: false
376
+ };
377
+ }
378
+
379
+ const list = filtered.map(r =>
380
+ `- ${r.name}\n 路径: ${r.localPath || '未克隆'}\n 状态: ${r.status}\n 提供商: ${r.providerType || 'unknown'}`
381
+ ).join('\n\n');
382
+
383
+ return {
384
+ content: [{ type: 'text', text: `共 ${filtered.length} 个仓库:\n\n${list}` }],
385
+ isError: false
386
+ };
387
+ }
388
+
389
+ case 'search_repositories': {
390
+ const query = (args?.query || '').toLowerCase();
391
+ if (!query) {
392
+ return { content: [{ type: 'text', text: '错误:请提供搜索关键词' }], isError: true };
393
+ }
394
+
395
+ const matches = repos.filter(r =>
396
+ r.name.toLowerCase().includes(query) ||
397
+ (r.gitUrl || '').toLowerCase().includes(query) ||
398
+ (r.localPath || '').toLowerCase().includes(query)
399
+ );
400
+
401
+ if (matches.length === 0) {
402
+ return {
403
+ content: [{ type: 'text', text: `未找到匹配 "${args.query}" 的仓库` }],
404
+ isError: false
405
+ };
406
+ }
407
+
408
+ const list = matches.map(r =>
409
+ `- ${r.name} → ${r.localPath || '未克隆'} (${r.status})`
410
+ ).join('\n');
411
+
412
+ return {
413
+ content: [{ type: 'text', text: `找到 ${matches.length} 个匹配仓库:\n${list}` }],
414
+ isError: false
415
+ };
416
+ }
417
+
418
+ case 'search_code': {
419
+ const query = (args?.query || '').trim();
420
+ if (!query) return { content: [{ type: 'text', text: '错误:请提供检索语句 query' }], isError: true };
421
+
422
+ // 单仓模式(指定 repo):完整深度分析(原逻辑,格式不变)
423
+ if (args?.repo) {
424
+ const hit = findRepo(repos, args.repo);
425
+ if (!hit) return { content: [{ type: 'text', text: `未找到匹配 "${args.repo}" 的仓库` }], isError: false };
426
+ if (hit.multiple) return { content: [{ type: 'text', text: `匹配到多个仓库:${hit.multiple.join(', ')},请用完整仓库名重试` }], isError: false };
427
+ const target = hit.repo;
428
+ if (!target.localPath) return { content: [{ type: 'text', text: `仓库 "${target.name}" 尚未克隆(状态: ${target.status})` }], isError: false };
429
+ // 多分支策略:指定 branch → 定位/触发该分支 worktree
430
+ if (args?.branch) {
431
+ const ensure = await opEnsureWorktree(target, args.branch);
432
+ if (!ensure.ready) {
433
+ const st = (target.worktrees || []).find((w) => w.branch === args.branch)?.status || 'registered';
434
+ return { content: [{ type: 'text', text: `分支 "${args.branch}" 索引${st === 'indexing' ? '正在创建' : '待创建'}中(fetch + worktree + 分析),请稍后重试。后台完成后即可检索该分支代码。` }], isError: false };
435
+ }
436
+ try {
437
+ const { text, isError } = await cgCallTool('codegraph_explore', { query, projectPath: ensure.localPath, maxFiles: args?.maxFiles ?? 12 });
438
+ const header = `[${target.name}@${args.branch}] ${query}\n`;
439
+ return { content: [{ type: 'text', text: text ? header + text : header + '(无结果)' }], isError };
440
+ } catch (e) {
441
+ return { content: [{ type: 'text', text: `检索失败: ${e.message}` }], isError: true };
442
+ }
443
+ }
444
+ try {
445
+ const { text, isError } = await cgCallTool('codegraph_explore', { query, projectPath: target.localPath, maxFiles: args?.maxFiles ?? 12 });
446
+ const header = `[${target.name}] ${query}\n`;
447
+ return { content: [{ type: 'text', text: text ? header + text : header + '(无结果)' }], isError };
448
+ } catch (e) {
449
+ return { content: [{ type: 'text', text: `检索失败: ${e.message}` }], isError: true };
450
+ }
451
+ }
452
+
453
+ // 跨仓模式(缺省 repo):定位所有已索引仓库——收敛防爆(每仓小 maxFiles + 截断 + 总量 cap)
454
+ const targets = repos.filter((r) => r.status === 'indexed' && r.localPath); // 空仓/未索引跳过
455
+ if (!targets.length) return { content: [{ type: 'text', text: '没有可检索的已索引仓库,请先克隆并分析' }], isError: false };
456
+ const crossMaxFiles = args?.maxFiles ?? 8;
457
+ const settled = await Promise.allSettled(targets.map((t) =>
458
+ cgCallTool('codegraph_explore', { query, projectPath: t.localPath, maxFiles: crossMaxFiles }, 15000)
459
+ ));
460
+ const rows = settled.map((s, i) => {
461
+ const name = targets[i].name;
462
+ if (s.status === 'rejected') return { name, hit: -1, text: `[${name}] 检索失败: ${s.reason.message}` };
463
+ const text = s.value.text;
464
+ const m = text.match(/Found (\d+) symbols?/i); // codegraph 稳定格式, 仅取数字做排序
465
+ const hit = m ? parseInt(m[1], 10) : 0;
466
+ return { name, hit, text };
467
+ }).sort((a, b) => b.hit - a.hit); // 命中多的排前(最相关优先)
468
+ const hitRows = rows.filter((r) => r.hit > 0);
469
+ const summary = hitRows.length
470
+ ? `命中概况:${hitRows.map((r) => `${r.name}(${r.hit})`).join('、')};${rows.length - hitRows.length} 个仓库无命中`
471
+ : `${rows.length} 个仓库均无命中`;
472
+ const parts = rows.map((r) => r.hit < 0 ? r.text : `[${r.name}]\n${r.text.slice(0, 400)}`); // 每仓收敛 400 字符
473
+ let joined = summary + '\n\n' + parts.join('\n\n');
474
+ if (joined.length > 4000) joined = joined.slice(0, 4000) + '\n...命中过多,请用 repo=<仓库名> 单仓深入';
475
+ return { content: [{ type: 'text', text: joined }], isError: false };
476
+ }
477
+
478
+ default:
479
+ return {
480
+ content: [{ type: 'text', text: `未知工具: ${name}` }],
481
+ isError: true
482
+ };
483
+ }
484
+ }
485
+
486
+ // 导出工具定义供主服务器使用
487
+ export { MCP_TOOLS };
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@kennyzhao/cgm",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "多 Git 平台仓库统一管理 + CodeGraph 代码知识图谱 + MCP 接口供 AI 检索代码(零依赖,仅 Node 内置模块)",
5
+ "license": "MIT",
6
+ "repository": { "type": "git", "url": "git+https://github.com/Zhao-T-X/CGM.git" },
7
+ "homepage": "https://github.com/Zhao-T-X/CGM#readme",
8
+ "bugs": { "url": "https://github.com/Zhao-T-X/CGM/issues" },
9
+ "files": [
10
+ "cli.mjs",
11
+ "server.mjs",
12
+ "mcp.mjs",
13
+ "verify-mcp.mjs",
14
+ "public/index.html",
15
+ "public/img",
16
+ "mcp-config.json",
17
+ "install.sh",
18
+ "install.cmd",
19
+ "install.ps1",
20
+ "README.en.md",
21
+ "DEPLOY-Linux.md",
22
+ "LICENSE"
23
+ ],
24
+ "bin": {
25
+ "cgm": "./cli.mjs"
26
+ },
27
+ "scripts": {
28
+ "start": "node server.mjs",
29
+ "verify": "node verify-mcp.mjs"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
Binary file
Binary file
Binary file
Binary file