@vohongtho.infotech/code-intel 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -6
- package/dist/cli/main.js +1802 -465
- package/dist/cli/main.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +1480 -588
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/cli/main.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Database, Connection } from '@ladybugdb/core';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
import path16, { dirname, join } from 'path';
|
|
4
|
+
import fs14, { readFileSync } from 'fs';
|
|
5
|
+
import os2 from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
|
+
import winston from 'winston';
|
|
9
|
+
import DailyRotateFile from 'winston-daily-rotate-file';
|
|
8
10
|
import express from 'express';
|
|
9
11
|
import cors from 'cors';
|
|
10
12
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
14
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
13
16
|
|
|
14
17
|
var __defProp = Object.defineProperty;
|
|
15
18
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -31,7 +34,7 @@ var init_db_manager = __esm({
|
|
|
31
34
|
this.dbPath = dbPath;
|
|
32
35
|
}
|
|
33
36
|
async init() {
|
|
34
|
-
|
|
37
|
+
fs14.mkdirSync(path16.dirname(this.dbPath), { recursive: true });
|
|
35
38
|
this.db = new Database(this.dbPath);
|
|
36
39
|
await this.db.init();
|
|
37
40
|
this.conn = new Connection(this.db);
|
|
@@ -129,15 +132,15 @@ var init_schema = __esm({
|
|
|
129
132
|
}
|
|
130
133
|
});
|
|
131
134
|
function writeNodeCSVs(graph, outputDir) {
|
|
132
|
-
|
|
135
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
133
136
|
const tableFiles = /* @__PURE__ */ new Map();
|
|
134
137
|
const tableFilePaths = /* @__PURE__ */ new Map();
|
|
135
138
|
const header = "id,name,file_path,start_line,end_line,exported,content,metadata\n";
|
|
136
139
|
for (const node of graph.allNodes()) {
|
|
137
140
|
const table = NODE_TABLE_MAP[node.kind];
|
|
138
141
|
if (!tableFiles.has(table)) {
|
|
139
|
-
const filePath =
|
|
140
|
-
const stream2 =
|
|
142
|
+
const filePath = path16.join(outputDir, `${table}.csv`);
|
|
143
|
+
const stream2 = fs14.createWriteStream(filePath);
|
|
141
144
|
stream2.write(header);
|
|
142
145
|
tableFiles.set(table, stream2);
|
|
143
146
|
tableFilePaths.set(table, filePath);
|
|
@@ -160,7 +163,7 @@ function writeNodeCSVs(graph, outputDir) {
|
|
|
160
163
|
return tableFilePaths;
|
|
161
164
|
}
|
|
162
165
|
function writeEdgeCSV(graph, outputDir) {
|
|
163
|
-
|
|
166
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
164
167
|
const groups = /* @__PURE__ */ new Map();
|
|
165
168
|
const header = "from_id,to_id,kind,weight,label\n";
|
|
166
169
|
for (const edge of graph.allEdges()) {
|
|
@@ -171,8 +174,8 @@ function writeEdgeCSV(graph, outputDir) {
|
|
|
171
174
|
const toTable = NODE_TABLE_MAP[targetNode.kind];
|
|
172
175
|
const key = `${fromTable}->${toTable}`;
|
|
173
176
|
if (!groups.has(key)) {
|
|
174
|
-
const filePath =
|
|
175
|
-
const stream =
|
|
177
|
+
const filePath = path16.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
|
|
178
|
+
const stream = fs14.createWriteStream(filePath);
|
|
176
179
|
stream.write(header);
|
|
177
180
|
groups.set(key, { stream, filePath, from: fromTable, to: toTable });
|
|
178
181
|
}
|
|
@@ -268,15 +271,15 @@ var init_graph_loader = __esm({
|
|
|
268
271
|
});
|
|
269
272
|
function loadRegistry() {
|
|
270
273
|
try {
|
|
271
|
-
const data =
|
|
274
|
+
const data = fs14.readFileSync(REPOS_FILE, "utf-8");
|
|
272
275
|
return JSON.parse(data);
|
|
273
276
|
} catch {
|
|
274
277
|
return [];
|
|
275
278
|
}
|
|
276
279
|
}
|
|
277
280
|
function saveRegistry(entries) {
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
fs14.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
282
|
+
fs14.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
|
|
280
283
|
}
|
|
281
284
|
function upsertRepo(entry) {
|
|
282
285
|
const entries = loadRegistry();
|
|
@@ -295,28 +298,28 @@ function removeRepo(repoPath) {
|
|
|
295
298
|
var GLOBAL_DIR, REPOS_FILE;
|
|
296
299
|
var init_repo_registry = __esm({
|
|
297
300
|
"src/storage/repo-registry.ts"() {
|
|
298
|
-
GLOBAL_DIR =
|
|
299
|
-
REPOS_FILE =
|
|
301
|
+
GLOBAL_DIR = path16.join(os2.homedir(), ".code-intel");
|
|
302
|
+
REPOS_FILE = path16.join(GLOBAL_DIR, "repos.json");
|
|
300
303
|
}
|
|
301
304
|
});
|
|
302
305
|
function saveMetadata(repoDir, metadata) {
|
|
303
|
-
const metaDir =
|
|
304
|
-
|
|
305
|
-
|
|
306
|
+
const metaDir = path16.join(repoDir, ".code-intel");
|
|
307
|
+
fs14.mkdirSync(metaDir, { recursive: true });
|
|
308
|
+
fs14.writeFileSync(path16.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
|
|
306
309
|
}
|
|
307
310
|
function loadMetadata(repoDir) {
|
|
308
311
|
try {
|
|
309
|
-
const data =
|
|
312
|
+
const data = fs14.readFileSync(path16.join(repoDir, ".code-intel", "meta.json"), "utf-8");
|
|
310
313
|
return JSON.parse(data);
|
|
311
314
|
} catch {
|
|
312
315
|
return null;
|
|
313
316
|
}
|
|
314
317
|
}
|
|
315
318
|
function getDbPath(repoDir) {
|
|
316
|
-
return
|
|
319
|
+
return path16.join(repoDir, ".code-intel", "graph.db");
|
|
317
320
|
}
|
|
318
321
|
function getVectorDbPath(repoDir) {
|
|
319
|
-
return
|
|
322
|
+
return path16.join(repoDir, ".code-intel", "vector.db");
|
|
320
323
|
}
|
|
321
324
|
var init_metadata = __esm({
|
|
322
325
|
"src/storage/metadata.ts"() {
|
|
@@ -448,27 +451,27 @@ __export(group_registry_exports, {
|
|
|
448
451
|
saveSyncResult: () => saveSyncResult
|
|
449
452
|
});
|
|
450
453
|
function groupFile(name) {
|
|
451
|
-
return
|
|
454
|
+
return path16.join(GROUPS_DIR, `${name}.json`);
|
|
452
455
|
}
|
|
453
456
|
function loadGroup(name) {
|
|
454
457
|
try {
|
|
455
|
-
return JSON.parse(
|
|
458
|
+
return JSON.parse(fs14.readFileSync(groupFile(name), "utf-8"));
|
|
456
459
|
} catch {
|
|
457
460
|
return null;
|
|
458
461
|
}
|
|
459
462
|
}
|
|
460
463
|
function saveGroup(group) {
|
|
461
|
-
|
|
462
|
-
|
|
464
|
+
fs14.mkdirSync(GROUPS_DIR, { recursive: true });
|
|
465
|
+
fs14.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
|
|
463
466
|
}
|
|
464
467
|
function listGroups() {
|
|
465
468
|
const groups = [];
|
|
466
469
|
try {
|
|
467
|
-
for (const file of
|
|
470
|
+
for (const file of fs14.readdirSync(GROUPS_DIR)) {
|
|
468
471
|
if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
|
|
469
472
|
try {
|
|
470
473
|
const g = JSON.parse(
|
|
471
|
-
|
|
474
|
+
fs14.readFileSync(path16.join(GROUPS_DIR, file), "utf-8")
|
|
472
475
|
);
|
|
473
476
|
groups.push(g);
|
|
474
477
|
} catch {
|
|
@@ -480,16 +483,16 @@ function listGroups() {
|
|
|
480
483
|
}
|
|
481
484
|
function deleteGroup(name) {
|
|
482
485
|
try {
|
|
483
|
-
|
|
486
|
+
fs14.unlinkSync(groupFile(name));
|
|
484
487
|
} catch {
|
|
485
488
|
}
|
|
486
489
|
try {
|
|
487
|
-
|
|
490
|
+
fs14.unlinkSync(path16.join(GROUPS_DIR, `${name}.sync.json`));
|
|
488
491
|
} catch {
|
|
489
492
|
}
|
|
490
493
|
}
|
|
491
494
|
function groupExists(name) {
|
|
492
|
-
return
|
|
495
|
+
return fs14.existsSync(groupFile(name));
|
|
493
496
|
}
|
|
494
497
|
function addMember(groupName, member) {
|
|
495
498
|
const group = loadGroup(groupName);
|
|
@@ -515,16 +518,16 @@ function removeMember(groupName, groupPath) {
|
|
|
515
518
|
return group;
|
|
516
519
|
}
|
|
517
520
|
function saveSyncResult(result) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
+
fs14.mkdirSync(GROUPS_DIR, { recursive: true });
|
|
522
|
+
fs14.writeFileSync(
|
|
523
|
+
path16.join(GROUPS_DIR, `${result.groupName}.sync.json`),
|
|
521
524
|
JSON.stringify(result, null, 2) + "\n"
|
|
522
525
|
);
|
|
523
526
|
}
|
|
524
527
|
function loadSyncResult(groupName) {
|
|
525
528
|
try {
|
|
526
529
|
return JSON.parse(
|
|
527
|
-
|
|
530
|
+
fs14.readFileSync(path16.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
|
|
528
531
|
);
|
|
529
532
|
} catch {
|
|
530
533
|
return null;
|
|
@@ -533,7 +536,7 @@ function loadSyncResult(groupName) {
|
|
|
533
536
|
var GROUPS_DIR;
|
|
534
537
|
var init_group_registry = __esm({
|
|
535
538
|
"src/multi-repo/group-registry.ts"() {
|
|
536
|
-
GROUPS_DIR =
|
|
539
|
+
GROUPS_DIR = path16.join(os2.homedir(), ".code-intel", "groups");
|
|
537
540
|
}
|
|
538
541
|
});
|
|
539
542
|
|
|
@@ -584,6 +587,227 @@ var init_embedder = __esm({
|
|
|
584
587
|
pipelineInstance = null;
|
|
585
588
|
}
|
|
586
589
|
});
|
|
590
|
+
var SENSITIVE_KEYS = [
|
|
591
|
+
"password",
|
|
592
|
+
"passwd",
|
|
593
|
+
"pass",
|
|
594
|
+
"pwd",
|
|
595
|
+
"secret",
|
|
596
|
+
"secretkey",
|
|
597
|
+
"secret_key",
|
|
598
|
+
"secretaccesskey",
|
|
599
|
+
"accesskeyid",
|
|
600
|
+
"credentials",
|
|
601
|
+
"auth",
|
|
602
|
+
"authentication",
|
|
603
|
+
"login",
|
|
604
|
+
"api_key",
|
|
605
|
+
"apikey",
|
|
606
|
+
"api",
|
|
607
|
+
"access_key",
|
|
608
|
+
"access_token",
|
|
609
|
+
"accesskey",
|
|
610
|
+
"auth_key",
|
|
611
|
+
"auth_token",
|
|
612
|
+
"authkey",
|
|
613
|
+
"token",
|
|
614
|
+
"jwt",
|
|
615
|
+
"bearer_token",
|
|
616
|
+
"refresh_token",
|
|
617
|
+
"session_token",
|
|
618
|
+
"session_key",
|
|
619
|
+
"oauth_token",
|
|
620
|
+
"connection_string",
|
|
621
|
+
"conn_string",
|
|
622
|
+
"db_uri",
|
|
623
|
+
"db_url",
|
|
624
|
+
"database_url",
|
|
625
|
+
"mongodb_uri",
|
|
626
|
+
"mysql_uri",
|
|
627
|
+
"postgres_uri",
|
|
628
|
+
"sql_uri",
|
|
629
|
+
"db_username",
|
|
630
|
+
"db_password",
|
|
631
|
+
"db_host",
|
|
632
|
+
"db_port",
|
|
633
|
+
"db_name",
|
|
634
|
+
"encryption_key",
|
|
635
|
+
"crypto_key",
|
|
636
|
+
"private_key",
|
|
637
|
+
"public_key",
|
|
638
|
+
"ssl_key",
|
|
639
|
+
"ssh_key",
|
|
640
|
+
"pgp_key",
|
|
641
|
+
"rsa_key",
|
|
642
|
+
"aes_key",
|
|
643
|
+
"email",
|
|
644
|
+
"phone",
|
|
645
|
+
"telephone",
|
|
646
|
+
"mobile",
|
|
647
|
+
"ssn",
|
|
648
|
+
"social_security",
|
|
649
|
+
"credit_card",
|
|
650
|
+
"cc_number",
|
|
651
|
+
"card_number",
|
|
652
|
+
"cvv",
|
|
653
|
+
"expiry_date",
|
|
654
|
+
"birth_date",
|
|
655
|
+
"dob",
|
|
656
|
+
"address",
|
|
657
|
+
"zip_code",
|
|
658
|
+
"postal_code",
|
|
659
|
+
"bank_account",
|
|
660
|
+
"iban",
|
|
661
|
+
"swift_code",
|
|
662
|
+
"routing_number",
|
|
663
|
+
"tax_id",
|
|
664
|
+
"vat_number",
|
|
665
|
+
"financial_id",
|
|
666
|
+
"certificate",
|
|
667
|
+
"client_cert",
|
|
668
|
+
"server_cert",
|
|
669
|
+
"ca_cert",
|
|
670
|
+
"aws_key",
|
|
671
|
+
"aws_secret",
|
|
672
|
+
"azure_key",
|
|
673
|
+
"gcp_key",
|
|
674
|
+
"s3_key",
|
|
675
|
+
"cloudinary_key",
|
|
676
|
+
"stripe_key",
|
|
677
|
+
"paypal_key",
|
|
678
|
+
"twilio_key",
|
|
679
|
+
"app_secret",
|
|
680
|
+
"client_secret",
|
|
681
|
+
"consumer_secret",
|
|
682
|
+
"encryption_secret",
|
|
683
|
+
"master_key",
|
|
684
|
+
"root_password",
|
|
685
|
+
"admin_password",
|
|
686
|
+
"config_secret",
|
|
687
|
+
"env_secret",
|
|
688
|
+
"deploy_key",
|
|
689
|
+
"ci_key",
|
|
690
|
+
"session_id",
|
|
691
|
+
"cookie_secret",
|
|
692
|
+
"csrf_token",
|
|
693
|
+
"xsrf_token",
|
|
694
|
+
"license_key",
|
|
695
|
+
"product_key",
|
|
696
|
+
"serial_number",
|
|
697
|
+
"activation_code"
|
|
698
|
+
];
|
|
699
|
+
var SENSITIVE_PATTERNS = [
|
|
700
|
+
/(?:password|passwd|secret|api_key|access_token|auth_token|token)\s*[:=]\s*([^\s,]+)/gi,
|
|
701
|
+
/\b\d{16}\b/gi,
|
|
702
|
+
/\b\d{3}-\d{2}-\d{4}\b/gi,
|
|
703
|
+
/\b[A-Za-z0-9]{32}\b/gi,
|
|
704
|
+
/\b[A-Za-z0-9_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b/gi,
|
|
705
|
+
/\b\d{10}\b/gi,
|
|
706
|
+
/\b[A-Za-z0-9]{64}\b/gi,
|
|
707
|
+
/(?:connection_string|db_uri|db_url|mongodb_uri)\s*[:=]\s*([^\s,]+)/gi,
|
|
708
|
+
/(?:apikey|api_key|auth_key)\s*[:=]\s*([^\s,]+)/gi,
|
|
709
|
+
/(?:bearer\s+)[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+/gi
|
|
710
|
+
];
|
|
711
|
+
var SENSITIVE_KEYS_REGEX = new RegExp(`^(${SENSITIVE_KEYS.join("|")})$`, "i");
|
|
712
|
+
var Logger = class _Logger {
|
|
713
|
+
static instance = null;
|
|
714
|
+
static maskSensitiveData(value) {
|
|
715
|
+
if (typeof value === "string" && value.length > 5) {
|
|
716
|
+
const firstChar = value.at(0);
|
|
717
|
+
const lastChar = value.at(-1);
|
|
718
|
+
return firstChar + "*".repeat(value.length - 2) + lastChar;
|
|
719
|
+
}
|
|
720
|
+
return value;
|
|
721
|
+
}
|
|
722
|
+
static maskSensitive(message, args = []) {
|
|
723
|
+
const maskString = (input) => {
|
|
724
|
+
if (typeof input !== "string") return input;
|
|
725
|
+
return SENSITIVE_PATTERNS.reduce((str, pattern) => {
|
|
726
|
+
return str.replace(
|
|
727
|
+
pattern,
|
|
728
|
+
(match, value) => value ? match.replace(value, _Logger.maskSensitiveData(value)) : match
|
|
729
|
+
);
|
|
730
|
+
}, input);
|
|
731
|
+
};
|
|
732
|
+
const deepMask = (obj) => {
|
|
733
|
+
if (typeof obj === "string") return maskString(obj);
|
|
734
|
+
if (Array.isArray(obj)) return obj.map((item) => deepMask(item));
|
|
735
|
+
if (typeof obj === "object" && obj !== null) {
|
|
736
|
+
return Object.entries(obj).reduce(
|
|
737
|
+
(acc, [key, value]) => {
|
|
738
|
+
if (value === void 0) return acc;
|
|
739
|
+
const isSensitiveKey = SENSITIVE_KEYS_REGEX.test(key);
|
|
740
|
+
acc[key] = isSensitiveKey && typeof value === "string" ? _Logger.maskSensitiveData(value) : deepMask(value);
|
|
741
|
+
return acc;
|
|
742
|
+
},
|
|
743
|
+
{}
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
return obj;
|
|
747
|
+
};
|
|
748
|
+
return {
|
|
749
|
+
maskedMessage: maskString(message),
|
|
750
|
+
maskedArgs: args.map((arg) => deepMask(arg))
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/** Global log directory: ~/.code-intel/logs */
|
|
754
|
+
static LOG_DIR = path16.join(os2.homedir(), ".code-intel", "logs");
|
|
755
|
+
static getLogger() {
|
|
756
|
+
if (!_Logger.instance) {
|
|
757
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
758
|
+
const logLevel = process.env.LOG_LEVEL ?? "info";
|
|
759
|
+
const transports = [];
|
|
760
|
+
transports.push(new winston.transports.Console());
|
|
761
|
+
if (!isProduction) {
|
|
762
|
+
try {
|
|
763
|
+
if (!fs14.existsSync(_Logger.LOG_DIR)) {
|
|
764
|
+
fs14.mkdirSync(_Logger.LOG_DIR, { recursive: true });
|
|
765
|
+
}
|
|
766
|
+
transports.push(
|
|
767
|
+
new DailyRotateFile({
|
|
768
|
+
filename: path16.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
|
|
769
|
+
datePattern: "YYYY-MM-DD",
|
|
770
|
+
maxSize: "20m",
|
|
771
|
+
maxFiles: "14d"
|
|
772
|
+
})
|
|
773
|
+
);
|
|
774
|
+
} catch {
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
_Logger.instance = winston.createLogger({
|
|
778
|
+
level: logLevel,
|
|
779
|
+
format: winston.format.combine(
|
|
780
|
+
winston.format.timestamp(),
|
|
781
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
782
|
+
const args = meta[/* @__PURE__ */ Symbol.for("splat")] || [];
|
|
783
|
+
const { maskedMessage, maskedArgs } = _Logger.maskSensitive(message, args);
|
|
784
|
+
const formattedArgs = maskedArgs.map(
|
|
785
|
+
(arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
|
|
786
|
+
);
|
|
787
|
+
const suffix = formattedArgs.length ? " " + formattedArgs.join(" ") : "";
|
|
788
|
+
return `${timestamp} [${level.toUpperCase()}]: ${maskedMessage}${suffix}`;
|
|
789
|
+
})
|
|
790
|
+
),
|
|
791
|
+
transports
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return _Logger.instance;
|
|
795
|
+
}
|
|
796
|
+
static info(message, ...args) {
|
|
797
|
+
_Logger.getLogger().info(message, ...args);
|
|
798
|
+
}
|
|
799
|
+
static warn(message, ...args) {
|
|
800
|
+
_Logger.getLogger().warn(message, ...args);
|
|
801
|
+
}
|
|
802
|
+
static error(message, ...args) {
|
|
803
|
+
_Logger.getLogger().error(message, ...args);
|
|
804
|
+
}
|
|
805
|
+
static debug(message, ...args) {
|
|
806
|
+
_Logger.getLogger().debug(message, ...args);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
var logger_default = Logger;
|
|
810
|
+
Logger.getLogger();
|
|
587
811
|
|
|
588
812
|
// src/graph/knowledge-graph.ts
|
|
589
813
|
function createKnowledgeGraph() {
|
|
@@ -729,25 +953,25 @@ function validateDAG(phases) {
|
|
|
729
953
|
const visiting = /* @__PURE__ */ new Set();
|
|
730
954
|
const visited = /* @__PURE__ */ new Set();
|
|
731
955
|
const phaseMap = new Map(phases.map((p) => [p.name, p]));
|
|
732
|
-
function dfs(name,
|
|
956
|
+
function dfs(name, path17) {
|
|
733
957
|
if (visiting.has(name)) {
|
|
734
|
-
const cycleStart =
|
|
735
|
-
const cycle =
|
|
958
|
+
const cycleStart = path17.indexOf(name);
|
|
959
|
+
const cycle = path17.slice(cycleStart).concat(name);
|
|
736
960
|
errors.push({ type: "cycle", message: `Cycle detected: ${cycle.join(" \u2192 ")}` });
|
|
737
961
|
return true;
|
|
738
962
|
}
|
|
739
963
|
if (visited.has(name)) return false;
|
|
740
964
|
visiting.add(name);
|
|
741
|
-
|
|
965
|
+
path17.push(name);
|
|
742
966
|
const phase = phaseMap.get(name);
|
|
743
967
|
if (phase) {
|
|
744
968
|
for (const dep of phase.dependencies) {
|
|
745
|
-
if (dfs(dep,
|
|
969
|
+
if (dfs(dep, path17)) return true;
|
|
746
970
|
}
|
|
747
971
|
}
|
|
748
972
|
visiting.delete(name);
|
|
749
973
|
visited.add(name);
|
|
750
|
-
|
|
974
|
+
path17.pop();
|
|
751
975
|
return false;
|
|
752
976
|
}
|
|
753
977
|
for (const phase of phases) {
|
|
@@ -903,11 +1127,18 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
903
1127
|
".cache",
|
|
904
1128
|
"tmp",
|
|
905
1129
|
"temp",
|
|
906
|
-
".parcel-cache"
|
|
1130
|
+
".parcel-cache",
|
|
1131
|
+
".venv",
|
|
1132
|
+
"venv",
|
|
1133
|
+
".env",
|
|
1134
|
+
"env",
|
|
1135
|
+
"__snapshots__",
|
|
1136
|
+
".nyc_output",
|
|
1137
|
+
"storybook-static"
|
|
907
1138
|
]);
|
|
908
1139
|
function loadIgnorePatterns(workspaceRoot) {
|
|
909
1140
|
try {
|
|
910
|
-
const raw =
|
|
1141
|
+
const raw = fs14.readFileSync(path16.join(workspaceRoot, ".codeintelignore"), "utf-8");
|
|
911
1142
|
const extras = /* @__PURE__ */ new Set();
|
|
912
1143
|
for (const line of raw.split("\n")) {
|
|
913
1144
|
const trimmed = line.trim();
|
|
@@ -918,6 +1149,8 @@ function loadIgnorePatterns(workspaceRoot) {
|
|
|
918
1149
|
return /* @__PURE__ */ new Set();
|
|
919
1150
|
}
|
|
920
1151
|
}
|
|
1152
|
+
var IGNORED_FILE_SUFFIXES = [".d.ts", ".js.map", ".d.ts.map", ".min.js", ".min.css"];
|
|
1153
|
+
var MAX_FILE_SIZE_BYTES = 512 * 1024;
|
|
921
1154
|
var scanPhase = {
|
|
922
1155
|
name: "scan",
|
|
923
1156
|
dependencies: [],
|
|
@@ -929,29 +1162,35 @@ var scanPhase = {
|
|
|
929
1162
|
function walk(dir) {
|
|
930
1163
|
let entries;
|
|
931
1164
|
try {
|
|
932
|
-
entries =
|
|
1165
|
+
entries = fs14.readdirSync(dir, { withFileTypes: true });
|
|
933
1166
|
} catch {
|
|
934
1167
|
return;
|
|
935
1168
|
}
|
|
936
1169
|
for (const entry of entries) {
|
|
937
|
-
if (entry.name.startsWith(".") && entry.isDirectory()) continue;
|
|
938
|
-
if (IGNORED_DIRS.has(entry.name) && entry.isDirectory()) continue;
|
|
939
|
-
if (extraIgnore.has(entry.name) && entry.isDirectory()) continue;
|
|
940
|
-
const fullPath = path.join(dir, entry.name);
|
|
941
1170
|
if (entry.isDirectory()) {
|
|
942
|
-
|
|
1171
|
+
if (entry.name.startsWith(".")) continue;
|
|
1172
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
1173
|
+
if (extraIgnore.has(entry.name)) continue;
|
|
1174
|
+
walk(path16.join(dir, entry.name));
|
|
943
1175
|
} else if (entry.isFile()) {
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
if (extensions.has(ext))
|
|
948
|
-
|
|
1176
|
+
const name = entry.name;
|
|
1177
|
+
if (IGNORED_FILE_SUFFIXES.some((s) => name.endsWith(s))) continue;
|
|
1178
|
+
const ext = path16.extname(name);
|
|
1179
|
+
if (!extensions.has(ext)) continue;
|
|
1180
|
+
const fullPath = path16.join(dir, name);
|
|
1181
|
+
try {
|
|
1182
|
+
const stat = fs14.statSync(fullPath);
|
|
1183
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) continue;
|
|
1184
|
+
} catch {
|
|
1185
|
+
continue;
|
|
949
1186
|
}
|
|
1187
|
+
filePaths.push(fullPath);
|
|
950
1188
|
}
|
|
951
1189
|
}
|
|
952
1190
|
}
|
|
953
1191
|
walk(context.workspaceRoot);
|
|
954
1192
|
context.filePaths.push(...filePaths);
|
|
1193
|
+
context.onPhaseProgress?.("scan", filePaths.length, filePaths.length);
|
|
955
1194
|
return {
|
|
956
1195
|
status: "completed",
|
|
957
1196
|
duration: Date.now() - start,
|
|
@@ -965,28 +1204,31 @@ var structurePhase = {
|
|
|
965
1204
|
async execute(context) {
|
|
966
1205
|
const start = Date.now();
|
|
967
1206
|
const dirs = /* @__PURE__ */ new Set();
|
|
1207
|
+
let structDone = 0;
|
|
968
1208
|
for (const filePath of context.filePaths) {
|
|
969
|
-
const relativePath =
|
|
1209
|
+
const relativePath = path16.relative(context.workspaceRoot, filePath);
|
|
970
1210
|
const lang = detectLanguage(filePath);
|
|
971
1211
|
context.graph.addNode({
|
|
972
1212
|
id: generateNodeId("file", relativePath, relativePath),
|
|
973
1213
|
kind: "file",
|
|
974
|
-
name:
|
|
1214
|
+
name: path16.basename(filePath),
|
|
975
1215
|
filePath: relativePath,
|
|
976
1216
|
metadata: lang ? { language: lang } : void 0
|
|
977
1217
|
});
|
|
978
|
-
let dir =
|
|
1218
|
+
let dir = path16.dirname(relativePath);
|
|
979
1219
|
while (dir && dir !== "." && dir !== "") {
|
|
980
1220
|
if (dirs.has(dir)) break;
|
|
981
1221
|
dirs.add(dir);
|
|
982
|
-
dir =
|
|
1222
|
+
dir = path16.dirname(dir);
|
|
983
1223
|
}
|
|
1224
|
+
structDone++;
|
|
1225
|
+
context.onPhaseProgress?.("structure", structDone, context.filePaths.length);
|
|
984
1226
|
}
|
|
985
1227
|
for (const dir of dirs) {
|
|
986
1228
|
context.graph.addNode({
|
|
987
1229
|
id: generateNodeId("directory", dir, dir),
|
|
988
1230
|
kind: "directory",
|
|
989
|
-
name:
|
|
1231
|
+
name: path16.basename(dir),
|
|
990
1232
|
filePath: dir
|
|
991
1233
|
});
|
|
992
1234
|
}
|
|
@@ -1003,23 +1245,37 @@ var parsePhase = {
|
|
|
1003
1245
|
async execute(context) {
|
|
1004
1246
|
const start = Date.now();
|
|
1005
1247
|
let symbolCount = 0;
|
|
1006
|
-
|
|
1248
|
+
if (!context.fileCache) context.fileCache = /* @__PURE__ */ new Map();
|
|
1249
|
+
if (!context.fileFunctionIndex) context.fileFunctionIndex = /* @__PURE__ */ new Map();
|
|
1250
|
+
const CONCURRENCY = 64;
|
|
1251
|
+
const filePaths = context.filePaths;
|
|
1252
|
+
let readDone = 0;
|
|
1253
|
+
for (let i = 0; i < filePaths.length; i += CONCURRENCY) {
|
|
1254
|
+
const batch = filePaths.slice(i, i + CONCURRENCY);
|
|
1255
|
+
await Promise.all(batch.map(async (filePath) => {
|
|
1256
|
+
try {
|
|
1257
|
+
const source = await fs14.promises.readFile(filePath, "utf-8");
|
|
1258
|
+
context.fileCache.set(filePath, source);
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
}));
|
|
1262
|
+
readDone += batch.length;
|
|
1263
|
+
context.onPhaseProgress?.("parse:read", readDone, filePaths.length);
|
|
1264
|
+
}
|
|
1265
|
+
let parseDone = 0;
|
|
1266
|
+
for (const filePath of filePaths) {
|
|
1007
1267
|
const lang = detectLanguage(filePath);
|
|
1008
1268
|
if (!lang) {
|
|
1009
1269
|
if (context.verbose) {
|
|
1010
|
-
const relativePath2 =
|
|
1011
|
-
|
|
1270
|
+
const relativePath2 = path16.relative(context.workspaceRoot, filePath);
|
|
1271
|
+
logger_default.info(` [parse] skipped (no parser): ${relativePath2}`);
|
|
1012
1272
|
}
|
|
1013
1273
|
continue;
|
|
1014
1274
|
}
|
|
1015
|
-
const
|
|
1275
|
+
const source = context.fileCache.get(filePath);
|
|
1276
|
+
if (!source) continue;
|
|
1277
|
+
const relativePath = path16.relative(context.workspaceRoot, filePath);
|
|
1016
1278
|
const fileNodeId = generateNodeId("file", relativePath, relativePath);
|
|
1017
|
-
let source;
|
|
1018
|
-
try {
|
|
1019
|
-
source = fs8.readFileSync(filePath, "utf-8");
|
|
1020
|
-
} catch {
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
1279
|
const fileNode = context.graph.getNode(fileNodeId);
|
|
1024
1280
|
if (fileNode) {
|
|
1025
1281
|
fileNode.content = source.slice(0, 2e3);
|
|
@@ -1034,15 +1290,18 @@ var parsePhase = {
|
|
|
1034
1290
|
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
1035
1291
|
const extracted = extractSymbol(trimmed, lang);
|
|
1036
1292
|
if (!extracted) continue;
|
|
1037
|
-
|
|
1038
|
-
seen.
|
|
1293
|
+
const dedupeKey = extracted.name + ":" + extracted.kind;
|
|
1294
|
+
if (seen.has(dedupeKey)) continue;
|
|
1295
|
+
seen.add(dedupeKey);
|
|
1039
1296
|
const nodeId = generateNodeId(extracted.kind, relativePath, extracted.name);
|
|
1297
|
+
const endLine = estimateEndLine(lines, i, lang);
|
|
1040
1298
|
nodes.push({
|
|
1041
1299
|
id: nodeId,
|
|
1042
1300
|
kind: extracted.kind,
|
|
1043
1301
|
name: extracted.name,
|
|
1044
1302
|
filePath: relativePath,
|
|
1045
1303
|
startLine: i + 1,
|
|
1304
|
+
endLine,
|
|
1046
1305
|
exported: extracted.exported,
|
|
1047
1306
|
content: extractBlock(lines, i, 20)
|
|
1048
1307
|
});
|
|
@@ -1069,11 +1328,17 @@ var parsePhase = {
|
|
|
1069
1328
|
}
|
|
1070
1329
|
for (const n of nodes) context.graph.addNode(n);
|
|
1071
1330
|
for (const e of edges) context.graph.addEdge(e);
|
|
1331
|
+
const funcs = nodes.filter((n) => n.kind === "function" || n.kind === "method").map((n) => ({ id: n.id, startLine: n.startLine ?? 0, endLine: n.endLine })).sort((a, b) => a.startLine - b.startLine);
|
|
1332
|
+
if (funcs.length > 0) {
|
|
1333
|
+
context.fileFunctionIndex.set(relativePath, funcs);
|
|
1334
|
+
}
|
|
1335
|
+
parseDone++;
|
|
1336
|
+
context.onPhaseProgress?.("parse", parseDone, filePaths.length);
|
|
1072
1337
|
}
|
|
1073
1338
|
return {
|
|
1074
1339
|
status: "completed",
|
|
1075
1340
|
duration: Date.now() - start,
|
|
1076
|
-
message: `Extracted ${symbolCount} symbols`
|
|
1341
|
+
message: `Extracted ${symbolCount} symbols from ${filePaths.length} files`
|
|
1077
1342
|
};
|
|
1078
1343
|
}
|
|
1079
1344
|
};
|
|
@@ -1097,12 +1362,6 @@ function extractSymbol(line, lang, _lineNum, _filePath) {
|
|
|
1097
1362
|
if (constVar && /^[A-Z_]+$/.test(constVar[1])) {
|
|
1098
1363
|
return { kind: "constant", name: constVar[1], exported: line.includes("export") };
|
|
1099
1364
|
}
|
|
1100
|
-
const method = line.match(/^(?:(?:public|private|protected|static|async|readonly)\s+)*(\w+)\s*\(/);
|
|
1101
|
-
if (method && !["if", "for", "while", "switch", "catch", "return", "constructor"].includes(method[1])) {
|
|
1102
|
-
if (method[1] === "constructor") {
|
|
1103
|
-
return { kind: "constructor", name: "constructor", exported: false };
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
1365
|
}
|
|
1107
1366
|
if (lang === "python" /* Python */) {
|
|
1108
1367
|
const func = line.match(/^(?:async\s+)?def\s+(\w+)/);
|
|
@@ -1214,32 +1473,100 @@ function extractSymbol(line, lang, _lineNum, _filePath) {
|
|
|
1214
1473
|
}
|
|
1215
1474
|
return null;
|
|
1216
1475
|
}
|
|
1476
|
+
function estimateEndLine(lines, startIdx, lang) {
|
|
1477
|
+
const MAX_SCAN = 200;
|
|
1478
|
+
const end = Math.min(startIdx + MAX_SCAN, lines.length);
|
|
1479
|
+
if (lang !== "python" /* Python */ && lang !== "ruby" /* Ruby */) {
|
|
1480
|
+
let depth = 0;
|
|
1481
|
+
let foundOpen = false;
|
|
1482
|
+
for (let i = startIdx; i < end; i++) {
|
|
1483
|
+
for (const ch of lines[i]) {
|
|
1484
|
+
if (ch === "{") {
|
|
1485
|
+
depth++;
|
|
1486
|
+
foundOpen = true;
|
|
1487
|
+
} else if (ch === "}") {
|
|
1488
|
+
depth--;
|
|
1489
|
+
if (foundOpen && depth === 0) return i + 1;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return void 0;
|
|
1494
|
+
}
|
|
1495
|
+
const startIndent = (lines[startIdx].match(/^(\s*)/) ?? ["", ""])[1].length;
|
|
1496
|
+
for (let i = startIdx + 1; i < end; i++) {
|
|
1497
|
+
const l = lines[i];
|
|
1498
|
+
if (l.trim() === "") continue;
|
|
1499
|
+
const indent = (l.match(/^(\s*)/) ?? ["", ""])[1].length;
|
|
1500
|
+
if (indent <= startIndent && l.trim() !== "") return i;
|
|
1501
|
+
}
|
|
1502
|
+
return void 0;
|
|
1503
|
+
}
|
|
1217
1504
|
function extractBlock(lines, startIdx, maxLines) {
|
|
1218
1505
|
const end = Math.min(startIdx + maxLines, lines.length);
|
|
1219
1506
|
return lines.slice(startIdx, end).join("\n");
|
|
1220
1507
|
}
|
|
1508
|
+
var CALL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1509
|
+
"if",
|
|
1510
|
+
"for",
|
|
1511
|
+
"while",
|
|
1512
|
+
"switch",
|
|
1513
|
+
"catch",
|
|
1514
|
+
"return",
|
|
1515
|
+
"throw",
|
|
1516
|
+
"typeof",
|
|
1517
|
+
"instanceof",
|
|
1518
|
+
"delete",
|
|
1519
|
+
"void",
|
|
1520
|
+
"new",
|
|
1521
|
+
"import",
|
|
1522
|
+
"export",
|
|
1523
|
+
"from",
|
|
1524
|
+
"const",
|
|
1525
|
+
"let",
|
|
1526
|
+
"var",
|
|
1527
|
+
"function",
|
|
1528
|
+
"class",
|
|
1529
|
+
"interface",
|
|
1530
|
+
"type",
|
|
1531
|
+
"enum",
|
|
1532
|
+
"extends",
|
|
1533
|
+
"implements"
|
|
1534
|
+
]);
|
|
1221
1535
|
var resolvePhase = {
|
|
1222
1536
|
name: "resolve",
|
|
1223
1537
|
dependencies: ["parse"],
|
|
1224
1538
|
async execute(context) {
|
|
1225
1539
|
const start = Date.now();
|
|
1226
1540
|
const { graph, workspaceRoot, filePaths } = context;
|
|
1541
|
+
const fileCache = context.fileCache ?? /* @__PURE__ */ new Map();
|
|
1542
|
+
const fileFunctionIndex = context.fileFunctionIndex ?? /* @__PURE__ */ new Map();
|
|
1227
1543
|
let importEdges = 0;
|
|
1228
1544
|
let callEdges = 0;
|
|
1229
1545
|
let heritageEdges = 0;
|
|
1230
1546
|
const fileIndex = /* @__PURE__ */ new Map();
|
|
1231
1547
|
for (const fp of filePaths) {
|
|
1232
|
-
const rel =
|
|
1548
|
+
const rel = path16.relative(workspaceRoot, fp);
|
|
1233
1549
|
fileIndex.set(rel, fp);
|
|
1234
1550
|
const noExt = rel.replace(/\.\w+$/, "");
|
|
1235
1551
|
if (!fileIndex.has(noExt)) fileIndex.set(noExt, fp);
|
|
1236
|
-
const base =
|
|
1552
|
+
const base = path16.basename(rel, path16.extname(rel));
|
|
1237
1553
|
if (!fileIndex.has(base)) fileIndex.set(base, fp);
|
|
1238
1554
|
}
|
|
1239
1555
|
const symbolIndex = /* @__PURE__ */ new Map();
|
|
1240
1556
|
const fileSymbolIndex = /* @__PURE__ */ new Map();
|
|
1241
1557
|
for (const node of graph.allNodes()) {
|
|
1242
|
-
if ([
|
|
1558
|
+
if ([
|
|
1559
|
+
"function",
|
|
1560
|
+
"class",
|
|
1561
|
+
"interface",
|
|
1562
|
+
"method",
|
|
1563
|
+
"enum",
|
|
1564
|
+
"type_alias",
|
|
1565
|
+
"variable",
|
|
1566
|
+
"constant",
|
|
1567
|
+
"struct",
|
|
1568
|
+
"trait"
|
|
1569
|
+
].includes(node.kind)) {
|
|
1243
1570
|
symbolIndex.set(node.name, node.id);
|
|
1244
1571
|
let fileMap = fileSymbolIndex.get(node.filePath);
|
|
1245
1572
|
if (!fileMap) {
|
|
@@ -1249,17 +1576,14 @@ var resolvePhase = {
|
|
|
1249
1576
|
fileMap.set(node.name, node.id);
|
|
1250
1577
|
}
|
|
1251
1578
|
}
|
|
1579
|
+
let fileDone = 0;
|
|
1252
1580
|
for (const filePath of filePaths) {
|
|
1253
1581
|
const lang = detectLanguage(filePath);
|
|
1254
1582
|
if (!lang) continue;
|
|
1255
|
-
const relativePath =
|
|
1583
|
+
const relativePath = path16.relative(workspaceRoot, filePath);
|
|
1256
1584
|
const fileNodeId = generateNodeId("file", relativePath, relativePath);
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
source = fs8.readFileSync(filePath, "utf-8");
|
|
1260
|
-
} catch {
|
|
1261
|
-
continue;
|
|
1262
|
-
}
|
|
1585
|
+
const source = fileCache.get(filePath);
|
|
1586
|
+
if (!source) continue;
|
|
1263
1587
|
const lines = source.split("\n");
|
|
1264
1588
|
const imports = extractImports(lines, lang === "python");
|
|
1265
1589
|
const calls = extractCalls(lines);
|
|
@@ -1269,13 +1593,13 @@ var resolvePhase = {
|
|
|
1269
1593
|
let resolvedRelPath = null;
|
|
1270
1594
|
if (cleaned.startsWith(".")) {
|
|
1271
1595
|
const cleanedNoJs = cleaned.replace(/\.(js|jsx)$/, "");
|
|
1272
|
-
const fromDir =
|
|
1596
|
+
const fromDir = path16.dirname(relativePath);
|
|
1273
1597
|
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", "/index.ts", "/index.js"]) {
|
|
1274
|
-
const candidate =
|
|
1275
|
-
const normalized =
|
|
1598
|
+
const candidate = path16.join(fromDir, cleanedNoJs + ext);
|
|
1599
|
+
const normalized = path16.normalize(candidate);
|
|
1276
1600
|
if (fileIndex.has(normalized)) {
|
|
1277
1601
|
const absPath = fileIndex.get(normalized);
|
|
1278
|
-
resolvedRelPath =
|
|
1602
|
+
resolvedRelPath = path16.relative(workspaceRoot, absPath);
|
|
1279
1603
|
break;
|
|
1280
1604
|
}
|
|
1281
1605
|
}
|
|
@@ -1286,11 +1610,13 @@ var resolvePhase = {
|
|
|
1286
1610
|
break;
|
|
1287
1611
|
}
|
|
1288
1612
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1613
|
+
if (!resolvedRelPath) {
|
|
1614
|
+
const asPath = cleaned.replace(/\./g, "/");
|
|
1615
|
+
for (const ext of ["", ".ts", ".js", ".py", ".java", ".go", "/index.ts", "/__init__.py"]) {
|
|
1616
|
+
if (fileIndex.has(asPath + ext)) {
|
|
1617
|
+
resolvedRelPath = asPath + ext;
|
|
1618
|
+
break;
|
|
1619
|
+
}
|
|
1294
1620
|
}
|
|
1295
1621
|
}
|
|
1296
1622
|
}
|
|
@@ -1313,6 +1639,7 @@ var resolvePhase = {
|
|
|
1313
1639
|
}
|
|
1314
1640
|
}
|
|
1315
1641
|
const localSymbols = fileSymbolIndex.get(relativePath);
|
|
1642
|
+
const funcList = fileFunctionIndex.get(relativePath);
|
|
1316
1643
|
for (const call of calls) {
|
|
1317
1644
|
let targetId = localSymbols?.get(call.name);
|
|
1318
1645
|
let confidence = 0.95;
|
|
@@ -1321,7 +1648,7 @@ var resolvePhase = {
|
|
|
1321
1648
|
confidence = 0.5;
|
|
1322
1649
|
}
|
|
1323
1650
|
if (targetId) {
|
|
1324
|
-
const callerNodeId =
|
|
1651
|
+
const callerNodeId = funcList ? findEnclosingFunctionFast(funcList, call.line) : null;
|
|
1325
1652
|
const sourceId = callerNodeId ?? fileNodeId;
|
|
1326
1653
|
if (sourceId !== targetId) {
|
|
1327
1654
|
const edgeId = generateEdgeId(sourceId, targetId, "calls");
|
|
@@ -1377,6 +1704,8 @@ var resolvePhase = {
|
|
|
1377
1704
|
}
|
|
1378
1705
|
}
|
|
1379
1706
|
}
|
|
1707
|
+
fileDone++;
|
|
1708
|
+
context.onPhaseProgress?.("resolve", fileDone, filePaths.length);
|
|
1380
1709
|
}
|
|
1381
1710
|
return {
|
|
1382
1711
|
status: "completed",
|
|
@@ -1394,18 +1723,13 @@ function extractImports(lines, isPython) {
|
|
|
1394
1723
|
const names = [];
|
|
1395
1724
|
const namedMatch = line.match(/\{([^}]+)\}/);
|
|
1396
1725
|
if (namedMatch) {
|
|
1397
|
-
names.push(
|
|
1726
|
+
names.push(
|
|
1727
|
+
...namedMatch[1].split(",").map((n) => n.trim().split(/\s+as\s+/).pop().trim()).filter(Boolean)
|
|
1728
|
+
);
|
|
1398
1729
|
}
|
|
1399
1730
|
const defaultMatch = line.match(/import\s+(\w+)/);
|
|
1400
|
-
if (defaultMatch && defaultMatch[1] !== "type")
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
imports.push({
|
|
1404
|
-
rawPath: tsImport[1],
|
|
1405
|
-
localNames: names,
|
|
1406
|
-
isDefault: !namedMatch,
|
|
1407
|
-
line: i + 1
|
|
1408
|
-
});
|
|
1731
|
+
if (defaultMatch && defaultMatch[1] !== "type") names.push(defaultMatch[1]);
|
|
1732
|
+
imports.push({ rawPath: tsImport[1], localNames: names, isDefault: !namedMatch, line: i + 1 });
|
|
1409
1733
|
continue;
|
|
1410
1734
|
}
|
|
1411
1735
|
if (isPython) {
|
|
@@ -1429,64 +1753,34 @@ function extractImports(lines, isPython) {
|
|
|
1429
1753
|
const javaImport = line.match(/^import\s+(?:static\s+)?([\w.]+)/);
|
|
1430
1754
|
if (javaImport && !line.includes("from")) {
|
|
1431
1755
|
const parts = javaImport[1].split(".");
|
|
1432
|
-
imports.push({
|
|
1433
|
-
rawPath: javaImport[1],
|
|
1434
|
-
localNames: [parts[parts.length - 1]],
|
|
1435
|
-
isDefault: false,
|
|
1436
|
-
line: i + 1
|
|
1437
|
-
});
|
|
1756
|
+
imports.push({ rawPath: javaImport[1], localNames: [parts[parts.length - 1]], isDefault: false, line: i + 1 });
|
|
1438
1757
|
continue;
|
|
1439
1758
|
}
|
|
1440
1759
|
const goImport = line.match(/^\s*"([^"]+)"/);
|
|
1441
1760
|
if (goImport && (i > 0 && lines[i - 1]?.includes("import") || line.match(/^import\s+"/))) {
|
|
1442
1761
|
const parts = goImport[1].split("/");
|
|
1443
|
-
imports.push({
|
|
1444
|
-
rawPath: goImport[1],
|
|
1445
|
-
localNames: [parts[parts.length - 1]],
|
|
1446
|
-
isDefault: false,
|
|
1447
|
-
line: i + 1
|
|
1448
|
-
});
|
|
1762
|
+
imports.push({ rawPath: goImport[1], localNames: [parts[parts.length - 1]], isDefault: false, line: i + 1 });
|
|
1449
1763
|
continue;
|
|
1450
1764
|
}
|
|
1451
1765
|
const includeMatch = line.match(/#include\s+[<"]([^>"]+)[>"]/);
|
|
1452
1766
|
if (includeMatch) {
|
|
1453
|
-
imports.push({
|
|
1454
|
-
rawPath: includeMatch[1],
|
|
1455
|
-
localNames: [],
|
|
1456
|
-
isDefault: false,
|
|
1457
|
-
line: i + 1
|
|
1458
|
-
});
|
|
1767
|
+
imports.push({ rawPath: includeMatch[1], localNames: [], isDefault: false, line: i + 1 });
|
|
1459
1768
|
continue;
|
|
1460
1769
|
}
|
|
1461
1770
|
const rustUse = line.match(/^use\s+([\w:]+)/);
|
|
1462
1771
|
if (rustUse) {
|
|
1463
1772
|
const parts = rustUse[1].split("::");
|
|
1464
|
-
imports.push({
|
|
1465
|
-
rawPath: rustUse[1],
|
|
1466
|
-
localNames: [parts[parts.length - 1]],
|
|
1467
|
-
isDefault: false,
|
|
1468
|
-
line: i + 1
|
|
1469
|
-
});
|
|
1773
|
+
imports.push({ rawPath: rustUse[1], localNames: [parts[parts.length - 1]], isDefault: false, line: i + 1 });
|
|
1470
1774
|
continue;
|
|
1471
1775
|
}
|
|
1472
1776
|
const usingMatch = line.match(/^using\s+([\w.]+)/);
|
|
1473
1777
|
if (usingMatch) {
|
|
1474
1778
|
const parts = usingMatch[1].split(".");
|
|
1475
|
-
imports.push({
|
|
1476
|
-
rawPath: usingMatch[1],
|
|
1477
|
-
localNames: [parts[parts.length - 1]],
|
|
1478
|
-
isDefault: false,
|
|
1479
|
-
line: i + 1
|
|
1480
|
-
});
|
|
1779
|
+
imports.push({ rawPath: usingMatch[1], localNames: [parts[parts.length - 1]], isDefault: false, line: i + 1 });
|
|
1481
1780
|
}
|
|
1482
1781
|
const requireMatch = line.match(/require\s+['"]([^'"]+)['"]/);
|
|
1483
1782
|
if (requireMatch) {
|
|
1484
|
-
imports.push({
|
|
1485
|
-
rawPath: requireMatch[1],
|
|
1486
|
-
localNames: [],
|
|
1487
|
-
isDefault: false,
|
|
1488
|
-
line: i + 1
|
|
1489
|
-
});
|
|
1783
|
+
imports.push({ rawPath: requireMatch[1], localNames: [], isDefault: false, line: i + 1 });
|
|
1490
1784
|
}
|
|
1491
1785
|
}
|
|
1492
1786
|
return imports;
|
|
@@ -1507,19 +1801,14 @@ function extractCalls(lines) {
|
|
|
1507
1801
|
callRegex.lastIndex = 0;
|
|
1508
1802
|
while ((match = callRegex.exec(line)) !== null) {
|
|
1509
1803
|
const name = match[1];
|
|
1510
|
-
if (
|
|
1804
|
+
if (CALL_KEYWORDS.has(name)) continue;
|
|
1511
1805
|
const isNew = line.substring(Math.max(0, match.index - 4), match.index).includes("new");
|
|
1512
1806
|
calls.push({ name, isNew, line: i + 1 });
|
|
1513
1807
|
}
|
|
1514
1808
|
const memberCallRegex = /(\w+)\.(\w+)\s*\(/g;
|
|
1515
1809
|
memberCallRegex.lastIndex = 0;
|
|
1516
1810
|
while ((match = memberCallRegex.exec(line)) !== null) {
|
|
1517
|
-
calls.push({
|
|
1518
|
-
name: match[2],
|
|
1519
|
-
receiverText: match[1],
|
|
1520
|
-
isNew: false,
|
|
1521
|
-
line: i + 1
|
|
1522
|
-
});
|
|
1811
|
+
calls.push({ name: match[2], receiverText: match[1], isNew: false, line: i + 1 });
|
|
1523
1812
|
}
|
|
1524
1813
|
}
|
|
1525
1814
|
return calls;
|
|
@@ -1542,19 +1831,23 @@ function extractHeritage(lines) {
|
|
|
1542
1831
|
}
|
|
1543
1832
|
return heritages;
|
|
1544
1833
|
}
|
|
1545
|
-
function
|
|
1834
|
+
function findEnclosingFunctionFast(funcs, line) {
|
|
1835
|
+
let lo = 0;
|
|
1836
|
+
let hi = funcs.length - 1;
|
|
1546
1837
|
let best = null;
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
if (
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
best = { id: node.id, startLine: node.startLine };
|
|
1838
|
+
while (lo <= hi) {
|
|
1839
|
+
const mid = lo + hi >> 1;
|
|
1840
|
+
const fn = funcs[mid];
|
|
1841
|
+
if (fn.startLine <= line) {
|
|
1842
|
+
if (fn.endLine === void 0 || line <= fn.endLine) {
|
|
1843
|
+
best = fn.id;
|
|
1554
1844
|
}
|
|
1845
|
+
lo = mid + 1;
|
|
1846
|
+
} else {
|
|
1847
|
+
hi = mid - 1;
|
|
1555
1848
|
}
|
|
1556
1849
|
}
|
|
1557
|
-
return best
|
|
1850
|
+
return best;
|
|
1558
1851
|
}
|
|
1559
1852
|
|
|
1560
1853
|
// src/pipeline/phases/cluster-phase.ts
|
|
@@ -1577,7 +1870,11 @@ var clusterPhase = {
|
|
|
1577
1870
|
group.push({ id: node.id, name: node.name });
|
|
1578
1871
|
}
|
|
1579
1872
|
let clusterCount = 0;
|
|
1580
|
-
|
|
1873
|
+
const dirEntries = [...nodesByDir.entries()];
|
|
1874
|
+
let clusterDone = 0;
|
|
1875
|
+
for (const [dir, members] of dirEntries) {
|
|
1876
|
+
clusterDone++;
|
|
1877
|
+
context.onPhaseProgress?.("cluster", clusterDone, dirEntries.length);
|
|
1581
1878
|
if (members.length < 2) continue;
|
|
1582
1879
|
const clusterId = generateNodeId("cluster", dir, `cluster-${clusterCount}`);
|
|
1583
1880
|
const label = dir.split("/").filter(Boolean).pop() ?? `cluster-${clusterCount}`;
|
|
@@ -1638,27 +1935,30 @@ var flowPhase = {
|
|
|
1638
1935
|
const maxDepth = 10;
|
|
1639
1936
|
const maxBranching = 4;
|
|
1640
1937
|
let flowCount = 0;
|
|
1641
|
-
|
|
1938
|
+
const epSlice = entryPoints.slice(0, 20);
|
|
1939
|
+
for (let epIdx = 0; epIdx < epSlice.length; epIdx++) {
|
|
1940
|
+
const ep = epSlice[epIdx];
|
|
1941
|
+
context.onPhaseProgress?.("flow", epIdx + 1, epSlice.length);
|
|
1642
1942
|
if (flowCount >= maxFlows) break;
|
|
1643
1943
|
const queue = [{ nodeId: ep.id, path: [ep.id] }];
|
|
1644
1944
|
const visited = /* @__PURE__ */ new Set();
|
|
1645
1945
|
while (queue.length > 0 && flowCount < maxFlows) {
|
|
1646
|
-
const { nodeId, path:
|
|
1647
|
-
if (
|
|
1946
|
+
const { nodeId, path: path17 } = queue.shift();
|
|
1947
|
+
if (path17.length > maxDepth) continue;
|
|
1648
1948
|
const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
|
|
1649
|
-
if (callEdges.length === 0 &&
|
|
1949
|
+
if (callEdges.length === 0 && path17.length >= 3) {
|
|
1650
1950
|
const flowId = generateNodeId("flow", ep.filePath, `flow-${flowCount}`);
|
|
1651
1951
|
graph.addNode({
|
|
1652
1952
|
id: flowId,
|
|
1653
1953
|
kind: "flow",
|
|
1654
1954
|
name: `${ep.name} flow ${flowCount}`,
|
|
1655
1955
|
filePath: ep.filePath,
|
|
1656
|
-
metadata: { steps:
|
|
1956
|
+
metadata: { steps: path17, entryPoint: ep.name }
|
|
1657
1957
|
});
|
|
1658
|
-
for (let i = 0; i <
|
|
1958
|
+
for (let i = 0; i < path17.length; i++) {
|
|
1659
1959
|
graph.addEdge({
|
|
1660
|
-
id: generateEdgeId(
|
|
1661
|
-
source:
|
|
1960
|
+
id: generateEdgeId(path17[i], flowId, `step_of_${i}`),
|
|
1961
|
+
source: path17[i],
|
|
1662
1962
|
target: flowId,
|
|
1663
1963
|
kind: "step_of",
|
|
1664
1964
|
weight: 1,
|
|
@@ -1671,7 +1971,7 @@ var flowPhase = {
|
|
|
1671
1971
|
for (const edge of callEdges) {
|
|
1672
1972
|
if (visited.has(edge.target)) continue;
|
|
1673
1973
|
visited.add(edge.target);
|
|
1674
|
-
queue.push({ nodeId: edge.target, path: [...
|
|
1974
|
+
queue.push({ nodeId: edge.target, path: [...path17, edge.target] });
|
|
1675
1975
|
}
|
|
1676
1976
|
}
|
|
1677
1977
|
}
|
|
@@ -1940,12 +2240,12 @@ async function syncGroup(group) {
|
|
|
1940
2240
|
for (const member of group.members) {
|
|
1941
2241
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
1942
2242
|
if (!regEntry) {
|
|
1943
|
-
|
|
2243
|
+
logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
|
|
1944
2244
|
continue;
|
|
1945
2245
|
}
|
|
1946
|
-
const dbPath =
|
|
1947
|
-
if (!
|
|
1948
|
-
|
|
2246
|
+
const dbPath = path16.join(regEntry.path, ".code-intel", "graph.db");
|
|
2247
|
+
if (!fs14.existsSync(dbPath)) {
|
|
2248
|
+
logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
|
|
1949
2249
|
continue;
|
|
1950
2250
|
}
|
|
1951
2251
|
const graph = createKnowledgeGraph();
|
|
@@ -1956,11 +2256,11 @@ async function syncGroup(group) {
|
|
|
1956
2256
|
db.close();
|
|
1957
2257
|
} catch (err) {
|
|
1958
2258
|
db.close();
|
|
1959
|
-
|
|
2259
|
+
logger_default.warn(` \u26A0 Could not load graph for "${member.registryName}": ${err instanceof Error ? err.message : err}`);
|
|
1960
2260
|
continue;
|
|
1961
2261
|
}
|
|
1962
2262
|
const contracts = extractContracts(graph, member.registryName, regEntry.path);
|
|
1963
|
-
|
|
2263
|
+
logger_default.info(` \u2713 ${member.registryName} (${member.groupPath}): ${contracts.length} contracts`);
|
|
1964
2264
|
allContracts.push(...contracts);
|
|
1965
2265
|
}
|
|
1966
2266
|
const links = matchContracts(allContracts);
|
|
@@ -1981,8 +2281,8 @@ async function queryGroup(group, query, limit = 20) {
|
|
|
1981
2281
|
for (const member of group.members) {
|
|
1982
2282
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
1983
2283
|
if (!regEntry) continue;
|
|
1984
|
-
const dbPath =
|
|
1985
|
-
if (!
|
|
2284
|
+
const dbPath = path16.join(regEntry.path, ".code-intel", "graph.db");
|
|
2285
|
+
if (!fs14.existsSync(dbPath)) continue;
|
|
1986
2286
|
const graph = createKnowledgeGraph();
|
|
1987
2287
|
const db = new DbManager(dbPath);
|
|
1988
2288
|
try {
|
|
@@ -2012,8 +2312,8 @@ async function queryGroup(group, query, limit = 20) {
|
|
|
2012
2312
|
|
|
2013
2313
|
// src/http/app.ts
|
|
2014
2314
|
init_repo_registry();
|
|
2015
|
-
var __dirname$1 =
|
|
2016
|
-
var WEB_DIST =
|
|
2315
|
+
var __dirname$1 = path16.dirname(fileURLToPath(import.meta.url));
|
|
2316
|
+
var WEB_DIST = path16.resolve(__dirname$1, "..", "..", "..", "web", "dist");
|
|
2017
2317
|
function createApp(graph, repoName, workspaceRoot) {
|
|
2018
2318
|
const app = express();
|
|
2019
2319
|
app.use(cors({ origin: true }));
|
|
@@ -2034,23 +2334,23 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2034
2334
|
await idx.init();
|
|
2035
2335
|
const alreadyBuilt = await idx.isBuilt();
|
|
2036
2336
|
if (!alreadyBuilt) {
|
|
2037
|
-
|
|
2337
|
+
logger_default.info(" [vector] Building embeddings\u2026");
|
|
2038
2338
|
const nodes = await embedNodes2(graph, {
|
|
2039
2339
|
onProgress: (done, total) => {
|
|
2040
2340
|
if (done % 50 === 0 || done === total) process.stdout.write(`\r [vector] ${done}/${total}`);
|
|
2041
2341
|
}
|
|
2042
2342
|
});
|
|
2043
|
-
|
|
2343
|
+
logger_default.info("");
|
|
2044
2344
|
await idx.buildIndex(nodes);
|
|
2045
|
-
|
|
2345
|
+
logger_default.info(` [vector] Index built: ${nodes.length} embeddings`);
|
|
2046
2346
|
} else {
|
|
2047
|
-
|
|
2347
|
+
logger_default.info(" [vector] Index already exists, skipping rebuild.");
|
|
2048
2348
|
}
|
|
2049
2349
|
vectorIndex = idx;
|
|
2050
2350
|
vectorIndexReady = true;
|
|
2051
2351
|
return idx;
|
|
2052
2352
|
} catch (err) {
|
|
2053
|
-
|
|
2353
|
+
logger_default.warn(" [vector] Index build failed:", err instanceof Error ? err.message : err);
|
|
2054
2354
|
return null;
|
|
2055
2355
|
} finally {
|
|
2056
2356
|
vectorIndexBuilding = false;
|
|
@@ -2064,16 +2364,59 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2064
2364
|
res.json({ status: "ok", nodes: graph.size.nodes, edges: graph.size.edges });
|
|
2065
2365
|
});
|
|
2066
2366
|
app.get("/api/repos", (_req, res) => {
|
|
2067
|
-
|
|
2367
|
+
const registry = loadRegistry();
|
|
2368
|
+
if (registry.length === 0) {
|
|
2369
|
+
res.json([{ name: repoName, path: workspaceRoot ?? "", nodes: graph.size.nodes, edges: graph.size.edges, indexedAt: null }]);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
res.json(registry.map((r) => ({
|
|
2373
|
+
name: r.name,
|
|
2374
|
+
path: r.path,
|
|
2375
|
+
nodes: r.stats.nodes,
|
|
2376
|
+
edges: r.stats.edges,
|
|
2377
|
+
indexedAt: r.indexedAt,
|
|
2378
|
+
active: r.path === workspaceRoot
|
|
2379
|
+
})));
|
|
2068
2380
|
});
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
const
|
|
2381
|
+
async function loadRepoGraph(requestedRepo) {
|
|
2382
|
+
if (requestedRepo === repoName) return graph;
|
|
2383
|
+
const registry = loadRegistry();
|
|
2384
|
+
const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
|
|
2385
|
+
if (!entry) return null;
|
|
2386
|
+
const dbPath = path16.join(entry.path, ".code-intel", "graph.db");
|
|
2387
|
+
if (!fs14.existsSync(dbPath)) return null;
|
|
2388
|
+
const repoGraph = createKnowledgeGraph();
|
|
2389
|
+
const db = new DbManager(dbPath);
|
|
2390
|
+
try {
|
|
2391
|
+
await db.init();
|
|
2392
|
+
await loadGraphFromDB(repoGraph, db);
|
|
2393
|
+
db.close();
|
|
2394
|
+
return repoGraph;
|
|
2395
|
+
} catch {
|
|
2396
|
+
db.close();
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
app.get("/api/graph/:repo", async (req, res) => {
|
|
2401
|
+
const requestedRepo = decodeURIComponent(req.params.repo);
|
|
2402
|
+
const g = await loadRepoGraph(requestedRepo);
|
|
2403
|
+
if (!g) {
|
|
2404
|
+
res.status(404).json({ error: `Repo "${requestedRepo}" not found or not indexed. Run: code-intel analyze <path>` });
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
const nodes = [...g.allNodes()];
|
|
2408
|
+
const edges = [...g.allEdges()];
|
|
2072
2409
|
res.json({ nodes, edges });
|
|
2073
2410
|
});
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
const
|
|
2411
|
+
async function getGraphForRepo(requestedRepo) {
|
|
2412
|
+
if (!requestedRepo || requestedRepo === repoName) return graph;
|
|
2413
|
+
const g = await loadRepoGraph(requestedRepo);
|
|
2414
|
+
return g ?? graph;
|
|
2415
|
+
}
|
|
2416
|
+
app.post("/api/search", async (req, res) => {
|
|
2417
|
+
const { query, limit, repo } = req.body;
|
|
2418
|
+
const g = await getGraphForRepo(repo);
|
|
2419
|
+
const results = textSearch(g, query, limit ?? 20);
|
|
2077
2420
|
res.json({ results });
|
|
2078
2421
|
});
|
|
2079
2422
|
app.post("/api/vector-search", async (req, res) => {
|
|
@@ -2113,7 +2456,7 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2113
2456
|
app.post("/api/files/read", (req, res) => {
|
|
2114
2457
|
const { file_path } = req.body;
|
|
2115
2458
|
try {
|
|
2116
|
-
const content =
|
|
2459
|
+
const content = fs14.readFileSync(file_path, "utf-8");
|
|
2117
2460
|
res.json({ content });
|
|
2118
2461
|
} catch {
|
|
2119
2462
|
res.status(404).json({ error: "File not found" });
|
|
@@ -2192,55 +2535,57 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2192
2535
|
res.status(400).json({ error: "Invalid query" });
|
|
2193
2536
|
}
|
|
2194
2537
|
});
|
|
2195
|
-
app.get("/api/nodes/:id", (req, res) => {
|
|
2538
|
+
app.get("/api/nodes/:id", async (req, res) => {
|
|
2196
2539
|
const nodeId = decodeURIComponent(req.params.id);
|
|
2197
|
-
const
|
|
2540
|
+
const g = await getGraphForRepo(req.query.repo);
|
|
2541
|
+
const node = g.getNode(nodeId);
|
|
2198
2542
|
if (!node) {
|
|
2199
2543
|
res.status(404).json({ error: "Node not found" });
|
|
2200
2544
|
return;
|
|
2201
2545
|
}
|
|
2202
|
-
const incoming = [...
|
|
2203
|
-
const outgoing = [...
|
|
2546
|
+
const incoming = [...g.findEdgesTo(nodeId)];
|
|
2547
|
+
const outgoing = [...g.findEdgesFrom(nodeId)];
|
|
2204
2548
|
res.json({
|
|
2205
2549
|
node,
|
|
2206
2550
|
callers: incoming.filter((e) => e.kind === "calls").map((e) => ({
|
|
2207
2551
|
id: e.source,
|
|
2208
|
-
name:
|
|
2552
|
+
name: g.getNode(e.source)?.name,
|
|
2209
2553
|
weight: e.weight
|
|
2210
2554
|
})),
|
|
2211
2555
|
callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
2212
2556
|
id: e.target,
|
|
2213
|
-
name:
|
|
2557
|
+
name: g.getNode(e.target)?.name,
|
|
2214
2558
|
weight: e.weight
|
|
2215
2559
|
})),
|
|
2216
2560
|
imports: outgoing.filter((e) => e.kind === "imports").map((e) => ({
|
|
2217
2561
|
id: e.target,
|
|
2218
|
-
name:
|
|
2562
|
+
name: g.getNode(e.target)?.name
|
|
2219
2563
|
})),
|
|
2220
2564
|
importedBy: incoming.filter((e) => e.kind === "imports").map((e) => ({
|
|
2221
2565
|
id: e.source,
|
|
2222
|
-
name:
|
|
2566
|
+
name: g.getNode(e.source)?.name
|
|
2223
2567
|
})),
|
|
2224
2568
|
extends: outgoing.filter((e) => e.kind === "extends").map((e) => ({
|
|
2225
2569
|
id: e.target,
|
|
2226
|
-
name:
|
|
2570
|
+
name: g.getNode(e.target)?.name
|
|
2227
2571
|
})),
|
|
2228
2572
|
implementsEdges: outgoing.filter((e) => e.kind === "implements").map((e) => ({
|
|
2229
2573
|
id: e.target,
|
|
2230
|
-
name:
|
|
2574
|
+
name: g.getNode(e.target)?.name
|
|
2231
2575
|
})),
|
|
2232
2576
|
members: outgoing.filter((e) => e.kind === "has_member").map((e) => ({
|
|
2233
2577
|
id: e.target,
|
|
2234
|
-
name:
|
|
2235
|
-
kind:
|
|
2578
|
+
name: g.getNode(e.target)?.name,
|
|
2579
|
+
kind: g.getNode(e.target)?.kind
|
|
2236
2580
|
})),
|
|
2237
|
-
cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) =>
|
|
2581
|
+
cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => g.getNode(e.target)?.name)[0]
|
|
2238
2582
|
});
|
|
2239
2583
|
});
|
|
2240
|
-
app.post("/api/blast-radius", (req, res) => {
|
|
2241
|
-
const { target, direction = "both", max_hops = 5 } = req.body;
|
|
2584
|
+
app.post("/api/blast-radius", async (req, res) => {
|
|
2585
|
+
const { target, direction = "both", max_hops = 5, repo } = req.body;
|
|
2586
|
+
const g = await getGraphForRepo(repo);
|
|
2242
2587
|
let targetNode = null;
|
|
2243
|
-
for (const node of
|
|
2588
|
+
for (const node of g.allNodes()) {
|
|
2244
2589
|
if (node.name === target || node.id === target) {
|
|
2245
2590
|
targetNode = node;
|
|
2246
2591
|
break;
|
|
@@ -2257,22 +2602,16 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2257
2602
|
const { id, depth } = queue.shift();
|
|
2258
2603
|
if (visited.has(id) || depth > max_hops) continue;
|
|
2259
2604
|
visited.add(id);
|
|
2260
|
-
const node =
|
|
2261
|
-
if (node) {
|
|
2262
|
-
affected.set(id, { name: node.name, kind: node.kind, depth });
|
|
2263
|
-
}
|
|
2605
|
+
const node = g.getNode(id);
|
|
2606
|
+
if (node) affected.set(id, { name: node.name, kind: node.kind, depth });
|
|
2264
2607
|
if (direction === "callers" || direction === "both") {
|
|
2265
|
-
for (const edge of
|
|
2266
|
-
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
2267
|
-
queue.push({ id: edge.source, depth: depth + 1 });
|
|
2268
|
-
}
|
|
2608
|
+
for (const edge of g.findEdgesTo(id)) {
|
|
2609
|
+
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.source, depth: depth + 1 });
|
|
2269
2610
|
}
|
|
2270
2611
|
}
|
|
2271
2612
|
if (direction === "callees" || direction === "both") {
|
|
2272
|
-
for (const edge of
|
|
2273
|
-
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
2274
|
-
queue.push({ id: edge.target, depth: depth + 1 });
|
|
2275
|
-
}
|
|
2613
|
+
for (const edge of g.findEdgesFrom(id)) {
|
|
2614
|
+
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.target, depth: depth + 1 });
|
|
2276
2615
|
}
|
|
2277
2616
|
}
|
|
2278
2617
|
}
|
|
@@ -2282,28 +2621,20 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2282
2621
|
affected: [...affected.entries()].map(([id, info]) => ({ id, ...info })).filter((a) => a.depth > 0)
|
|
2283
2622
|
});
|
|
2284
2623
|
});
|
|
2285
|
-
app.get("/api/flows", (
|
|
2624
|
+
app.get("/api/flows", async (req, res) => {
|
|
2625
|
+
const g = await getGraphForRepo(req.query.repo);
|
|
2286
2626
|
const flows = [];
|
|
2287
|
-
for (const node of
|
|
2288
|
-
if (node.kind === "flow") {
|
|
2289
|
-
flows.push({
|
|
2290
|
-
id: node.id,
|
|
2291
|
-
name: node.name,
|
|
2292
|
-
steps: node.metadata?.steps
|
|
2293
|
-
});
|
|
2294
|
-
}
|
|
2627
|
+
for (const node of g.allNodes()) {
|
|
2628
|
+
if (node.kind === "flow") flows.push({ id: node.id, name: node.name, steps: node.metadata?.steps });
|
|
2295
2629
|
}
|
|
2296
2630
|
res.json({ flows });
|
|
2297
2631
|
});
|
|
2298
|
-
app.get("/api/clusters", (
|
|
2632
|
+
app.get("/api/clusters", async (req, res) => {
|
|
2633
|
+
const g = await getGraphForRepo(req.query.repo);
|
|
2299
2634
|
const clusters = [];
|
|
2300
|
-
for (const node of
|
|
2635
|
+
for (const node of g.allNodes()) {
|
|
2301
2636
|
if (node.kind === "cluster") {
|
|
2302
|
-
clusters.push({
|
|
2303
|
-
id: node.id,
|
|
2304
|
-
name: node.name,
|
|
2305
|
-
memberCount: node.metadata?.memberCount ?? 0
|
|
2306
|
-
});
|
|
2637
|
+
clusters.push({ id: node.id, name: node.name, memberCount: node.metadata?.memberCount ?? 0 });
|
|
2307
2638
|
}
|
|
2308
2639
|
}
|
|
2309
2640
|
res.json({ clusters });
|
|
@@ -2379,8 +2710,8 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2379
2710
|
for (const member of group.members) {
|
|
2380
2711
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
2381
2712
|
if (!regEntry) continue;
|
|
2382
|
-
const dbPath =
|
|
2383
|
-
if (!
|
|
2713
|
+
const dbPath = path16.join(regEntry.path, ".code-intel", "graph.db");
|
|
2714
|
+
if (!fs14.existsSync(dbPath)) continue;
|
|
2384
2715
|
const db = new DbManager(dbPath);
|
|
2385
2716
|
try {
|
|
2386
2717
|
await db.init();
|
|
@@ -2392,10 +2723,10 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2392
2723
|
}
|
|
2393
2724
|
res.json({ nodes: [...mergedGraph.allNodes()], edges: [...mergedGraph.allEdges()] });
|
|
2394
2725
|
});
|
|
2395
|
-
if (
|
|
2726
|
+
if (fs14.existsSync(WEB_DIST)) {
|
|
2396
2727
|
app.use(express.static(WEB_DIST));
|
|
2397
2728
|
app.get("/{*path}", (_req, res) => {
|
|
2398
|
-
res.sendFile(
|
|
2729
|
+
res.sendFile(path16.join(WEB_DIST, "index.html"));
|
|
2399
2730
|
});
|
|
2400
2731
|
}
|
|
2401
2732
|
return app;
|
|
@@ -2403,73 +2734,229 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
2403
2734
|
function startHttpServer(graph, repoName, port = 4747, workspaceRoot) {
|
|
2404
2735
|
const app = createApp(graph, repoName, workspaceRoot);
|
|
2405
2736
|
app.listen(port, () => {
|
|
2406
|
-
|
|
2407
|
-
|
|
2737
|
+
logger_default.info(`Code Intelligence server running at http://localhost:${port}`);
|
|
2738
|
+
logger_default.info(` Graph: ${graph.size.nodes} nodes, ${graph.size.edges} edges`);
|
|
2408
2739
|
});
|
|
2409
2740
|
}
|
|
2410
|
-
|
|
2741
|
+
init_repo_registry();
|
|
2742
|
+
init_metadata();
|
|
2743
|
+
init_group_registry();
|
|
2744
|
+
function createMcpServer(graph, repoName, workspaceRoot) {
|
|
2411
2745
|
const server = new Server(
|
|
2412
2746
|
{ name: "code-intel", version: "0.1.0" },
|
|
2413
2747
|
{ capabilities: { tools: {}, resources: {} } }
|
|
2414
2748
|
);
|
|
2415
2749
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2416
2750
|
tools: [
|
|
2751
|
+
// ── Core repo tools ──────────────────────────────────────────────────
|
|
2417
2752
|
{
|
|
2418
2753
|
name: "repos",
|
|
2419
|
-
description: "List indexed repositories",
|
|
2754
|
+
description: "List all indexed repositories with node and edge counts",
|
|
2420
2755
|
inputSchema: { type: "object", properties: {} }
|
|
2421
2756
|
},
|
|
2757
|
+
{
|
|
2758
|
+
name: "overview",
|
|
2759
|
+
description: "Repository summary: total nodes/edges and a full breakdown of node and edge counts by kind. Use this first to understand the shape of the codebase.",
|
|
2760
|
+
inputSchema: { type: "object", properties: {} }
|
|
2761
|
+
},
|
|
2762
|
+
// ── Search & inspect ─────────────────────────────────────────────────
|
|
2422
2763
|
{
|
|
2423
2764
|
name: "search",
|
|
2424
|
-
description: "
|
|
2765
|
+
description: "BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc.",
|
|
2425
2766
|
inputSchema: {
|
|
2426
2767
|
type: "object",
|
|
2427
2768
|
properties: {
|
|
2428
|
-
query: { type: "string", description: "Search query" },
|
|
2429
|
-
limit: { type: "number", description: "Max results (default 20)" }
|
|
2769
|
+
query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
|
|
2770
|
+
limit: { type: "number", description: "Max results to return (default: 20)" }
|
|
2430
2771
|
},
|
|
2431
2772
|
required: ["query"]
|
|
2432
2773
|
}
|
|
2433
2774
|
},
|
|
2434
2775
|
{
|
|
2435
2776
|
name: "inspect",
|
|
2436
|
-
description: "360\xB0 view of a symbol: definition, callers, callees, heritage,
|
|
2777
|
+
description: "360\xB0 view of a symbol: definition location, callers, callees, heritage (extends/implements), members, cluster, and source preview (first 500 chars)",
|
|
2437
2778
|
inputSchema: {
|
|
2438
2779
|
type: "object",
|
|
2439
2780
|
properties: {
|
|
2440
|
-
symbol_name: { type: "string", description: "
|
|
2781
|
+
symbol_name: { type: "string", description: "Exact symbol name to inspect" }
|
|
2441
2782
|
},
|
|
2442
2783
|
required: ["symbol_name"]
|
|
2443
2784
|
}
|
|
2444
2785
|
},
|
|
2445
2786
|
{
|
|
2446
2787
|
name: "blast_radius",
|
|
2447
|
-
description: "Impact analysis:
|
|
2788
|
+
description: "Impact analysis: traverse the call/import graph to find all symbols that depend on or are affected by a given symbol. Returns risk level (LOW / MEDIUM / HIGH).",
|
|
2448
2789
|
inputSchema: {
|
|
2449
2790
|
type: "object",
|
|
2450
2791
|
properties: {
|
|
2451
2792
|
target: { type: "string", description: "Target symbol name" },
|
|
2452
|
-
direction: {
|
|
2453
|
-
|
|
2793
|
+
direction: {
|
|
2794
|
+
type: "string",
|
|
2795
|
+
enum: ["callers", "callees", "both"],
|
|
2796
|
+
description: "Which direction to trace \u2014 callers (who depends on it), callees (what it depends on), or both (default: both)"
|
|
2797
|
+
},
|
|
2798
|
+
max_hops: { type: "number", description: "Maximum traversal depth (default: 5)" }
|
|
2454
2799
|
},
|
|
2455
2800
|
required: ["target"]
|
|
2456
2801
|
}
|
|
2457
2802
|
},
|
|
2803
|
+
{
|
|
2804
|
+
name: "file_symbols",
|
|
2805
|
+
description: "List all symbols defined in a specific file \u2014 useful to understand what a file exports or contains without reading raw source.",
|
|
2806
|
+
inputSchema: {
|
|
2807
|
+
type: "object",
|
|
2808
|
+
properties: {
|
|
2809
|
+
file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' }
|
|
2810
|
+
},
|
|
2811
|
+
required: ["file_path"]
|
|
2812
|
+
}
|
|
2813
|
+
},
|
|
2814
|
+
{
|
|
2815
|
+
name: "find_path",
|
|
2816
|
+
description: "Find the shortest call/import path between two symbols. Useful for tracing how one module reaches another.",
|
|
2817
|
+
inputSchema: {
|
|
2818
|
+
type: "object",
|
|
2819
|
+
properties: {
|
|
2820
|
+
from: { type: "string", description: "Source symbol name" },
|
|
2821
|
+
to: { type: "string", description: "Target symbol name" },
|
|
2822
|
+
max_hops: { type: "number", description: "Maximum path length to search (default: 8)" }
|
|
2823
|
+
},
|
|
2824
|
+
required: ["from", "to"]
|
|
2825
|
+
}
|
|
2826
|
+
},
|
|
2827
|
+
{
|
|
2828
|
+
name: "list_exports",
|
|
2829
|
+
description: "List all exported symbols in the repository. Helps AI understand the public API surface of the codebase.",
|
|
2830
|
+
inputSchema: {
|
|
2831
|
+
type: "object",
|
|
2832
|
+
properties: {
|
|
2833
|
+
kind: {
|
|
2834
|
+
type: "string",
|
|
2835
|
+
description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
|
|
2836
|
+
},
|
|
2837
|
+
limit: { type: "number", description: "Max results (default: 100)" }
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
},
|
|
2841
|
+
// ── Routes, clusters, flows ──────────────────────────────────────────
|
|
2458
2842
|
{
|
|
2459
2843
|
name: "routes",
|
|
2460
|
-
description: "List route handler mappings in the codebase",
|
|
2844
|
+
description: "List all HTTP route handler mappings detected in the codebase (kind=route or route/handler/controller files)",
|
|
2461
2845
|
inputSchema: { type: "object", properties: {} }
|
|
2462
2846
|
},
|
|
2847
|
+
{
|
|
2848
|
+
name: "clusters",
|
|
2849
|
+
description: "List detected code clusters (directory-based communities) with member counts and top 10 symbols each. Useful for understanding code organisation.",
|
|
2850
|
+
inputSchema: {
|
|
2851
|
+
type: "object",
|
|
2852
|
+
properties: {
|
|
2853
|
+
limit: { type: "number", description: "Max clusters to return (default: 50)" }
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
},
|
|
2857
|
+
{
|
|
2858
|
+
name: "flows",
|
|
2859
|
+
description: "List all detected execution flows \u2014 entry points traced through the call graph. Each flow has a name, entry point, and ordered steps.",
|
|
2860
|
+
inputSchema: {
|
|
2861
|
+
type: "object",
|
|
2862
|
+
properties: {
|
|
2863
|
+
limit: { type: "number", description: "Max flows to return (default: 50)" }
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
},
|
|
2867
|
+
// ── Git change impact ─────────────────────────────────────────────────
|
|
2868
|
+
{
|
|
2869
|
+
name: "detect_changes",
|
|
2870
|
+
description: "Git-diff impact analysis: detects which source files and line ranges changed (HEAD vs working tree or a custom diff), maps them to graph symbols, and computes the combined blast radius. Ideal for PR review or pre-commit analysis.",
|
|
2871
|
+
inputSchema: {
|
|
2872
|
+
type: "object",
|
|
2873
|
+
properties: {
|
|
2874
|
+
base_ref: {
|
|
2875
|
+
type: "string",
|
|
2876
|
+
description: 'Git ref to diff against (default: HEAD). Examples: "HEAD~1", "main", a commit SHA.'
|
|
2877
|
+
},
|
|
2878
|
+
diff_text: {
|
|
2879
|
+
type: "string",
|
|
2880
|
+
description: "Raw unified diff text. If provided, base_ref is ignored and this diff is parsed directly."
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
},
|
|
2885
|
+
// ── Raw query ─────────────────────────────────────────────────────────
|
|
2463
2886
|
{
|
|
2464
2887
|
name: "raw_query",
|
|
2465
|
-
description: "Execute a graph query (
|
|
2888
|
+
description: "Execute a simplified Cypher-like graph query. Supports: name='X' (exact name match) or :kind (list nodes of a kind, max 50)",
|
|
2466
2889
|
inputSchema: {
|
|
2467
2890
|
type: "object",
|
|
2468
2891
|
properties: {
|
|
2469
|
-
cypher: { type: "string", description: "Query string
|
|
2892
|
+
cypher: { type: "string", description: "Query string \u2014 e.g. name='runPipeline' or :function" }
|
|
2470
2893
|
},
|
|
2471
2894
|
required: ["cypher"]
|
|
2472
2895
|
}
|
|
2896
|
+
},
|
|
2897
|
+
// ── Group / multi-repo tools ──────────────────────────────────────────
|
|
2898
|
+
{
|
|
2899
|
+
name: "group_list",
|
|
2900
|
+
description: "List all configured repository groups, or show the full membership of one group. Repository groups track multiple repos as a logical system.",
|
|
2901
|
+
inputSchema: {
|
|
2902
|
+
type: "object",
|
|
2903
|
+
properties: {
|
|
2904
|
+
name: { type: "string", description: "Group name to inspect (optional \u2014 omit to list all groups)" }
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
name: "group_sync",
|
|
2910
|
+
description: "Extract cross-repo contracts (exports, routes, schemas, events) from every member repo in a group and detect provider\u2192consumer links via name matching and RRF scoring.",
|
|
2911
|
+
inputSchema: {
|
|
2912
|
+
type: "object",
|
|
2913
|
+
properties: {
|
|
2914
|
+
name: { type: "string", description: "Group name to sync" }
|
|
2915
|
+
},
|
|
2916
|
+
required: ["name"]
|
|
2917
|
+
}
|
|
2918
|
+
},
|
|
2919
|
+
{
|
|
2920
|
+
name: "group_contracts",
|
|
2921
|
+
description: "Inspect extracted contracts and confidence-ranked cross-repo links from the last group sync. Supports filtering by kind, repo, and minimum confidence.",
|
|
2922
|
+
inputSchema: {
|
|
2923
|
+
type: "object",
|
|
2924
|
+
properties: {
|
|
2925
|
+
name: { type: "string", description: "Group name" },
|
|
2926
|
+
kind: {
|
|
2927
|
+
type: "string",
|
|
2928
|
+
enum: ["export", "route", "schema", "event"],
|
|
2929
|
+
description: "Filter by contract kind (optional)"
|
|
2930
|
+
},
|
|
2931
|
+
repo: { type: "string", description: "Filter by registry name (optional)" },
|
|
2932
|
+
min_confidence: { type: "number", description: "Minimum link confidence 0\u20131 (default: 0)" }
|
|
2933
|
+
},
|
|
2934
|
+
required: ["name"]
|
|
2935
|
+
}
|
|
2936
|
+
},
|
|
2937
|
+
{
|
|
2938
|
+
name: "group_query",
|
|
2939
|
+
description: "BM25 search across all repos in a group, merged via Reciprocal Rank Fusion (RRF). Returns a unified ranked list plus per-repo breakdown.",
|
|
2940
|
+
inputSchema: {
|
|
2941
|
+
type: "object",
|
|
2942
|
+
properties: {
|
|
2943
|
+
name: { type: "string", description: "Group name" },
|
|
2944
|
+
query: { type: "string", description: "Search query" },
|
|
2945
|
+
limit: { type: "number", description: "Max results per repo (default: 10)" }
|
|
2946
|
+
},
|
|
2947
|
+
required: ["name", "query"]
|
|
2948
|
+
}
|
|
2949
|
+
},
|
|
2950
|
+
{
|
|
2951
|
+
name: "group_status",
|
|
2952
|
+
description: "Check index freshness and sync staleness for all repos in a group. Flags repos that have not been indexed or are stale (>24h).",
|
|
2953
|
+
inputSchema: {
|
|
2954
|
+
type: "object",
|
|
2955
|
+
properties: {
|
|
2956
|
+
name: { type: "string", description: "Group name" }
|
|
2957
|
+
},
|
|
2958
|
+
required: ["name"]
|
|
2959
|
+
}
|
|
2473
2960
|
}
|
|
2474
2961
|
]
|
|
2475
2962
|
}));
|
|
@@ -2477,43 +2964,100 @@ function createMcpServer(graph, repoName) {
|
|
|
2477
2964
|
const { name, arguments: args } = request.params;
|
|
2478
2965
|
const a = args ?? {};
|
|
2479
2966
|
switch (name) {
|
|
2967
|
+
// ── repos ──────────────────────────────────────────────────────────────
|
|
2480
2968
|
case "repos": {
|
|
2481
|
-
|
|
2969
|
+
const registry = loadRegistry();
|
|
2970
|
+
return {
|
|
2971
|
+
content: [{
|
|
2972
|
+
type: "text",
|
|
2973
|
+
text: JSON.stringify(
|
|
2974
|
+
registry.map((r) => ({ name: r.name, path: r.path, indexedAt: r.indexedAt, stats: r.stats })),
|
|
2975
|
+
null,
|
|
2976
|
+
2
|
|
2977
|
+
)
|
|
2978
|
+
}]
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
// ── overview ───────────────────────────────────────────────────────────
|
|
2982
|
+
case "overview": {
|
|
2983
|
+
const kindCounts = {};
|
|
2984
|
+
for (const node of graph.allNodes()) {
|
|
2985
|
+
kindCounts[node.kind] = (kindCounts[node.kind] ?? 0) + 1;
|
|
2986
|
+
}
|
|
2987
|
+
const edgeCounts = {};
|
|
2988
|
+
for (const edge of graph.allEdges()) {
|
|
2989
|
+
edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
|
|
2990
|
+
}
|
|
2991
|
+
return {
|
|
2992
|
+
content: [{
|
|
2993
|
+
type: "text",
|
|
2994
|
+
text: JSON.stringify({
|
|
2995
|
+
repo: repoName,
|
|
2996
|
+
stats: graph.size,
|
|
2997
|
+
nodeCounts: kindCounts,
|
|
2998
|
+
edgeCounts
|
|
2999
|
+
}, null, 2)
|
|
3000
|
+
}]
|
|
3001
|
+
};
|
|
2482
3002
|
}
|
|
3003
|
+
// ── search ─────────────────────────────────────────────────────────────
|
|
2483
3004
|
case "search": {
|
|
2484
3005
|
const query = a.query;
|
|
2485
3006
|
const limit = a.limit ?? 20;
|
|
2486
3007
|
const results = textSearch(graph, query, limit);
|
|
2487
3008
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
2488
3009
|
}
|
|
3010
|
+
// ── inspect ────────────────────────────────────────────────────────────
|
|
2489
3011
|
case "inspect": {
|
|
2490
3012
|
const symbolName = a.symbol_name;
|
|
2491
3013
|
const node = findNodeByName(graph, symbolName);
|
|
2492
|
-
if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found
|
|
3014
|
+
if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
|
|
2493
3015
|
const incoming = [...graph.findEdgesTo(node.id)];
|
|
2494
3016
|
const outgoing = [...graph.findEdgesFrom(node.id)];
|
|
2495
3017
|
return {
|
|
2496
3018
|
content: [{
|
|
2497
3019
|
type: "text",
|
|
2498
3020
|
text: JSON.stringify({
|
|
2499
|
-
node: {
|
|
2500
|
-
|
|
2501
|
-
|
|
3021
|
+
node: {
|
|
3022
|
+
id: node.id,
|
|
3023
|
+
kind: node.kind,
|
|
3024
|
+
name: node.name,
|
|
3025
|
+
filePath: node.filePath,
|
|
3026
|
+
startLine: node.startLine,
|
|
3027
|
+
endLine: node.endLine,
|
|
3028
|
+
exported: node.exported
|
|
3029
|
+
},
|
|
3030
|
+
callers: incoming.filter((e) => e.kind === "calls").map((e) => ({
|
|
3031
|
+
id: e.source,
|
|
3032
|
+
name: graph.getNode(e.source)?.name,
|
|
3033
|
+
file: graph.getNode(e.source)?.filePath
|
|
3034
|
+
})),
|
|
3035
|
+
callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
3036
|
+
id: e.target,
|
|
3037
|
+
name: graph.getNode(e.target)?.name,
|
|
3038
|
+
file: graph.getNode(e.target)?.filePath
|
|
3039
|
+
})),
|
|
3040
|
+
imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
|
|
3041
|
+
importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
|
|
2502
3042
|
extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
|
|
2503
3043
|
implements: outgoing.filter((e) => e.kind === "implements").map((e) => graph.getNode(e.target)?.name),
|
|
2504
|
-
members: outgoing.filter((e) => e.kind === "has_member").map((e) => ({
|
|
3044
|
+
members: outgoing.filter((e) => e.kind === "has_member").map((e) => ({
|
|
3045
|
+
name: graph.getNode(e.target)?.name,
|
|
3046
|
+
kind: graph.getNode(e.target)?.kind
|
|
3047
|
+
})),
|
|
2505
3048
|
cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0],
|
|
2506
3049
|
content: node.content?.slice(0, 500)
|
|
2507
3050
|
}, null, 2)
|
|
2508
3051
|
}]
|
|
2509
3052
|
};
|
|
2510
3053
|
}
|
|
3054
|
+
// ── blast_radius ───────────────────────────────────────────────────────
|
|
2511
3055
|
case "blast_radius": {
|
|
2512
3056
|
const target = a.target;
|
|
2513
3057
|
const direction = a.direction ?? "both";
|
|
2514
3058
|
const maxHops = a.max_hops ?? 5;
|
|
2515
3059
|
const node = findNodeByName(graph, target);
|
|
2516
|
-
if (!node) return { content: [{ type: "text", text: `Symbol "${target}" not found
|
|
3060
|
+
if (!node) return { content: [{ type: "text", text: `Symbol "${target}" not found.` }] };
|
|
2517
3061
|
const affected = /* @__PURE__ */ new Set();
|
|
2518
3062
|
const queue = [{ id: node.id, depth: 0 }];
|
|
2519
3063
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -2537,17 +3081,219 @@ function createMcpServer(graph, repoName) {
|
|
|
2537
3081
|
const n = graph.getNode(id);
|
|
2538
3082
|
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
2539
3083
|
});
|
|
2540
|
-
|
|
3084
|
+
const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
|
|
3085
|
+
return {
|
|
3086
|
+
content: [{
|
|
3087
|
+
type: "text",
|
|
3088
|
+
text: JSON.stringify({
|
|
3089
|
+
target: node.name,
|
|
3090
|
+
affectedCount: affected.size,
|
|
3091
|
+
riskLevel: risk,
|
|
3092
|
+
affected: affectedDetails
|
|
3093
|
+
}, null, 2)
|
|
3094
|
+
}]
|
|
3095
|
+
};
|
|
2541
3096
|
}
|
|
3097
|
+
// ── file_symbols ───────────────────────────────────────────────────────
|
|
3098
|
+
case "file_symbols": {
|
|
3099
|
+
const filePath = a.file_path;
|
|
3100
|
+
const matches = [];
|
|
3101
|
+
for (const node of graph.allNodes()) {
|
|
3102
|
+
if (node.filePath && node.filePath.includes(filePath)) {
|
|
3103
|
+
matches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
if (matches.length === 0) {
|
|
3107
|
+
return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
|
|
3108
|
+
}
|
|
3109
|
+
matches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
|
|
3110
|
+
return { content: [{ type: "text", text: JSON.stringify(matches, null, 2) }] };
|
|
3111
|
+
}
|
|
3112
|
+
// ── find_path ──────────────────────────────────────────────────────────
|
|
3113
|
+
case "find_path": {
|
|
3114
|
+
const fromName = a.from;
|
|
3115
|
+
const toName = a.to;
|
|
3116
|
+
const maxHops = a.max_hops ?? 8;
|
|
3117
|
+
const fromNode = findNodeByName(graph, fromName);
|
|
3118
|
+
const toNode = findNodeByName(graph, toName);
|
|
3119
|
+
if (!fromNode) return { content: [{ type: "text", text: `Source symbol "${fromName}" not found.` }] };
|
|
3120
|
+
if (!toNode) return { content: [{ type: "text", text: `Target symbol "${toName}" not found.` }] };
|
|
3121
|
+
const queue = [{ id: fromNode.id, path: [fromNode.id] }];
|
|
3122
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3123
|
+
let foundPath = null;
|
|
3124
|
+
while (queue.length > 0) {
|
|
3125
|
+
const { id, path: currentPath } = queue.shift();
|
|
3126
|
+
if (visited.has(id)) continue;
|
|
3127
|
+
visited.add(id);
|
|
3128
|
+
if (id === toNode.id) {
|
|
3129
|
+
foundPath = currentPath;
|
|
3130
|
+
break;
|
|
3131
|
+
}
|
|
3132
|
+
if (currentPath.length > maxHops) continue;
|
|
3133
|
+
for (const edge of graph.findEdgesFrom(id)) {
|
|
3134
|
+
if ((edge.kind === "calls" || edge.kind === "imports") && !visited.has(edge.target)) {
|
|
3135
|
+
queue.push({ id: edge.target, path: [...currentPath, edge.target] });
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (!foundPath) {
|
|
3140
|
+
return { content: [{ type: "text", text: `No path found from "${fromName}" to "${toName}" within ${maxHops} hops.` }] };
|
|
3141
|
+
}
|
|
3142
|
+
const pathDetails = foundPath.map((id) => {
|
|
3143
|
+
const n = graph.getNode(id);
|
|
3144
|
+
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
3145
|
+
});
|
|
3146
|
+
return {
|
|
3147
|
+
content: [{
|
|
3148
|
+
type: "text",
|
|
3149
|
+
text: JSON.stringify({ from: fromName, to: toName, hops: foundPath.length - 1, path: pathDetails }, null, 2)
|
|
3150
|
+
}]
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
// ── list_exports ───────────────────────────────────────────────────────
|
|
3154
|
+
case "list_exports": {
|
|
3155
|
+
const kindFilter = a.kind;
|
|
3156
|
+
const limit = a.limit ?? 100;
|
|
3157
|
+
const exports$1 = [];
|
|
3158
|
+
for (const node of graph.allNodes()) {
|
|
3159
|
+
if (!node.exported) continue;
|
|
3160
|
+
if (kindFilter && node.kind !== kindFilter) continue;
|
|
3161
|
+
exports$1.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
|
|
3162
|
+
if (exports$1.length >= limit) break;
|
|
3163
|
+
}
|
|
3164
|
+
return { content: [{ type: "text", text: JSON.stringify({ total: exports$1.length, exports: exports$1 }, null, 2) }] };
|
|
3165
|
+
}
|
|
3166
|
+
// ── routes ─────────────────────────────────────────────────────────────
|
|
2542
3167
|
case "routes": {
|
|
2543
3168
|
const routes = [];
|
|
2544
3169
|
for (const node of graph.allNodes()) {
|
|
2545
3170
|
if (node.kind === "route" || node.kind === "function" && /route|handler|controller/i.test(node.filePath)) {
|
|
2546
|
-
routes.push({ name: node.name, filePath: node.filePath });
|
|
3171
|
+
routes.push({ name: node.name, filePath: node.filePath, startLine: node.startLine });
|
|
2547
3172
|
}
|
|
2548
3173
|
}
|
|
2549
3174
|
return { content: [{ type: "text", text: JSON.stringify(routes, null, 2) }] };
|
|
2550
3175
|
}
|
|
3176
|
+
// ── clusters ───────────────────────────────────────────────────────────
|
|
3177
|
+
case "clusters": {
|
|
3178
|
+
const limit = a.limit ?? 50;
|
|
3179
|
+
const clusters = [];
|
|
3180
|
+
for (const node of graph.allNodes()) {
|
|
3181
|
+
if (node.kind === "cluster") {
|
|
3182
|
+
const members = [];
|
|
3183
|
+
for (const edge of graph.findEdgesTo(node.id)) {
|
|
3184
|
+
if (edge.kind === "belongs_to") {
|
|
3185
|
+
const member = graph.getNode(edge.source);
|
|
3186
|
+
if (member && member.kind !== "cluster") {
|
|
3187
|
+
members.push({ name: member.name, kind: member.kind });
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
clusters.push({
|
|
3192
|
+
id: node.id,
|
|
3193
|
+
name: node.name,
|
|
3194
|
+
memberCount: node.metadata?.memberCount ?? members.length,
|
|
3195
|
+
topSymbols: members.slice(0, 10)
|
|
3196
|
+
});
|
|
3197
|
+
if (clusters.length >= limit) break;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
return { content: [{ type: "text", text: JSON.stringify(clusters, null, 2) }] };
|
|
3201
|
+
}
|
|
3202
|
+
// ── flows ──────────────────────────────────────────────────────────────
|
|
3203
|
+
case "flows": {
|
|
3204
|
+
const limit = a.limit ?? 50;
|
|
3205
|
+
const flows = [];
|
|
3206
|
+
for (const node of graph.allNodes()) {
|
|
3207
|
+
if (node.kind === "flow") {
|
|
3208
|
+
const steps = node.metadata?.steps;
|
|
3209
|
+
flows.push({
|
|
3210
|
+
id: node.id,
|
|
3211
|
+
name: node.name,
|
|
3212
|
+
entryPoint: node.metadata?.entryPoint,
|
|
3213
|
+
steps: steps ?? [],
|
|
3214
|
+
stepCount: Array.isArray(steps) ? steps.length : 0
|
|
3215
|
+
});
|
|
3216
|
+
if (flows.length >= limit) break;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
return { content: [{ type: "text", text: JSON.stringify(flows, null, 2) }] };
|
|
3220
|
+
}
|
|
3221
|
+
// ── detect_changes ─────────────────────────────────────────────────────
|
|
3222
|
+
case "detect_changes": {
|
|
3223
|
+
const baseRef = a.base_ref ?? "HEAD";
|
|
3224
|
+
const diffTextInput = a.diff_text;
|
|
3225
|
+
let diffText;
|
|
3226
|
+
const repoRoot = workspaceRoot ?? process.cwd();
|
|
3227
|
+
if (diffTextInput) {
|
|
3228
|
+
diffText = diffTextInput;
|
|
3229
|
+
} else {
|
|
3230
|
+
try {
|
|
3231
|
+
diffText = execSync(`git diff ${baseRef}`, { cwd: repoRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
3232
|
+
if (!diffText.trim()) {
|
|
3233
|
+
diffText = execSync(`git diff HEAD`, { cwd: repoRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
3234
|
+
}
|
|
3235
|
+
} catch {
|
|
3236
|
+
return { content: [{ type: "text", text: `Could not run git diff in ${repoRoot}. Ensure the path is a Git repository.` }] };
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
if (!diffText.trim()) {
|
|
3240
|
+
return { content: [{ type: "text", text: "No changes detected in git diff." }] };
|
|
3241
|
+
}
|
|
3242
|
+
const changedFiles = parseDiff(diffText);
|
|
3243
|
+
const hitNodes = /* @__PURE__ */ new Set();
|
|
3244
|
+
for (const { filePath: changedFile, changedLines } of changedFiles) {
|
|
3245
|
+
for (const node of graph.allNodes()) {
|
|
3246
|
+
if (!node.filePath) continue;
|
|
3247
|
+
const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path16.sep, "");
|
|
3248
|
+
const normChanged = changedFile.replace(/^a\/|^b\//, "");
|
|
3249
|
+
if (!normNode.endsWith(normChanged) && !normChanged.endsWith(normNode)) continue;
|
|
3250
|
+
if (node.startLine !== void 0 && node.endLine !== void 0) {
|
|
3251
|
+
const overlaps = changedLines.some((l) => l >= node.startLine && l <= node.endLine);
|
|
3252
|
+
if (overlaps) hitNodes.add(node.id);
|
|
3253
|
+
} else if (node.startLine !== void 0) {
|
|
3254
|
+
const overlaps = changedLines.some((l) => Math.abs(l - node.startLine) <= 3);
|
|
3255
|
+
if (overlaps) hitNodes.add(node.id);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
const allAffected = /* @__PURE__ */ new Set();
|
|
3260
|
+
for (const startId of hitNodes) {
|
|
3261
|
+
const queue = [{ id: startId, depth: 0 }];
|
|
3262
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3263
|
+
while (queue.length > 0) {
|
|
3264
|
+
const { id, depth } = queue.shift();
|
|
3265
|
+
if (visited.has(id) || depth > 5) continue;
|
|
3266
|
+
visited.add(id);
|
|
3267
|
+
allAffected.add(id);
|
|
3268
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
3269
|
+
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.source, depth: depth + 1 });
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
const changedSymbols = [...hitNodes].map((id) => {
|
|
3274
|
+
const n = graph.getNode(id);
|
|
3275
|
+
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
3276
|
+
});
|
|
3277
|
+
const affectedSymbols = [...allAffected].filter((id) => !hitNodes.has(id)).map((id) => {
|
|
3278
|
+
const n = graph.getNode(id);
|
|
3279
|
+
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
3280
|
+
});
|
|
3281
|
+
const risk = allAffected.size > 10 ? "HIGH" : allAffected.size > 4 ? "MEDIUM" : "LOW";
|
|
3282
|
+
return {
|
|
3283
|
+
content: [{
|
|
3284
|
+
type: "text",
|
|
3285
|
+
text: JSON.stringify({
|
|
3286
|
+
baseRef,
|
|
3287
|
+
changedFiles: changedFiles.map((f) => f.filePath),
|
|
3288
|
+
directlyChangedSymbols: changedSymbols,
|
|
3289
|
+
transitivelyAffectedSymbols: affectedSymbols,
|
|
3290
|
+
totalAffected: allAffected.size,
|
|
3291
|
+
riskLevel: risk
|
|
3292
|
+
}, null, 2)
|
|
3293
|
+
}]
|
|
3294
|
+
};
|
|
3295
|
+
}
|
|
3296
|
+
// ── raw_query ──────────────────────────────────────────────────────────
|
|
2551
3297
|
case "raw_query": {
|
|
2552
3298
|
const q = a.cypher;
|
|
2553
3299
|
const nameMatch = q?.match(/name\s*=\s*['"]([^'"]+)['"]/i);
|
|
@@ -2567,7 +3313,123 @@ function createMcpServer(graph, repoName) {
|
|
|
2567
3313
|
}
|
|
2568
3314
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
2569
3315
|
}
|
|
2570
|
-
return { content: [{ type: "text", text: "Query not recognized" }] };
|
|
3316
|
+
return { content: [{ type: "text", text: "Query not recognized. Use name='X' or :kind syntax." }] };
|
|
3317
|
+
}
|
|
3318
|
+
// ── group_list ─────────────────────────────────────────────────────────
|
|
3319
|
+
case "group_list": {
|
|
3320
|
+
const groupName = a.name;
|
|
3321
|
+
if (groupName) {
|
|
3322
|
+
const group = loadGroup(groupName);
|
|
3323
|
+
if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
|
|
3324
|
+
return { content: [{ type: "text", text: JSON.stringify(group, null, 2) }] };
|
|
3325
|
+
}
|
|
3326
|
+
const groups = listGroups();
|
|
3327
|
+
return {
|
|
3328
|
+
content: [{
|
|
3329
|
+
type: "text",
|
|
3330
|
+
text: JSON.stringify(
|
|
3331
|
+
groups.map((g) => ({ name: g.name, createdAt: g.createdAt, lastSync: g.lastSync, memberCount: g.members.length, members: g.members })),
|
|
3332
|
+
null,
|
|
3333
|
+
2
|
|
3334
|
+
)
|
|
3335
|
+
}]
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
// ── group_sync ─────────────────────────────────────────────────────────
|
|
3339
|
+
case "group_sync": {
|
|
3340
|
+
const groupName = a.name;
|
|
3341
|
+
const group = loadGroup(groupName);
|
|
3342
|
+
if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
|
|
3343
|
+
if (group.members.length === 0) return { content: [{ type: "text", text: `Group "${groupName}" has no members.` }] };
|
|
3344
|
+
const result = await syncGroup(group);
|
|
3345
|
+
saveSyncResult(result);
|
|
3346
|
+
group.lastSync = result.syncedAt;
|
|
3347
|
+
saveGroup(group);
|
|
3348
|
+
return {
|
|
3349
|
+
content: [{
|
|
3350
|
+
type: "text",
|
|
3351
|
+
text: JSON.stringify({
|
|
3352
|
+
groupName: result.groupName,
|
|
3353
|
+
syncedAt: result.syncedAt,
|
|
3354
|
+
memberCount: result.memberCount,
|
|
3355
|
+
contractCount: result.contracts.length,
|
|
3356
|
+
linkCount: result.links.length,
|
|
3357
|
+
topLinks: result.links.slice(0, 20)
|
|
3358
|
+
}, null, 2)
|
|
3359
|
+
}]
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
// ── group_contracts ────────────────────────────────────────────────────
|
|
3363
|
+
case "group_contracts": {
|
|
3364
|
+
const groupName = a.name;
|
|
3365
|
+
const kindFilter = a.kind;
|
|
3366
|
+
const repoFilter = a.repo;
|
|
3367
|
+
const minConf = a.min_confidence ?? 0;
|
|
3368
|
+
const result = loadSyncResult(groupName);
|
|
3369
|
+
if (!result) return { content: [{ type: "text", text: `No sync data for group "${groupName}". Run group_sync first.` }] };
|
|
3370
|
+
let contracts = result.contracts;
|
|
3371
|
+
if (kindFilter) contracts = contracts.filter((c) => c.kind === kindFilter);
|
|
3372
|
+
if (repoFilter) contracts = contracts.filter((c) => c.repoName === repoFilter);
|
|
3373
|
+
let links = result.links.filter((l) => l.confidence >= minConf);
|
|
3374
|
+
if (repoFilter) links = links.filter((l) => l.providerRepo === repoFilter || l.consumerRepo === repoFilter);
|
|
3375
|
+
return {
|
|
3376
|
+
content: [{
|
|
3377
|
+
type: "text",
|
|
3378
|
+
text: JSON.stringify({ syncedAt: result.syncedAt, contracts, links }, null, 2)
|
|
3379
|
+
}]
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
// ── group_query ────────────────────────────────────────────────────────
|
|
3383
|
+
case "group_query": {
|
|
3384
|
+
const groupName = a.name;
|
|
3385
|
+
const query = a.query;
|
|
3386
|
+
const limit = a.limit ?? 10;
|
|
3387
|
+
const group = loadGroup(groupName);
|
|
3388
|
+
if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
|
|
3389
|
+
const { perRepo, merged } = await queryGroup(group, query, limit);
|
|
3390
|
+
return {
|
|
3391
|
+
content: [{
|
|
3392
|
+
type: "text",
|
|
3393
|
+
text: JSON.stringify({ query, merged, perRepo }, null, 2)
|
|
3394
|
+
}]
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
// ── group_status ───────────────────────────────────────────────────────
|
|
3398
|
+
case "group_status": {
|
|
3399
|
+
const groupName = a.name;
|
|
3400
|
+
const group = loadGroup(groupName);
|
|
3401
|
+
if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
|
|
3402
|
+
const registry = loadRegistry();
|
|
3403
|
+
const now = Date.now();
|
|
3404
|
+
const memberStatus = group.members.map((m) => {
|
|
3405
|
+
const regEntry = registry.find((r) => r.name === m.registryName);
|
|
3406
|
+
if (!regEntry) return { groupPath: m.groupPath, registryName: m.registryName, status: "NOT_IN_REGISTRY" };
|
|
3407
|
+
const meta = loadMetadata(regEntry.path);
|
|
3408
|
+
if (!meta) return { groupPath: m.groupPath, registryName: m.registryName, repoPath: regEntry.path, status: "NOT_INDEXED" };
|
|
3409
|
+
const ageMin = Math.round((now - new Date(meta.indexedAt).getTime()) / 6e4);
|
|
3410
|
+
const stale = ageMin > 1440;
|
|
3411
|
+
return {
|
|
3412
|
+
groupPath: m.groupPath,
|
|
3413
|
+
registryName: m.registryName,
|
|
3414
|
+
repoPath: regEntry.path,
|
|
3415
|
+
indexedAt: meta.indexedAt,
|
|
3416
|
+
ageMinutes: ageMin,
|
|
3417
|
+
status: stale ? "STALE" : "OK",
|
|
3418
|
+
stats: meta.stats
|
|
3419
|
+
};
|
|
3420
|
+
});
|
|
3421
|
+
const syncAge = group.lastSync ? Math.round((now - new Date(group.lastSync).getTime()) / 6e4) : null;
|
|
3422
|
+
return {
|
|
3423
|
+
content: [{
|
|
3424
|
+
type: "text",
|
|
3425
|
+
text: JSON.stringify({
|
|
3426
|
+
group: groupName,
|
|
3427
|
+
lastSync: group.lastSync ?? null,
|
|
3428
|
+
syncAgeMinutes: syncAge,
|
|
3429
|
+
members: memberStatus
|
|
3430
|
+
}, null, 2)
|
|
3431
|
+
}]
|
|
3432
|
+
};
|
|
2571
3433
|
}
|
|
2572
3434
|
default:
|
|
2573
3435
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
@@ -2607,8 +3469,8 @@ function createMcpServer(graph, repoName) {
|
|
|
2607
3469
|
});
|
|
2608
3470
|
return server;
|
|
2609
3471
|
}
|
|
2610
|
-
async function startMcpStdio(graph, repoName) {
|
|
2611
|
-
const server = createMcpServer(graph, repoName);
|
|
3472
|
+
async function startMcpStdio(graph, repoName, workspaceRoot) {
|
|
3473
|
+
const server = createMcpServer(graph, repoName, workspaceRoot);
|
|
2612
3474
|
const transport = new StdioServerTransport();
|
|
2613
3475
|
await server.connect(transport);
|
|
2614
3476
|
}
|
|
@@ -2618,24 +3480,54 @@ function findNodeByName(graph, name) {
|
|
|
2618
3480
|
}
|
|
2619
3481
|
return void 0;
|
|
2620
3482
|
}
|
|
3483
|
+
function parseDiff(diffText) {
|
|
3484
|
+
const result = [];
|
|
3485
|
+
let currentFile = null;
|
|
3486
|
+
let currentNewLine = 0;
|
|
3487
|
+
const changedLinesMap = /* @__PURE__ */ new Map();
|
|
3488
|
+
for (const raw of diffText.split("\n")) {
|
|
3489
|
+
const fileMatch = raw.match(/^\+\+\+ b\/(.+)/);
|
|
3490
|
+
if (fileMatch) {
|
|
3491
|
+
currentFile = fileMatch[1];
|
|
3492
|
+
if (!changedLinesMap.has(currentFile)) changedLinesMap.set(currentFile, []);
|
|
3493
|
+
continue;
|
|
3494
|
+
}
|
|
3495
|
+
const hunkMatch = raw.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
3496
|
+
if (hunkMatch) {
|
|
3497
|
+
currentNewLine = parseInt(hunkMatch[1], 10);
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
if (!currentFile) continue;
|
|
3501
|
+
if (raw.startsWith("+") && !raw.startsWith("+++")) {
|
|
3502
|
+
changedLinesMap.get(currentFile).push(currentNewLine);
|
|
3503
|
+
currentNewLine++;
|
|
3504
|
+
} else if (raw.startsWith("-") && !raw.startsWith("---")) ; else if (!raw.startsWith("\\")) {
|
|
3505
|
+
currentNewLine++;
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
for (const [filePath, changedLines] of changedLinesMap) {
|
|
3509
|
+
result.push({ filePath, changedLines });
|
|
3510
|
+
}
|
|
3511
|
+
return result;
|
|
3512
|
+
}
|
|
2621
3513
|
|
|
2622
3514
|
// src/cli/main.ts
|
|
2623
3515
|
init_metadata();
|
|
2624
3516
|
async function writeSkillFiles(graph, workspaceRoot, projectName) {
|
|
2625
|
-
const outputDir =
|
|
3517
|
+
const outputDir = path16.join(workspaceRoot, ".claude", "skills", "code-intel");
|
|
2626
3518
|
const areas = buildAreaMap(graph, workspaceRoot);
|
|
2627
3519
|
if (areas.length === 0) return { skills: [], outputDir };
|
|
2628
|
-
|
|
2629
|
-
|
|
3520
|
+
fs14.rmSync(outputDir, { recursive: true, force: true });
|
|
3521
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
2630
3522
|
const skills = [];
|
|
2631
3523
|
const usedNames = /* @__PURE__ */ new Set();
|
|
2632
3524
|
for (const area of areas) {
|
|
2633
3525
|
const kebab = uniqueKebab(area.label, usedNames);
|
|
2634
3526
|
usedNames.add(kebab);
|
|
2635
3527
|
const content = renderSkill(area, projectName, kebab);
|
|
2636
|
-
const dir =
|
|
2637
|
-
|
|
2638
|
-
|
|
3528
|
+
const dir = path16.join(outputDir, kebab);
|
|
3529
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
3530
|
+
fs14.writeFileSync(path16.join(dir, "SKILL.md"), content, "utf-8");
|
|
2639
3531
|
skills.push({ name: kebab, label: area.label, symbolCount: area.nodes.length, fileCount: area.files.size });
|
|
2640
3532
|
}
|
|
2641
3533
|
return { skills, outputDir };
|
|
@@ -2815,11 +3707,13 @@ var BLOCK_START = "<!-- code-intel:start -->";
|
|
|
2815
3707
|
var BLOCK_END = "<!-- code-intel:end -->";
|
|
2816
3708
|
function writeContextFiles(workspaceRoot, projectName, stats, skills) {
|
|
2817
3709
|
const block = buildBlock(projectName, stats, skills);
|
|
2818
|
-
upsertFile(
|
|
2819
|
-
upsertFile(
|
|
3710
|
+
upsertFile(path16.join(workspaceRoot, "AGENTS.md"), block, "AGENTS.md");
|
|
3711
|
+
upsertFile(path16.join(workspaceRoot, "CLAUDE.md"), block, "CLAUDE.md");
|
|
2820
3712
|
}
|
|
2821
3713
|
function buildBlock(projectName, stats, skills) {
|
|
2822
|
-
const skillRows = skills.map(
|
|
3714
|
+
const skillRows = skills.map(
|
|
3715
|
+
(s) => `| Work in \`${s.label}\` (${s.symbolCount} symbols) | \`.claude/skills/code-intel/${s.name}/SKILL.md\` |`
|
|
3716
|
+
).join("\n");
|
|
2823
3717
|
const skillTable = `| Task | Skill file |
|
|
2824
3718
|
|------|------------|
|
|
2825
3719
|
| Understand architecture / "How does X work?" | Load \`code-intel-exploring\` skill |
|
|
@@ -2829,16 +3723,19 @@ ${skillRows ? skillRows + "\n" : ""}`;
|
|
|
2829
3723
|
return `${BLOCK_START}
|
|
2830
3724
|
# Code Intelligence \u2014 ${projectName}
|
|
2831
3725
|
|
|
3726
|
+
> \u26A0 This section is auto-managed by \`code-intel analyze\`. Do **not** edit between the markers \u2014 your changes will be overwritten.
|
|
3727
|
+
> Add your own notes below the \`${BLOCK_END}\` marker.
|
|
3728
|
+
|
|
2832
3729
|
Indexed: **${stats.nodes.toLocaleString()} nodes** | **${stats.edges.toLocaleString()} edges** | **${stats.files} files** | analyzed in ${(stats.duration / 1e3).toFixed(1)}s
|
|
2833
3730
|
|
|
2834
|
-
>
|
|
3731
|
+
> Index stale? Re-run: \`code-intel analyze\`
|
|
2835
3732
|
|
|
2836
3733
|
## Always Do
|
|
2837
3734
|
|
|
2838
|
-
- **Before editing any symbol**, run \`code-intel impact <symbol>\`
|
|
3735
|
+
- **Before editing any symbol**, run \`code-intel impact <symbol>\` to review its blast radius.
|
|
2839
3736
|
- **Before committing**, verify scope with \`code-intel inspect <symbol>\`.
|
|
2840
3737
|
- Use \`code-intel search "<concept>"\` to find related symbols instead of grepping.
|
|
2841
|
-
- Warn the user if impact shows \u2265 5 direct callers (HIGH risk).
|
|
3738
|
+
- Warn the user if impact shows \u2265 5 direct callers (**HIGH risk**).
|
|
2842
3739
|
|
|
2843
3740
|
## Never Do
|
|
2844
3741
|
|
|
@@ -2862,29 +3759,50 @@ code-intel clean [path] # Remove index data
|
|
|
2862
3759
|
${skillTable}
|
|
2863
3760
|
${BLOCK_END}`;
|
|
2864
3761
|
}
|
|
2865
|
-
function upsertFile(filePath, block) {
|
|
2866
|
-
if (!
|
|
2867
|
-
|
|
3762
|
+
function upsertFile(filePath, block, fileName) {
|
|
3763
|
+
if (!fs14.existsSync(filePath)) {
|
|
3764
|
+
const newContent = [
|
|
3765
|
+
`# ${fileName}`,
|
|
3766
|
+
"",
|
|
3767
|
+
block,
|
|
3768
|
+
"",
|
|
3769
|
+
"---",
|
|
3770
|
+
"",
|
|
3771
|
+
"<!-- Add your own custom notes below this line. They will never be overwritten by code-intel. -->",
|
|
3772
|
+
""
|
|
3773
|
+
].join("\n");
|
|
3774
|
+
fs14.writeFileSync(filePath, newContent, "utf-8");
|
|
2868
3775
|
return;
|
|
2869
3776
|
}
|
|
2870
|
-
const existing =
|
|
3777
|
+
const existing = fs14.readFileSync(filePath, "utf-8");
|
|
2871
3778
|
const startIdx = findLineMarker(existing, BLOCK_START);
|
|
2872
3779
|
const endIdx = findLineMarker(existing, BLOCK_END, startIdx === -1 ? 0 : startIdx);
|
|
2873
3780
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
2874
3781
|
const before = existing.slice(0, startIdx);
|
|
2875
3782
|
const after = existing.slice(endIdx + BLOCK_END.length);
|
|
2876
|
-
|
|
3783
|
+
const updated = (before + block + after).trimEnd() + "\n";
|
|
3784
|
+
fs14.writeFileSync(filePath, updated, "utf-8");
|
|
2877
3785
|
return;
|
|
2878
3786
|
}
|
|
2879
|
-
|
|
3787
|
+
const appended = [
|
|
3788
|
+
existing.trimEnd(),
|
|
3789
|
+
"",
|
|
3790
|
+
"---",
|
|
3791
|
+
"",
|
|
3792
|
+
"<!-- The following section is auto-managed by code-intel. Do not edit between the markers. -->",
|
|
3793
|
+
"",
|
|
3794
|
+
block,
|
|
3795
|
+
""
|
|
3796
|
+
].join("\n");
|
|
3797
|
+
fs14.writeFileSync(filePath, appended, "utf-8");
|
|
2880
3798
|
}
|
|
2881
3799
|
function findLineMarker(content, marker, startFrom = 0) {
|
|
2882
3800
|
let idx = content.indexOf(marker, startFrom);
|
|
2883
3801
|
while (idx !== -1) {
|
|
2884
|
-
const
|
|
3802
|
+
const atLineStart = idx === 0 || content[idx - 1] === "\n";
|
|
2885
3803
|
const end = idx + marker.length;
|
|
2886
|
-
const
|
|
2887
|
-
if (
|
|
3804
|
+
const atLineEnd = end === content.length || content[end] === "\n" || content[end] === "\r";
|
|
3805
|
+
if (atLineStart && atLineEnd) return idx;
|
|
2888
3806
|
idx = content.indexOf(marker, idx + 1);
|
|
2889
3807
|
}
|
|
2890
3808
|
return -1;
|
|
@@ -2898,27 +3816,160 @@ var __filename$1 = fileURLToPath(import.meta.url);
|
|
|
2898
3816
|
var __dirname2 = dirname(__filename$1);
|
|
2899
3817
|
var _pkg = JSON.parse(readFileSync(join(__dirname2, "../../package.json"), "utf-8"));
|
|
2900
3818
|
var program = new Command();
|
|
2901
|
-
|
|
3819
|
+
var BANNER = `
|
|
3820
|
+
\u25C8 Code Intelligence Platform v${_pkg.version}
|
|
3821
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3822
|
+
Build a Knowledge Graph from source code and explore it via Web UI, HTTP API,
|
|
3823
|
+
CLI, and MCP server. Supports 14+ languages. Zero config.
|
|
3824
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3825
|
+
`;
|
|
3826
|
+
program.name("code-intel").description("Code Intelligence Platform \u2014 Static Analysis + Knowledge Graph").version(_pkg.version).addHelpText("beforeAll", BANNER).addHelpText("after", `
|
|
3827
|
+
\u250C\u2500 Quick Start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3828
|
+
\u2502 \u2502
|
|
3829
|
+
\u2502 code-intel setup Configure MCP for your editors \u2502
|
|
3830
|
+
\u2502 code-intel analyze Index current directory \u2502
|
|
3831
|
+
\u2502 code-intel serve Start web UI at http://localhost:4747 \u2502
|
|
3832
|
+
\u2502 code-intel search "query" Search the knowledge graph \u2502
|
|
3833
|
+
\u2502 code-intel inspect <symbol> Inspect a symbol's connections \u2502
|
|
3834
|
+
\u2502 code-intel impact <symbol> Show blast radius for a symbol \u2502
|
|
3835
|
+
\u2502 \u2502
|
|
3836
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3837
|
+
|
|
3838
|
+
\u250C\u2500 All Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3839
|
+
\u2502 \u2502
|
|
3840
|
+
\u2502 setup \u2502
|
|
3841
|
+
\u2502 code-intel setup Register the MCP server in your editor config (one-time) \u2502
|
|
3842
|
+
\u2502 \u2502
|
|
3843
|
+
\u2502 analyze \u2502
|
|
3844
|
+
\u2502 code-intel analyze [path] Parse source code and build the knowledge graph \u2502
|
|
3845
|
+
\u2502 code-intel analyze --force Discard the existing index and perform a full re-analysis \u2502
|
|
3846
|
+
\u2502 code-intel analyze --skills Emit per-cluster SKILL.md files under .claude/skills/code-intel/ \u2502
|
|
3847
|
+
\u2502 code-intel analyze --embeddings Build a vector index for semantic (natural-language) search \u2502
|
|
3848
|
+
\u2502 code-intel analyze --skip-embeddings Omit embedding generation for a significantly faster run \u2502
|
|
3849
|
+
\u2502 code-intel analyze --skip-agents-md Preserve any hand-edited content in AGENTS.md / CLAUDE.md \u2502
|
|
3850
|
+
\u2502 code-intel analyze --skip-git Allow analysis of directories that are not Git repositories \u2502
|
|
3851
|
+
\u2502 code-intel analyze --verbose Print every file skipped due to an unsupported parser \u2502
|
|
3852
|
+
\u2502 \u2502
|
|
3853
|
+
\u2502 server \u2502
|
|
3854
|
+
\u2502 code-intel mcp [path] Launch the MCP stdio server consumed by AI-enabled editors \u2502
|
|
3855
|
+
\u2502 code-intel serve [path] --port <n> Start the HTTP API and serve the interactive web UI (default :4747) \u2502
|
|
3856
|
+
\u2502 \u2502
|
|
3857
|
+
\u2502 registry \u2502
|
|
3858
|
+
\u2502 code-intel list Display all repositories that have been indexed \u2502
|
|
3859
|
+
\u2502 code-intel status [path] Report index freshness, symbol counts, and last-run duration \u2502
|
|
3860
|
+
\u2502 code-intel clean [path] Remove the .code-intel/ index for the specified repository \u2502
|
|
3861
|
+
\u2502 code-intel clean --all --force Permanently remove all indexed repositories (requires --force) \u2502
|
|
3862
|
+
\u2502 \u2502
|
|
3863
|
+
\u2502 exploration \u2502
|
|
3864
|
+
\u2502 code-intel search <query> Execute a BM25 keyword search across all indexed symbols \u2502
|
|
3865
|
+
\u2502 code-intel inspect <symbol> Show callers, callees, import edges, and source location \u2502
|
|
3866
|
+
\u2502 code-intel impact <symbol> Compute the transitive blast radius of a change to a symbol \u2502
|
|
3867
|
+
\u2502 \u2502
|
|
3868
|
+
\u2502 groups (multi-repo / monorepo service tracking) \u2502
|
|
3869
|
+
\u2502 code-intel group create <name> Create a named group to track multiple repositories together \u2502
|
|
3870
|
+
\u2502 code-intel group add <g> <path> <repo> Enroll an indexed repo in a group under the given hierarchy path \u2502
|
|
3871
|
+
\u2502 code-intel group remove <g> <path> Remove a repository from a group by its hierarchy path \u2502
|
|
3872
|
+
\u2502 code-intel group list [name] List all groups, or print the full membership of one group \u2502
|
|
3873
|
+
\u2502 code-intel group sync <name> Extract cross-repo contracts and resolve provider/consumer links \u2502
|
|
3874
|
+
\u2502 code-intel group contracts <name> Inspect extracted contracts and confidence-ranked cross-links \u2502
|
|
3875
|
+
\u2502 code-intel group query <name> <q> Run a merged RRF search across every repository in a group \u2502
|
|
3876
|
+
\u2502 code-intel group status <name> Audit index freshness and sync staleness for all group members \u2502
|
|
3877
|
+
\u2502 \u2502
|
|
3878
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3879
|
+
|
|
3880
|
+
Multi-language: TypeScript \xB7 JavaScript \xB7 Python \xB7 Java \xB7 Go \xB7 Rust \xB7 C/C++
|
|
3881
|
+
C# \xB7 PHP \xB7 Kotlin \xB7 Ruby \xB7 Swift \xB7 Dart
|
|
3882
|
+
|
|
3883
|
+
Docs: https://github.com/vohongtho/code-intel-platform
|
|
3884
|
+
`);
|
|
2902
3885
|
async function analyzeWorkspace(targetPath, options) {
|
|
2903
|
-
const workspaceRoot =
|
|
3886
|
+
const workspaceRoot = path16.resolve(targetPath);
|
|
2904
3887
|
if (!options?.silent) console.log(`Analyzing: ${workspaceRoot}`);
|
|
3888
|
+
logger_default.info(`analyze started: ${workspaceRoot}`);
|
|
3889
|
+
if (options?.force) {
|
|
3890
|
+
const dbPath = getDbPath(workspaceRoot);
|
|
3891
|
+
const { getVectorDbPath: getVectorDbPath2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
|
|
3892
|
+
const vdbPath = getVectorDbPath2(workspaceRoot);
|
|
3893
|
+
const wipeFiles = [
|
|
3894
|
+
dbPath,
|
|
3895
|
+
`${dbPath}-shm`,
|
|
3896
|
+
`${dbPath}-wal`,
|
|
3897
|
+
`${dbPath}.shm`,
|
|
3898
|
+
`${dbPath}.wal`,
|
|
3899
|
+
vdbPath,
|
|
3900
|
+
`${vdbPath}-shm`,
|
|
3901
|
+
`${vdbPath}-wal`,
|
|
3902
|
+
`${vdbPath}.shm`,
|
|
3903
|
+
`${vdbPath}.wal`
|
|
3904
|
+
];
|
|
3905
|
+
for (const f of wipeFiles) {
|
|
3906
|
+
try {
|
|
3907
|
+
if (fs14.existsSync(f)) fs14.unlinkSync(f);
|
|
3908
|
+
} catch {
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
2905
3912
|
if (!options?.skipGit) {
|
|
2906
|
-
const gitDir =
|
|
2907
|
-
if (!
|
|
2908
|
-
|
|
3913
|
+
const gitDir = path16.join(workspaceRoot, ".git");
|
|
3914
|
+
if (!fs14.existsSync(gitDir)) {
|
|
3915
|
+
logger_default.warn(`${workspaceRoot} is not a Git repository`);
|
|
2909
3916
|
}
|
|
2910
3917
|
}
|
|
2911
3918
|
const graph = createKnowledgeGraph();
|
|
3919
|
+
const BAR_WIDTH = 30;
|
|
3920
|
+
let currentPhase = "";
|
|
3921
|
+
function renderBar(phase, done, total) {
|
|
3922
|
+
const pct = total > 0 ? done / total : 1;
|
|
3923
|
+
const filled = Math.round(pct * BAR_WIDTH);
|
|
3924
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
|
|
3925
|
+
const pctStr = (pct * 100).toFixed(0).padStart(3);
|
|
3926
|
+
process.stdout.write(`\r [${phase.padEnd(9)}] ${bar} ${pctStr}% (${done}/${total})`);
|
|
3927
|
+
}
|
|
3928
|
+
function clearBar() {
|
|
3929
|
+
process.stdout.write("\r" + " ".repeat(70) + "\r");
|
|
3930
|
+
}
|
|
3931
|
+
const SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3932
|
+
let spinnerIdx = 0;
|
|
3933
|
+
let spinnerTimer = null;
|
|
3934
|
+
function startSpinner(label) {
|
|
3935
|
+
if (options?.silent) return;
|
|
3936
|
+
spinnerIdx = 0;
|
|
3937
|
+
spinnerTimer = setInterval(() => {
|
|
3938
|
+
process.stdout.write(`\r ${SPINNER_FRAMES[spinnerIdx % SPINNER_FRAMES.length]} ${label}\u2026`);
|
|
3939
|
+
spinnerIdx++;
|
|
3940
|
+
}, 80);
|
|
3941
|
+
}
|
|
3942
|
+
function stopSpinner() {
|
|
3943
|
+
if (spinnerTimer) {
|
|
3944
|
+
clearInterval(spinnerTimer);
|
|
3945
|
+
spinnerTimer = null;
|
|
3946
|
+
}
|
|
3947
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
3948
|
+
}
|
|
2912
3949
|
const context = {
|
|
2913
3950
|
workspaceRoot,
|
|
2914
3951
|
graph,
|
|
2915
3952
|
filePaths: [],
|
|
2916
3953
|
verbose: options?.verbose,
|
|
2917
|
-
onProgress: options?.silent ? void 0 : (phase, msg) =>
|
|
3954
|
+
onProgress: options?.silent ? void 0 : (phase, msg) => {
|
|
3955
|
+
if (!options?.silent) {
|
|
3956
|
+
if (currentPhase) clearBar();
|
|
3957
|
+
console.log(` [${phase}] ${msg}`);
|
|
3958
|
+
currentPhase = "";
|
|
3959
|
+
}
|
|
3960
|
+
},
|
|
3961
|
+
onPhaseProgress: options?.silent ? void 0 : (phase, done, total) => {
|
|
3962
|
+
currentPhase = phase;
|
|
3963
|
+
renderBar(phase, done, total);
|
|
3964
|
+
if (done >= total) {
|
|
3965
|
+
clearBar();
|
|
3966
|
+
currentPhase = "";
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
2918
3969
|
};
|
|
2919
3970
|
const phases = [scanPhase, structurePhase, parsePhase, resolvePhase, clusterPhase, flowPhase];
|
|
2920
3971
|
const result = await runPipeline(phases, context);
|
|
2921
|
-
const repoName =
|
|
3972
|
+
const repoName = path16.basename(workspaceRoot);
|
|
2922
3973
|
saveMetadata(workspaceRoot, {
|
|
2923
3974
|
indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2924
3975
|
stats: {
|
|
@@ -2938,45 +3989,71 @@ async function analyzeWorkspace(targetPath, options) {
|
|
|
2938
3989
|
files: context.filePaths.length
|
|
2939
3990
|
}
|
|
2940
3991
|
});
|
|
3992
|
+
startSpinner("Persisting graph to DB");
|
|
2941
3993
|
try {
|
|
2942
3994
|
const dbPath = getDbPath(workspaceRoot);
|
|
3995
|
+
const staleFiles = [
|
|
3996
|
+
dbPath,
|
|
3997
|
+
`${dbPath}-shm`,
|
|
3998
|
+
`${dbPath}-wal`,
|
|
3999
|
+
`${dbPath}.shm`,
|
|
4000
|
+
`${dbPath}.wal`
|
|
4001
|
+
];
|
|
4002
|
+
for (const f of staleFiles) {
|
|
4003
|
+
try {
|
|
4004
|
+
if (fs14.existsSync(f)) fs14.unlinkSync(f);
|
|
4005
|
+
} catch {
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
2943
4008
|
const db = new DbManager(dbPath);
|
|
2944
4009
|
await db.init();
|
|
2945
4010
|
const { nodeCount, edgeCount } = await loadGraphToDB(graph, db);
|
|
2946
4011
|
db.close();
|
|
4012
|
+
stopSpinner();
|
|
4013
|
+
logger_default.info(`DB persisted: ${nodeCount} nodes, ${edgeCount} edges`);
|
|
2947
4014
|
if (!options?.silent) {
|
|
2948
|
-
console.log(` DB: ${nodeCount} nodes, ${edgeCount} edges persisted`);
|
|
4015
|
+
console.log(` \u2713 DB: ${nodeCount} nodes, ${edgeCount} edges persisted`);
|
|
2949
4016
|
}
|
|
2950
4017
|
} catch (err) {
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
}
|
|
4018
|
+
stopSpinner();
|
|
4019
|
+
logger_default.warn(`DB persist failed: ${err instanceof Error ? err.message : err}`);
|
|
2954
4020
|
}
|
|
2955
4021
|
const doEmbeddings = options?.embeddings && !options?.skipEmbeddings;
|
|
2956
4022
|
if (doEmbeddings) {
|
|
2957
|
-
|
|
4023
|
+
startSpinner("Building vector embeddings");
|
|
2958
4024
|
try {
|
|
2959
4025
|
const { embedNodes: embedNodes2 } = await Promise.resolve().then(() => (init_embedder(), embedder_exports));
|
|
2960
4026
|
const { getVectorDbPath: getVectorDbPath2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
|
|
2961
4027
|
const { VectorIndex: VectorIndex2 } = await Promise.resolve().then(() => (init_vector_index(), vector_index_exports));
|
|
2962
4028
|
const vdbPath = getVectorDbPath2(workspaceRoot);
|
|
4029
|
+
const staleVdb = [vdbPath, `${vdbPath}-shm`, `${vdbPath}-wal`, `${vdbPath}.shm`, `${vdbPath}.wal`];
|
|
4030
|
+
for (const f of staleVdb) {
|
|
4031
|
+
try {
|
|
4032
|
+
if (fs14.existsSync(f)) fs14.unlinkSync(f);
|
|
4033
|
+
} catch {
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
2963
4036
|
const vdb = new DbManager(vdbPath);
|
|
2964
4037
|
await vdb.init();
|
|
2965
4038
|
const idx = new VectorIndex2(vdb);
|
|
2966
4039
|
await idx.init();
|
|
2967
4040
|
const nodes = await embedNodes2(graph, {
|
|
2968
4041
|
onProgress: (done, total) => {
|
|
2969
|
-
if (!options?.silent)
|
|
4042
|
+
if (!options?.silent) {
|
|
4043
|
+
stopSpinner();
|
|
4044
|
+
renderBar("vector", done, total);
|
|
4045
|
+
if (done >= total) clearBar();
|
|
4046
|
+
}
|
|
2970
4047
|
}
|
|
2971
4048
|
});
|
|
2972
|
-
|
|
4049
|
+
stopSpinner();
|
|
4050
|
+
logger_default.info(`Embeddings built: ${nodes.length} vectors`);
|
|
2973
4051
|
await idx.buildIndex(nodes);
|
|
2974
|
-
if (!options?.silent) console.log(` Embeddings: ${nodes.length} vectors built`);
|
|
4052
|
+
if (!options?.silent) console.log(` \u2713 Embeddings: ${nodes.length} vectors built`);
|
|
2975
4053
|
vdb.close();
|
|
2976
4054
|
} catch (err) {
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
}
|
|
4055
|
+
stopSpinner();
|
|
4056
|
+
logger_default.warn(`Embeddings failed: ${err instanceof Error ? err.message : err}`);
|
|
2980
4057
|
}
|
|
2981
4058
|
} else if (!options?.skipEmbeddings && !options?.silent) {
|
|
2982
4059
|
console.log(" Embeddings: skipped (use --embeddings to enable)");
|
|
@@ -2984,19 +4061,22 @@ async function analyzeWorkspace(targetPath, options) {
|
|
|
2984
4061
|
const doSkills = options?.skills !== false;
|
|
2985
4062
|
let skillSummaries = [];
|
|
2986
4063
|
if (doSkills) {
|
|
4064
|
+
startSpinner("Generating skill files");
|
|
2987
4065
|
try {
|
|
2988
4066
|
const { skills } = await writeSkillFiles(graph, workspaceRoot, repoName);
|
|
2989
4067
|
skillSummaries = skills;
|
|
4068
|
+
stopSpinner();
|
|
4069
|
+
logger_default.info(`Skills generated: ${skills.length}`);
|
|
2990
4070
|
if (!options?.silent && skills.length > 0) {
|
|
2991
|
-
console.log(` Skills: ${skills.length} generated \u2192 .claude/skills/code-intel/`);
|
|
4071
|
+
console.log(` \u2713 Skills: ${skills.length} generated \u2192 .claude/skills/code-intel/`);
|
|
2992
4072
|
}
|
|
2993
4073
|
} catch (err) {
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
}
|
|
4074
|
+
stopSpinner();
|
|
4075
|
+
logger_default.warn(`Skills generation failed: ${err instanceof Error ? err.message : err}`);
|
|
2997
4076
|
}
|
|
2998
4077
|
}
|
|
2999
4078
|
if (!options?.skipAgentsMd) {
|
|
4079
|
+
startSpinner("Writing context files");
|
|
3000
4080
|
try {
|
|
3001
4081
|
writeContextFiles(workspaceRoot, repoName, {
|
|
3002
4082
|
nodes: graph.size.nodes,
|
|
@@ -3004,29 +4084,37 @@ async function analyzeWorkspace(targetPath, options) {
|
|
|
3004
4084
|
files: context.filePaths.length,
|
|
3005
4085
|
duration: result.totalDuration
|
|
3006
4086
|
}, skillSummaries);
|
|
4087
|
+
stopSpinner();
|
|
4088
|
+
logger_default.info("Context files written: AGENTS.md + CLAUDE.md");
|
|
3007
4089
|
if (!options?.silent) {
|
|
3008
|
-
console.log(` Context: AGENTS.md + CLAUDE.md updated`);
|
|
4090
|
+
console.log(` \u2713 Context: AGENTS.md + CLAUDE.md updated`);
|
|
3009
4091
|
}
|
|
3010
4092
|
} catch (err) {
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
}
|
|
4093
|
+
stopSpinner();
|
|
4094
|
+
logger_default.warn(`Context file write failed: ${err instanceof Error ? err.message : err}`);
|
|
3014
4095
|
}
|
|
3015
4096
|
}
|
|
3016
4097
|
if (!options?.silent) {
|
|
4098
|
+
const dur = result.totalDuration;
|
|
4099
|
+
const durStr = dur >= 1e3 ? `${(dur / 1e3).toFixed(1)}s` : `${dur}ms`;
|
|
3017
4100
|
console.log(`
|
|
3018
|
-
Done in ${
|
|
3019
|
-
console.log(` Nodes: ${graph.size.nodes}`);
|
|
3020
|
-
console.log(` Edges: ${graph.size.edges}`);
|
|
3021
|
-
console.log(` Files: ${context.filePaths.length}`);
|
|
3022
|
-
console.log(` Success: ${result.success}`);
|
|
4101
|
+
\u2705 Done in ${durStr} \u2014 ${graph.size.nodes} nodes \xB7 ${graph.size.edges} edges \xB7 ${context.filePaths.length} files`);
|
|
3023
4102
|
}
|
|
4103
|
+
logger_default.info(`analyze complete: ${graph.size.nodes} nodes, ${graph.size.edges} edges, ${context.filePaths.length} files, ${result.totalDuration}ms`);
|
|
3024
4104
|
return { graph, result, repoName, workspaceRoot };
|
|
3025
4105
|
}
|
|
3026
|
-
program.command("setup").description("Configure MCP server for your editors (one-time setup)").
|
|
4106
|
+
program.command("setup").description("Configure MCP server for your editors (one-time setup)").addHelpText("after", `
|
|
4107
|
+
Configure the code-intel MCP server for Claude Desktop, VS Code, or any
|
|
4108
|
+
editor that supports the Model Context Protocol.
|
|
4109
|
+
|
|
4110
|
+
Auto-writes to ~/.config/claude/claude_desktop_config.json when available.
|
|
4111
|
+
|
|
4112
|
+
Examples:
|
|
4113
|
+
$ code-intel setup
|
|
4114
|
+
`).action(() => {
|
|
3027
4115
|
const configDir = process.env.HOME ? `${process.env.HOME}/.config/claude` : null;
|
|
3028
|
-
console.log("\n\
|
|
3029
|
-
console.log("Add the following to your editor MCP configuration:\n");
|
|
4116
|
+
console.log("\n \u25C8 Code Intelligence \u2014 MCP Setup\n");
|
|
4117
|
+
console.log(" Add the following to your editor MCP configuration:\n");
|
|
3030
4118
|
const mcpConfig = {
|
|
3031
4119
|
mcpServers: {
|
|
3032
4120
|
"code-intel": {
|
|
@@ -3035,14 +4123,14 @@ program.command("setup").description("Configure MCP server for your editors (one
|
|
|
3035
4123
|
}
|
|
3036
4124
|
}
|
|
3037
4125
|
};
|
|
3038
|
-
console.log("
|
|
3039
|
-
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
4126
|
+
console.log(" Claude Desktop / Claude Code (~/.config/claude/claude_desktop_config.json)");
|
|
4127
|
+
console.log(" " + JSON.stringify(mcpConfig, null, 2).split("\n").join("\n "));
|
|
3040
4128
|
if (configDir) {
|
|
3041
4129
|
const configFile = `${configDir}/claude_desktop_config.json`;
|
|
3042
4130
|
try {
|
|
3043
4131
|
let existing = {};
|
|
3044
|
-
if (
|
|
3045
|
-
existing = JSON.parse(
|
|
4132
|
+
if (fs14.existsSync(configFile)) {
|
|
4133
|
+
existing = JSON.parse(fs14.readFileSync(configFile, "utf-8"));
|
|
3046
4134
|
}
|
|
3047
4135
|
const merged = {
|
|
3048
4136
|
...existing,
|
|
@@ -3051,30 +4139,35 @@ program.command("setup").description("Configure MCP server for your editors (one
|
|
|
3051
4139
|
...mcpConfig.mcpServers
|
|
3052
4140
|
}
|
|
3053
4141
|
};
|
|
3054
|
-
|
|
3055
|
-
|
|
4142
|
+
fs14.mkdirSync(configDir, { recursive: true });
|
|
4143
|
+
fs14.writeFileSync(configFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
3056
4144
|
console.log(`
|
|
3057
|
-
\u2705
|
|
4145
|
+
\u2705 Written to ${configFile}`);
|
|
3058
4146
|
} catch (err) {
|
|
3059
|
-
|
|
3060
|
-
\u26A0
|
|
3061
|
-
console.log("Please add the config above manually.");
|
|
4147
|
+
logger_default.warn(`
|
|
4148
|
+
\u26A0 Could not auto-write config: ${err instanceof Error ? err.message : err}`);
|
|
4149
|
+
console.log(" Please add the config above manually.");
|
|
3062
4150
|
}
|
|
3063
4151
|
}
|
|
3064
|
-
console.log("\
|
|
3065
|
-
console.log("\
|
|
4152
|
+
console.log("\n VS Code / Cursor \u2014 add the same mcpServers block to settings.json or .vscode/mcp.json");
|
|
4153
|
+
console.log("\n Next: run `code-intel analyze` inside your project to build the knowledge graph.\n");
|
|
3066
4154
|
});
|
|
3067
|
-
program.command("analyze").description("Index a repository
|
|
3068
|
-
|
|
3069
|
-
code-intel
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
4155
|
+
program.command("analyze").description("Index a repository and build the knowledge graph").argument("[path]", "Path to the repository (default: current directory)", ".").option("--force", "Force full re-index, ignoring cached data").option("--skills", "Generate .claude/skills/ SKILL.md files from detected clusters").option("--embeddings", "Build vector embeddings for semantic search (slower, recommended)").option("--skip-embeddings", "Skip embedding generation (faster, text-search only)").option("--skip-agents-md", "Preserve any custom edits inside AGENTS.md / CLAUDE.md").option("--skip-git", "Allow indexing directories that are not Git repositories").option("--verbose", "Log every file skipped due to missing parser support").addHelpText("after", `
|
|
4156
|
+
Parses your source code with tree-sitter, builds a Knowledge Graph of
|
|
4157
|
+
symbols and their relationships, persists it to .code-intel/graph.db,
|
|
4158
|
+
and auto-generates AGENTS.md + CLAUDE.md context blocks.
|
|
4159
|
+
|
|
4160
|
+
Examples:
|
|
4161
|
+
$ code-intel analyze Index current directory
|
|
4162
|
+
$ code-intel analyze ./my-project Index a specific path
|
|
4163
|
+
$ code-intel analyze --force Force full re-index
|
|
4164
|
+
$ code-intel analyze --embeddings Enable semantic (vector) search
|
|
4165
|
+
$ code-intel analyze --skills Generate .claude/skills/ files
|
|
4166
|
+
$ code-intel analyze --skip-embeddings Skip vectors for a faster run
|
|
4167
|
+
$ code-intel analyze --skip-agents-md Preserve your custom AGENTS.md edits
|
|
4168
|
+
$ code-intel analyze --skip-git Index a non-Git folder
|
|
4169
|
+
$ code-intel analyze --verbose Show files skipped by the parser
|
|
4170
|
+
`).action(async (targetPath, opts) => {
|
|
3078
4171
|
await analyzeWorkspace(targetPath, {
|
|
3079
4172
|
force: opts.force,
|
|
3080
4173
|
skills: opts.skills,
|
|
@@ -3085,123 +4178,252 @@ Examples:
|
|
|
3085
4178
|
verbose: opts.verbose
|
|
3086
4179
|
});
|
|
3087
4180
|
});
|
|
3088
|
-
program.command("mcp").description("Start MCP server
|
|
3089
|
-
|
|
3090
|
-
|
|
4181
|
+
program.command("mcp").description("Start MCP server over stdio \u2014 exposes all tools to your AI editor").argument("[path]", "Path to analyze (default: current directory)", ".").addHelpText("after", `
|
|
4182
|
+
Starts the Model Context Protocol server over stdio transport.
|
|
4183
|
+
Your editor (Claude Desktop, VS Code, Cursor, etc.) connects to it
|
|
4184
|
+
and gains access to search, inspect, blast-radius, and flow tools.
|
|
4185
|
+
|
|
4186
|
+
Typically invoked automatically by your editor via the config from \`code-intel setup\`.
|
|
4187
|
+
|
|
4188
|
+
Examples:
|
|
4189
|
+
$ code-intel mcp
|
|
4190
|
+
$ code-intel mcp ./my-project
|
|
4191
|
+
`).action(async (targetPath) => {
|
|
4192
|
+
const workspaceRoot = path16.resolve(targetPath);
|
|
4193
|
+
const repoName = path16.basename(workspaceRoot);
|
|
4194
|
+
const dbPath = getDbPath(workspaceRoot);
|
|
4195
|
+
const existingIndex = fs14.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
|
|
4196
|
+
if (existingIndex) {
|
|
4197
|
+
const graph = createKnowledgeGraph();
|
|
4198
|
+
const db = new DbManager(dbPath);
|
|
4199
|
+
await db.init();
|
|
4200
|
+
await loadGraphFromDB(graph, db);
|
|
4201
|
+
db.close();
|
|
4202
|
+
await startMcpStdio(graph, repoName, workspaceRoot);
|
|
4203
|
+
} else {
|
|
4204
|
+
const { graph, repoName: name, workspaceRoot: root } = await analyzeWorkspace(targetPath, { silent: true });
|
|
4205
|
+
await startMcpStdio(graph, name, root);
|
|
4206
|
+
}
|
|
3091
4207
|
});
|
|
3092
|
-
program.command("serve").description("Start local HTTP server + web UI
|
|
3093
|
-
|
|
3094
|
-
|
|
4208
|
+
program.command("serve").description("Start the local HTTP server + web UI for graph exploration").argument("[path]", "Path to analyze (default: current directory)", ".").option("-p, --port <port>", "Port to listen on", "4747").option("--force", "Force re-analysis even if an index already exists").addHelpText("after", `
|
|
4209
|
+
If a .code-intel/graph.db index already exists for the path, the server
|
|
4210
|
+
loads the persisted graph directly and starts immediately \u2014 no re-analysis.
|
|
4211
|
+
Use --force to discard the existing index and re-analyze from scratch.
|
|
4212
|
+
|
|
4213
|
+
The web UI offers:
|
|
4214
|
+
\xB7 Force-directed Knowledge Graph with color-coded node types
|
|
4215
|
+
\xB7 BM25 text search + optional semantic (vector) search
|
|
4216
|
+
\xB7 Node detail panel: callers, callees, blast radius, source preview
|
|
4217
|
+
\xB7 AI Code Chat grounded on your codebase
|
|
4218
|
+
\xB7 Multi-repo group view (if groups are configured)
|
|
4219
|
+
|
|
4220
|
+
Examples:
|
|
4221
|
+
$ code-intel serve
|
|
4222
|
+
$ code-intel serve ./my-project
|
|
4223
|
+
$ code-intel serve --port 8080
|
|
4224
|
+
$ code-intel serve --force
|
|
4225
|
+
`).action(async (targetPath, options) => {
|
|
4226
|
+
const workspaceRoot = path16.resolve(targetPath);
|
|
4227
|
+
const repoName = path16.basename(workspaceRoot);
|
|
4228
|
+
const dbPath = getDbPath(workspaceRoot);
|
|
4229
|
+
const existingIndex = !options.force && fs14.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
|
|
4230
|
+
if (existingIndex) {
|
|
4231
|
+
console.log(`Loading index: ${workspaceRoot}`);
|
|
4232
|
+
const meta = loadMetadata(workspaceRoot);
|
|
4233
|
+
console.log(` \u25C8 ${meta.stats.nodes} nodes \xB7 ${meta.stats.edges} edges \xB7 ${meta.stats.files} files (indexed ${meta.indexedAt})`);
|
|
4234
|
+
const graph = createKnowledgeGraph();
|
|
4235
|
+
const db = new DbManager(dbPath);
|
|
4236
|
+
await db.init();
|
|
4237
|
+
await loadGraphFromDB(graph, db);
|
|
4238
|
+
db.close();
|
|
4239
|
+
startHttpServer(graph, repoName, parseInt(options.port, 10), workspaceRoot);
|
|
4240
|
+
} else {
|
|
4241
|
+
const { graph, workspaceRoot: root, repoName: name } = await analyzeWorkspace(targetPath, { force: options.force });
|
|
4242
|
+
startHttpServer(graph, name, parseInt(options.port, 10), root);
|
|
4243
|
+
}
|
|
3095
4244
|
});
|
|
3096
|
-
program.command("list").description("List all indexed repositories").
|
|
4245
|
+
program.command("list").description("List all indexed repositories in the registry").addHelpText("after", `
|
|
4246
|
+
Shows every repository that has been indexed with \`code-intel analyze\`.
|
|
4247
|
+
Useful for checking what is available before using \`code-intel group add\`.
|
|
4248
|
+
|
|
4249
|
+
Examples:
|
|
4250
|
+
$ code-intel list
|
|
4251
|
+
`).action(() => {
|
|
3097
4252
|
const repos = loadRegistry();
|
|
3098
4253
|
if (repos.length === 0) {
|
|
3099
|
-
console.log("No indexed repositories
|
|
4254
|
+
console.log("\n No indexed repositories found.");
|
|
4255
|
+
console.log(" Run `code-intel analyze <path>` to index a project.\n");
|
|
3100
4256
|
return;
|
|
3101
4257
|
}
|
|
3102
4258
|
console.log(`
|
|
3103
|
-
Indexed repositories (${repos.length}):
|
|
4259
|
+
Indexed repositories (${repos.length}):
|
|
3104
4260
|
`);
|
|
3105
4261
|
for (const r of repos) {
|
|
3106
|
-
console.log(` ${r.name
|
|
3107
|
-
console.log(`
|
|
3108
|
-
console.log(`
|
|
4262
|
+
console.log(` \u25C6 ${r.name}`);
|
|
4263
|
+
console.log(` Nodes: ${r.stats.nodes} \xB7 Edges: ${r.stats.edges} \xB7 Files: ${r.stats.files}`);
|
|
4264
|
+
console.log(` Path: ${r.path}`);
|
|
4265
|
+
console.log(` Indexed: ${r.indexedAt}
|
|
4266
|
+
`);
|
|
3109
4267
|
}
|
|
3110
4268
|
});
|
|
3111
|
-
program.command("status").description("Show index
|
|
3112
|
-
|
|
4269
|
+
program.command("status").description("Show index freshness and statistics for a repository").argument("[path]", "Path to check (default: current directory)", ".").addHelpText("after", `
|
|
4270
|
+
Reads the metadata from .code-intel/meta.json and reports when the index
|
|
4271
|
+
was last built and how many symbols were found.
|
|
4272
|
+
|
|
4273
|
+
Examples:
|
|
4274
|
+
$ code-intel status
|
|
4275
|
+
$ code-intel status ./my-project
|
|
4276
|
+
`).action((targetPath) => {
|
|
4277
|
+
const workspaceRoot = path16.resolve(targetPath);
|
|
3113
4278
|
const meta = loadMetadata(workspaceRoot);
|
|
3114
4279
|
if (!meta) {
|
|
3115
|
-
console.log(
|
|
4280
|
+
console.log(`
|
|
4281
|
+
\u2717 ${workspaceRoot} is not indexed.`);
|
|
4282
|
+
console.log(" Run `code-intel analyze` to build the index.\n");
|
|
3116
4283
|
return;
|
|
3117
4284
|
}
|
|
3118
4285
|
console.log(`
|
|
3119
|
-
Index status
|
|
3120
|
-
|
|
3121
|
-
console.log(`
|
|
3122
|
-
console.log(`
|
|
3123
|
-
console.log(`
|
|
3124
|
-
console.log(`
|
|
4286
|
+
\u25C8 Index status \u2014 ${workspaceRoot}
|
|
4287
|
+
`);
|
|
4288
|
+
console.log(` Indexed at : ${meta.indexedAt}`);
|
|
4289
|
+
console.log(` Nodes : ${meta.stats.nodes}`);
|
|
4290
|
+
console.log(` Edges : ${meta.stats.edges}`);
|
|
4291
|
+
console.log(` Files : ${meta.stats.files}`);
|
|
4292
|
+
console.log(` Duration : ${meta.stats.duration}ms
|
|
4293
|
+
`);
|
|
3125
4294
|
});
|
|
3126
|
-
program.command("clean").description("
|
|
4295
|
+
program.command("clean").description("Remove the knowledge graph index for a repository").argument("[path]", "Path to clean (default: current directory)", ".").option("--all", "Remove indexes for ALL indexed repositories").option("--force", "Required with --all to confirm the destructive operation").addHelpText("after", `
|
|
4296
|
+
Deletes the .code-intel/ directory and removes the entry from the registry.
|
|
4297
|
+
|
|
4298
|
+
\u26A0 --all --force is irreversible \u2014 it deletes every indexed repo's data.
|
|
4299
|
+
|
|
4300
|
+
Examples:
|
|
4301
|
+
$ code-intel clean Remove index for current directory
|
|
4302
|
+
$ code-intel clean ./my-project Remove index for a specific path
|
|
4303
|
+
$ code-intel clean --all --force Remove ALL indexes (requires --force)
|
|
4304
|
+
`).action((targetPath, opts) => {
|
|
3127
4305
|
if (opts.all) {
|
|
3128
4306
|
if (!opts.force) {
|
|
3129
|
-
console.error("
|
|
4307
|
+
console.error("\n \u2717 --all requires --force to confirm the destructive operation.");
|
|
4308
|
+
console.error(" Run: code-intel clean --all --force\n");
|
|
3130
4309
|
process.exit(1);
|
|
3131
4310
|
}
|
|
3132
4311
|
const repos = loadRegistry();
|
|
3133
4312
|
if (repos.length === 0) {
|
|
3134
|
-
console.log("No indexed repositories to clean
|
|
4313
|
+
console.log("\n No indexed repositories to clean.\n");
|
|
3135
4314
|
return;
|
|
3136
4315
|
}
|
|
3137
4316
|
for (const r of repos) {
|
|
3138
|
-
const codeIntelDir2 =
|
|
3139
|
-
if (
|
|
3140
|
-
|
|
3141
|
-
console.log(` Removed ${codeIntelDir2}`);
|
|
4317
|
+
const codeIntelDir2 = path16.join(r.path, ".code-intel");
|
|
4318
|
+
if (fs14.existsSync(codeIntelDir2)) {
|
|
4319
|
+
fs14.rmSync(codeIntelDir2, { recursive: true, force: true });
|
|
4320
|
+
console.log(` \u2713 Removed ${codeIntelDir2}`);
|
|
3142
4321
|
}
|
|
3143
4322
|
removeRepo(r.path);
|
|
3144
4323
|
}
|
|
3145
4324
|
console.log(`
|
|
3146
|
-
Cleaned ${repos.length} repositor${repos.length === 1 ? "y" : "ies"}
|
|
4325
|
+
Cleaned ${repos.length} repositor${repos.length === 1 ? "y" : "ies"}.
|
|
4326
|
+
`);
|
|
3147
4327
|
return;
|
|
3148
4328
|
}
|
|
3149
|
-
const workspaceRoot =
|
|
3150
|
-
const codeIntelDir =
|
|
3151
|
-
if (
|
|
3152
|
-
|
|
3153
|
-
console.log(`
|
|
4329
|
+
const workspaceRoot = path16.resolve(targetPath);
|
|
4330
|
+
const codeIntelDir = path16.join(workspaceRoot, ".code-intel");
|
|
4331
|
+
if (fs14.existsSync(codeIntelDir)) {
|
|
4332
|
+
fs14.rmSync(codeIntelDir, { recursive: true, force: true });
|
|
4333
|
+
console.log(`
|
|
4334
|
+
\u2713 Removed ${codeIntelDir}`);
|
|
3154
4335
|
}
|
|
3155
4336
|
removeRepo(workspaceRoot);
|
|
3156
|
-
console.log("Index cleaned
|
|
4337
|
+
console.log(" Index cleaned.\n");
|
|
3157
4338
|
});
|
|
3158
|
-
program.command("search").description("Search the knowledge graph").argument("<query>", "Search query").option("-l, --limit <
|
|
4339
|
+
program.command("search").description("Search the knowledge graph for symbols matching a query").argument("<query>", "Search query (name, kind, or partial match)").option("-l, --limit <n>", "Maximum number of results", "20").option("-p, --path <path>", "Path to the repository (default: current directory)", ".").addHelpText("after", `
|
|
4340
|
+
Runs BM25 text search across all indexed symbols \u2014 functions, classes,
|
|
4341
|
+
files, routes, interfaces, and more.
|
|
4342
|
+
|
|
4343
|
+
Examples:
|
|
4344
|
+
$ code-intel search "handleRequest"
|
|
4345
|
+
$ code-intel search "auth" --limit 10
|
|
4346
|
+
$ code-intel search "UserService" --path ./backend
|
|
4347
|
+
`).action(async (query, options) => {
|
|
3159
4348
|
const { graph } = await analyzeWorkspace(options.path, { silent: true });
|
|
3160
4349
|
const results = textSearch(graph, query, parseInt(options.limit, 10));
|
|
3161
4350
|
if (results.length === 0) {
|
|
3162
|
-
console.log(
|
|
4351
|
+
console.log(`
|
|
4352
|
+
No results found for "${query}".
|
|
4353
|
+
`);
|
|
3163
4354
|
return;
|
|
3164
4355
|
}
|
|
3165
|
-
console.log(`
|
|
4356
|
+
console.log(`
|
|
4357
|
+
${results.length} result(s) for "${query}":
|
|
3166
4358
|
`);
|
|
3167
4359
|
for (const r of results) {
|
|
3168
|
-
console.log(` ${r.kind.padEnd(
|
|
4360
|
+
console.log(` ${r.kind.padEnd(14)} ${r.name.padEnd(32)} ${r.filePath}`);
|
|
3169
4361
|
}
|
|
4362
|
+
console.log("");
|
|
3170
4363
|
});
|
|
3171
|
-
program.command("inspect").description("Inspect a symbol
|
|
4364
|
+
program.command("inspect").description("Inspect a symbol \u2014 show callers, callees, file location, and export status").argument("<symbol>", "Exact symbol name to inspect").option("-p, --path <path>", "Path to the repository (default: current directory)", ".").addHelpText("after", `
|
|
4365
|
+
Finds the symbol in the knowledge graph and prints its full connection
|
|
4366
|
+
profile: where it lives, who calls it, and what it calls.
|
|
4367
|
+
|
|
4368
|
+
Use this before renaming a symbol to understand its blast radius.
|
|
4369
|
+
|
|
4370
|
+
Examples:
|
|
4371
|
+
$ code-intel inspect runPipeline
|
|
4372
|
+
$ code-intel inspect ApiClient --path ./frontend
|
|
4373
|
+
`).action(async (symbol, options) => {
|
|
3172
4374
|
const { graph } = await analyzeWorkspace(options.path, { silent: true });
|
|
3173
4375
|
let found = false;
|
|
3174
4376
|
for (const node of graph.allNodes()) {
|
|
3175
4377
|
if (node.name === symbol) {
|
|
3176
4378
|
found = true;
|
|
3177
4379
|
console.log(`
|
|
3178
|
-
${node.kind}: ${node.name}`);
|
|
3179
|
-
console.log(`
|
|
3180
|
-
console.log(`
|
|
4380
|
+
\u25C6 ${node.kind}: ${node.name}`);
|
|
4381
|
+
console.log(` File : ${node.filePath}:${node.startLine ?? "?"}`);
|
|
4382
|
+
console.log(` Exported : ${node.exported ?? "unknown"}`);
|
|
3181
4383
|
const incoming = [...graph.findEdgesTo(node.id)];
|
|
3182
4384
|
const outgoing = [...graph.findEdgesFrom(node.id)];
|
|
3183
4385
|
const callers = incoming.filter((e) => e.kind === "calls");
|
|
3184
4386
|
const callees = outgoing.filter((e) => e.kind === "calls");
|
|
3185
4387
|
if (callers.length > 0) {
|
|
3186
|
-
console.log(`
|
|
4388
|
+
console.log(`
|
|
4389
|
+
Callers (${callers.length}):`);
|
|
3187
4390
|
for (const c of callers.slice(0, 10)) {
|
|
3188
4391
|
const n = graph.getNode(c.source);
|
|
3189
|
-
console.log(`
|
|
4392
|
+
console.log(` \u2190 ${n?.name ?? c.source} (${n?.filePath})`);
|
|
3190
4393
|
}
|
|
4394
|
+
if (callers.length > 10) console.log(` \u2026 and ${callers.length - 10} more`);
|
|
3191
4395
|
}
|
|
3192
4396
|
if (callees.length > 0) {
|
|
3193
|
-
console.log(`
|
|
4397
|
+
console.log(`
|
|
4398
|
+
Callees (${callees.length}):`);
|
|
3194
4399
|
for (const c of callees.slice(0, 10)) {
|
|
3195
4400
|
const n = graph.getNode(c.target);
|
|
3196
|
-
console.log(`
|
|
4401
|
+
console.log(` \u2192 ${n?.name ?? c.target} (${n?.filePath})`);
|
|
3197
4402
|
}
|
|
4403
|
+
if (callees.length > 10) console.log(` \u2026 and ${callees.length - 10} more`);
|
|
3198
4404
|
}
|
|
4405
|
+
console.log("");
|
|
3199
4406
|
break;
|
|
3200
4407
|
}
|
|
3201
4408
|
}
|
|
3202
|
-
if (!found)
|
|
4409
|
+
if (!found) {
|
|
4410
|
+
console.log(`
|
|
4411
|
+
Symbol "${symbol}" not found.`);
|
|
4412
|
+
console.log(` Try: code-intel search "${symbol}"
|
|
4413
|
+
`);
|
|
4414
|
+
}
|
|
3203
4415
|
});
|
|
3204
|
-
program.command("impact").description("Show blast radius
|
|
4416
|
+
program.command("impact").description("Show the blast radius \u2014 all symbols that break if this one changes").argument("<symbol>", "Symbol name to analyse").option("-p, --path <path>", "Path to the repository (default: current directory)", ".").option("-d, --depth <n>", "Maximum traversal depth (hops)", "5").addHelpText("after", `
|
|
4417
|
+
Traverses the call graph upward from the target symbol, collecting every
|
|
4418
|
+
symbol that transitively depends on it via calls or imports.
|
|
4419
|
+
|
|
4420
|
+
\u26A0 If impact shows \u2265 5 direct callers, treat the change as HIGH risk.
|
|
4421
|
+
|
|
4422
|
+
Examples:
|
|
4423
|
+
$ code-intel impact runPipeline
|
|
4424
|
+
$ code-intel impact ApiClient --depth 3
|
|
4425
|
+
$ code-intel impact UserService --path ./backend
|
|
4426
|
+
`).action(async (symbol, options) => {
|
|
3205
4427
|
const { graph } = await analyzeWorkspace(options.path, { silent: true });
|
|
3206
4428
|
const maxHops = parseInt(options.depth, 10);
|
|
3207
4429
|
let targetNode = null;
|
|
@@ -3212,7 +4434,10 @@ program.command("impact").description("Show blast radius for a symbol").argument
|
|
|
3212
4434
|
}
|
|
3213
4435
|
}
|
|
3214
4436
|
if (!targetNode) {
|
|
3215
|
-
console.log(`
|
|
4437
|
+
console.log(`
|
|
4438
|
+
Symbol "${symbol}" not found.`);
|
|
4439
|
+
console.log(` Try: code-intel search "${symbol}"
|
|
4440
|
+
`);
|
|
3216
4441
|
return;
|
|
3217
4442
|
}
|
|
3218
4443
|
const affected = /* @__PURE__ */ new Set();
|
|
@@ -3229,123 +4454,213 @@ program.command("impact").description("Show blast radius for a symbol").argument
|
|
|
3229
4454
|
}
|
|
3230
4455
|
}
|
|
3231
4456
|
}
|
|
4457
|
+
const risk = affected.size > 10 ? "\u26A0 HIGH" : affected.size > 5 ? "\u26A1 MEDIUM" : "\u2713 LOW";
|
|
3232
4458
|
console.log(`
|
|
3233
|
-
Blast radius for "${symbol}"
|
|
4459
|
+
\u25C8 Blast radius for "${symbol}"
|
|
4460
|
+
`);
|
|
4461
|
+
console.log(` Affected symbols : ${affected.size}`);
|
|
4462
|
+
console.log(` Risk level : ${risk}
|
|
3234
4463
|
`);
|
|
3235
4464
|
for (const id of affected) {
|
|
3236
4465
|
const n = graph.getNode(id);
|
|
3237
|
-
if (n) console.log(` ${n.kind.padEnd(
|
|
4466
|
+
if (n) console.log(` ${n.kind.padEnd(14)} ${n.name.padEnd(32)} ${n.filePath}`);
|
|
3238
4467
|
}
|
|
4468
|
+
console.log("");
|
|
3239
4469
|
});
|
|
3240
|
-
var groupCmd = program.command("group").description("Manage repository groups
|
|
3241
|
-
|
|
4470
|
+
var groupCmd = program.command("group").description("Manage repository groups for multi-repo / monorepo service tracking").addHelpText("after", `
|
|
4471
|
+
Repository groups let you track contracts (exports, routes, schemas, events)
|
|
4472
|
+
across multiple indexed repos and detect cross-repo dependencies automatically.
|
|
4473
|
+
|
|
4474
|
+
Subcommands:
|
|
4475
|
+
create <name> Create a new group
|
|
4476
|
+
add <group> <groupPath> <registry> Add a repo to the group
|
|
4477
|
+
remove <group> <groupPath> Remove a repo from the group
|
|
4478
|
+
list [name] List all groups or inspect one
|
|
4479
|
+
sync <name> Extract contracts + detect cross-links
|
|
4480
|
+
contracts <name> View extracted contracts and links
|
|
4481
|
+
query <name> <q> Search across all repos in the group
|
|
4482
|
+
status <name> Check index freshness of group members
|
|
4483
|
+
|
|
4484
|
+
Examples:
|
|
4485
|
+
$ code-intel group create my-platform
|
|
4486
|
+
$ code-intel group add my-platform services/auth auth-service
|
|
4487
|
+
$ code-intel group sync my-platform
|
|
4488
|
+
$ code-intel group contracts my-platform --kind route
|
|
4489
|
+
`);
|
|
4490
|
+
groupCmd.command("create <name>").description("Create a new repository group").addHelpText("after", `
|
|
4491
|
+
Examples:
|
|
4492
|
+
$ code-intel group create my-platform
|
|
4493
|
+
$ code-intel group create hr-services
|
|
4494
|
+
`).action((name) => {
|
|
3242
4495
|
if (groupExists(name)) {
|
|
3243
|
-
console.error(`
|
|
4496
|
+
console.error(`
|
|
4497
|
+
\u2717 Group "${name}" already exists.
|
|
4498
|
+
`);
|
|
3244
4499
|
process.exit(1);
|
|
3245
4500
|
}
|
|
3246
4501
|
saveGroup({ name, createdAt: (/* @__PURE__ */ new Date()).toISOString(), members: [] });
|
|
3247
|
-
console.log(
|
|
4502
|
+
console.log(`
|
|
4503
|
+
\u2705 Group "${name}" created.`);
|
|
4504
|
+
console.log(` Add repos with: code-intel group add ${name} <groupPath> <registryName>
|
|
4505
|
+
`);
|
|
3248
4506
|
});
|
|
3249
|
-
groupCmd.command("add <group> <groupPath> <registryName>").description("Add
|
|
4507
|
+
groupCmd.command("add <group> <groupPath> <registryName>").description("Add an indexed repository to a group at the given hierarchy path").addHelpText("after", `
|
|
4508
|
+
<groupPath> Dot-separated or slash-separated hierarchy path, e.g. hr/hiring/backend
|
|
4509
|
+
<registryName> The repo's name as shown by \`code-intel list\`
|
|
4510
|
+
|
|
4511
|
+
Examples:
|
|
4512
|
+
$ code-intel group add my-platform services/auth auth-service
|
|
4513
|
+
$ code-intel group add my-platform services/payments payments-api
|
|
4514
|
+
$ code-intel group add my-platform frontend web-app
|
|
4515
|
+
`).action((group, groupPath, registryName) => {
|
|
3250
4516
|
const registry = loadRegistry();
|
|
3251
4517
|
const regEntry = registry.find((r) => r.name === registryName);
|
|
3252
4518
|
if (!regEntry) {
|
|
3253
|
-
console.error(`
|
|
4519
|
+
console.error(`
|
|
4520
|
+
\u2717 Registry entry "${registryName}" not found.`);
|
|
4521
|
+
console.error(` Run \`code-intel list\` to see available repos.
|
|
4522
|
+
`);
|
|
3254
4523
|
process.exit(1);
|
|
3255
4524
|
}
|
|
3256
4525
|
if (!groupExists(group)) {
|
|
3257
|
-
console.error(`
|
|
4526
|
+
console.error(`
|
|
4527
|
+
\u2717 Group "${group}" does not exist.`);
|
|
4528
|
+
console.error(` Create it first: code-intel group create ${group}
|
|
4529
|
+
`);
|
|
3258
4530
|
process.exit(1);
|
|
3259
4531
|
}
|
|
3260
4532
|
addMember(group, { groupPath, registryName });
|
|
3261
|
-
console.log(
|
|
4533
|
+
console.log(`
|
|
4534
|
+
\u2705 Added "${registryName}" \u2192 group "${group}" at path "${groupPath}"
|
|
4535
|
+
`);
|
|
3262
4536
|
});
|
|
3263
|
-
groupCmd.command("remove <group> <groupPath>").description("Remove a
|
|
4537
|
+
groupCmd.command("remove <group> <groupPath>").description("Remove a repository from a group by its hierarchy path").addHelpText("after", `
|
|
4538
|
+
Examples:
|
|
4539
|
+
$ code-intel group remove my-platform services/auth
|
|
4540
|
+
`).action((group, groupPath) => {
|
|
3264
4541
|
if (!groupExists(group)) {
|
|
3265
|
-
console.error(`
|
|
4542
|
+
console.error(`
|
|
4543
|
+
\u2717 Group "${group}" does not exist.
|
|
4544
|
+
`);
|
|
3266
4545
|
process.exit(1);
|
|
3267
4546
|
}
|
|
3268
4547
|
try {
|
|
3269
4548
|
removeMember(group, groupPath);
|
|
3270
|
-
console.log(
|
|
4549
|
+
console.log(`
|
|
4550
|
+
\u2705 Removed "${groupPath}" from group "${group}"
|
|
4551
|
+
`);
|
|
3271
4552
|
} catch (err) {
|
|
3272
|
-
console.error(`
|
|
4553
|
+
console.error(`
|
|
4554
|
+
\u2717 ${err instanceof Error ? err.message : err}
|
|
4555
|
+
`);
|
|
3273
4556
|
process.exit(1);
|
|
3274
4557
|
}
|
|
3275
4558
|
});
|
|
3276
|
-
groupCmd.command("list [name]").description("List all groups, or show one group
|
|
4559
|
+
groupCmd.command("list [name]").description("List all groups, or show the full config of one group").addHelpText("after", `
|
|
4560
|
+
Examples:
|
|
4561
|
+
$ code-intel group list
|
|
4562
|
+
$ code-intel group list my-platform
|
|
4563
|
+
`).action((name) => {
|
|
3277
4564
|
if (name) {
|
|
3278
4565
|
const group = loadGroup(name);
|
|
3279
4566
|
if (!group) {
|
|
3280
|
-
console.error(`
|
|
4567
|
+
console.error(`
|
|
4568
|
+
\u2717 Group "${name}" not found.
|
|
4569
|
+
`);
|
|
3281
4570
|
process.exit(1);
|
|
3282
4571
|
}
|
|
3283
4572
|
console.log(`
|
|
3284
|
-
Group: ${group.name}`);
|
|
3285
|
-
console.log(`Created: ${group.createdAt}`);
|
|
3286
|
-
if (group.lastSync) console.log(`Last sync: ${group.lastSync}`);
|
|
4573
|
+
\u25C8 Group: ${group.name}`);
|
|
4574
|
+
console.log(` Created : ${group.createdAt}`);
|
|
4575
|
+
if (group.lastSync) console.log(` Last sync: ${group.lastSync}`);
|
|
3287
4576
|
console.log(`
|
|
3288
|
-
Members (${group.members.length}):`);
|
|
4577
|
+
Members (${group.members.length}):`);
|
|
3289
4578
|
if (group.members.length === 0) {
|
|
3290
|
-
console.log("
|
|
4579
|
+
console.log(" (none \u2014 use `code-intel group add` to add repos)");
|
|
3291
4580
|
} else {
|
|
3292
4581
|
for (const m of group.members) {
|
|
3293
|
-
console.log(`
|
|
4582
|
+
console.log(` ${m.groupPath.padEnd(35)} \u2192 ${m.registryName}`);
|
|
3294
4583
|
}
|
|
3295
4584
|
}
|
|
4585
|
+
console.log("");
|
|
3296
4586
|
} else {
|
|
3297
4587
|
const groups = listGroups();
|
|
3298
4588
|
if (groups.length === 0) {
|
|
3299
|
-
console.log("No groups found.
|
|
4589
|
+
console.log("\n No groups found.");
|
|
4590
|
+
console.log(" Create one with: code-intel group create <name>\n");
|
|
3300
4591
|
return;
|
|
3301
4592
|
}
|
|
3302
4593
|
console.log(`
|
|
3303
|
-
Repository groups (${groups.length}):
|
|
4594
|
+
Repository groups (${groups.length}):
|
|
3304
4595
|
`);
|
|
3305
4596
|
for (const g of groups) {
|
|
3306
|
-
const sync = g.lastSync ? `synced ${g.lastSync}` : "
|
|
3307
|
-
console.log(` ${g.name.padEnd(25)} ${g.members.length} member(s) [${sync}]`);
|
|
4597
|
+
const sync = g.lastSync ? `synced ${g.lastSync}` : "never synced";
|
|
4598
|
+
console.log(` \u25C6 ${g.name.padEnd(25)} ${g.members.length} member(s) [${sync}]`);
|
|
3308
4599
|
}
|
|
4600
|
+
console.log("");
|
|
3309
4601
|
}
|
|
3310
4602
|
});
|
|
3311
|
-
groupCmd.command("sync <name>").description("Extract contracts and
|
|
4603
|
+
groupCmd.command("sync <name>").description("Extract contracts and detect cross-repo dependencies in a group").addHelpText("after", `
|
|
4604
|
+
Scans every member repo's knowledge graph for exported symbols, routes,
|
|
4605
|
+
schemas, and events, then cross-matches names across repos to find
|
|
4606
|
+
likely provider \u2192 consumer relationships.
|
|
4607
|
+
|
|
4608
|
+
Examples:
|
|
4609
|
+
$ code-intel group sync my-platform
|
|
4610
|
+
`).action(async (name) => {
|
|
3312
4611
|
const group = loadGroup(name);
|
|
3313
4612
|
if (!group) {
|
|
3314
|
-
console.error(`
|
|
4613
|
+
console.error(`
|
|
4614
|
+
\u2717 Group "${name}" not found.
|
|
4615
|
+
`);
|
|
3315
4616
|
process.exit(1);
|
|
3316
4617
|
}
|
|
3317
4618
|
if (group.members.length === 0) {
|
|
3318
|
-
console.error(`
|
|
4619
|
+
console.error(`
|
|
4620
|
+
\u2717 Group "${name}" has no members.`);
|
|
4621
|
+
console.error(` Add repos with \`code-intel group add\`.
|
|
4622
|
+
`);
|
|
3319
4623
|
process.exit(1);
|
|
3320
4624
|
}
|
|
3321
4625
|
console.log(`
|
|
3322
|
-
\
|
|
4626
|
+
\u27F3 Syncing group "${name}" (${group.members.length} member(s))\u2026
|
|
3323
4627
|
`);
|
|
3324
4628
|
const result = await syncGroup(group);
|
|
3325
4629
|
saveSyncResult(result);
|
|
3326
4630
|
group.lastSync = result.syncedAt;
|
|
3327
4631
|
saveGroup(group);
|
|
3328
|
-
console.log(`
|
|
3329
|
-
|
|
3330
|
-
console.log(`
|
|
3331
|
-
console.log(`
|
|
3332
|
-
console.log(`
|
|
4632
|
+
console.log(` \u2705 Sync complete
|
|
4633
|
+
`);
|
|
4634
|
+
console.log(` Repos synced : ${result.memberCount}`);
|
|
4635
|
+
console.log(` Contracts : ${result.contracts.length}`);
|
|
4636
|
+
console.log(` Cross-links : ${result.links.length}`);
|
|
3333
4637
|
if (result.links.length > 0) {
|
|
3334
4638
|
console.log(`
|
|
3335
|
-
Top cross-repo links
|
|
4639
|
+
Top cross-repo links:
|
|
4640
|
+
`);
|
|
3336
4641
|
for (const link of result.links.slice(0, 10)) {
|
|
3337
4642
|
const conf = (link.confidence * 100).toFixed(0).padStart(3);
|
|
3338
4643
|
console.log(` ${conf}% ${link.providerRepo} \u2237 ${link.providerContract.padEnd(30)} \u2194 ${link.consumerRepo} \u2237 ${link.consumerContract}`);
|
|
3339
4644
|
}
|
|
3340
4645
|
if (result.links.length > 10) {
|
|
3341
|
-
console.log(`
|
|
4646
|
+
console.log(`
|
|
4647
|
+
\u2026 and ${result.links.length - 10} more. Run \`code-intel group contracts ${name}\` for full details.`);
|
|
3342
4648
|
}
|
|
3343
4649
|
}
|
|
4650
|
+
console.log("");
|
|
3344
4651
|
});
|
|
3345
|
-
groupCmd.command("contracts <name>").description("Inspect extracted contracts and cross-links from the last sync").option("--kind <kind>", "Filter by contract kind: export | route | schema | event").option("--repo <repo>", "Filter by registry name").option("--min-confidence <pct>", "Minimum link confidence 0
|
|
4652
|
+
groupCmd.command("contracts <name>").description("Inspect extracted contracts and cross-links from the last sync").option("--kind <kind>", "Filter by contract kind: export | route | schema | event").option("--repo <repo>", "Filter by registry name").option("--min-confidence <pct>", "Minimum link confidence 0\u2013100 (default: 0)", "0").addHelpText("after", `
|
|
4653
|
+
Examples:
|
|
4654
|
+
$ code-intel group contracts my-platform
|
|
4655
|
+
$ code-intel group contracts my-platform --kind route
|
|
4656
|
+
$ code-intel group contracts my-platform --repo auth-service --min-confidence 70
|
|
4657
|
+
`).action((name, opts) => {
|
|
3346
4658
|
const result = loadSyncResult(name);
|
|
3347
4659
|
if (!result) {
|
|
3348
|
-
console.error(`
|
|
4660
|
+
console.error(`
|
|
4661
|
+
\u2717 No sync data for group "${name}".`);
|
|
4662
|
+
console.error(` Run: code-intel group sync ${name}
|
|
4663
|
+
`);
|
|
3349
4664
|
process.exit(1);
|
|
3350
4665
|
}
|
|
3351
4666
|
const minConf = parseInt(opts.minConfidence, 10) / 100;
|
|
@@ -3355,15 +4670,17 @@ groupCmd.command("contracts <name>").description("Inspect extracted contracts an
|
|
|
3355
4670
|
let links = result.links.filter((l) => l.confidence >= minConf);
|
|
3356
4671
|
if (opts.repo) links = links.filter((l) => l.providerRepo === opts.repo || l.consumerRepo === opts.repo);
|
|
3357
4672
|
console.log(`
|
|
3358
|
-
\
|
|
4673
|
+
\u25C8 Group "${name}" \u2014 synced ${result.syncedAt}
|
|
4674
|
+
`);
|
|
4675
|
+
console.log(` Contracts (${contracts.length}):
|
|
3359
4676
|
`);
|
|
3360
|
-
console.log(`Contracts (${contracts.length}):`);
|
|
3361
4677
|
for (const c of contracts) {
|
|
3362
|
-
const sig = c.signature ? ` ${c.signature.slice(0,
|
|
3363
|
-
console.log(` [${c.kind.padEnd(6)}]
|
|
4678
|
+
const sig = c.signature ? ` ${c.signature.slice(0, 55)}` : "";
|
|
4679
|
+
console.log(` [${c.kind.padEnd(6)}] ${c.repoName.padEnd(22)} ${c.name.padEnd(35)}${sig}`);
|
|
3364
4680
|
}
|
|
3365
4681
|
console.log(`
|
|
3366
|
-
Cross-repo links (${links.length})
|
|
4682
|
+
Cross-repo links (${links.length}):
|
|
4683
|
+
`);
|
|
3367
4684
|
if (links.length === 0) {
|
|
3368
4685
|
console.log(" (none)");
|
|
3369
4686
|
} else {
|
|
@@ -3372,72 +4689,92 @@ Cross-repo links (${links.length}):`);
|
|
|
3372
4689
|
console.log(` ${conf}% [${link.matchKind}] ${link.providerRepo} \u2237 ${link.providerContract.padEnd(30)} \u2194 ${link.consumerRepo} \u2237 ${link.consumerContract}`);
|
|
3373
4690
|
}
|
|
3374
4691
|
}
|
|
4692
|
+
console.log("");
|
|
3375
4693
|
});
|
|
3376
|
-
groupCmd.command("query <name> <q>").description("Search execution flows across all repos in a group").option("-l, --limit <
|
|
4694
|
+
groupCmd.command("query <name> <q>").description("Search execution flows across all repos in a group").option("-l, --limit <n>", "Max results per repo", "10").addHelpText("after", `
|
|
4695
|
+
Uses BM25 search within each member repo's graph, then merges the results
|
|
4696
|
+
using Reciprocal Rank Fusion (RRF) for a unified ranked list.
|
|
4697
|
+
|
|
4698
|
+
Examples:
|
|
4699
|
+
$ code-intel group query my-platform "handlePayment"
|
|
4700
|
+
$ code-intel group query my-platform "UserAuth" --limit 5
|
|
4701
|
+
`).action(async (name, q, opts) => {
|
|
3377
4702
|
const group = loadGroup(name);
|
|
3378
4703
|
if (!group) {
|
|
3379
|
-
console.error(`
|
|
4704
|
+
console.error(`
|
|
4705
|
+
\u2717 Group "${name}" not found.
|
|
4706
|
+
`);
|
|
3380
4707
|
process.exit(1);
|
|
3381
4708
|
}
|
|
3382
4709
|
console.log(`
|
|
3383
|
-
\
|
|
4710
|
+
\u25C8 Querying group "${name}" for: "${q}"
|
|
3384
4711
|
`);
|
|
3385
4712
|
const limit = parseInt(opts.limit, 10);
|
|
3386
4713
|
const { perRepo, merged } = await queryGroup(group, q, limit);
|
|
3387
4714
|
if (merged.length === 0) {
|
|
3388
|
-
console.log("No results found across any repo in this group
|
|
4715
|
+
console.log(" No results found across any repo in this group.\n");
|
|
3389
4716
|
return;
|
|
3390
4717
|
}
|
|
3391
|
-
console.log(`Merged results (${merged.length}
|
|
4718
|
+
console.log(` Merged results (${merged.length}, ranked by RRF):
|
|
3392
4719
|
`);
|
|
3393
4720
|
for (const r of merged) {
|
|
3394
|
-
console.log(` ${r.kind.padEnd(
|
|
3395
|
-
if (r.snippet) console.log(`
|
|
4721
|
+
console.log(` ${r.kind.padEnd(14)} ${r.name.padEnd(32)} ${r.filePath}`);
|
|
4722
|
+
if (r.snippet) console.log(` ${r.snippet.slice(0, 95)}`);
|
|
3396
4723
|
}
|
|
3397
4724
|
console.log(`
|
|
3398
|
-
Per-repo breakdown
|
|
4725
|
+
Per-repo breakdown:
|
|
4726
|
+
`);
|
|
3399
4727
|
for (const rr of perRepo) {
|
|
3400
|
-
console.log(`
|
|
4728
|
+
console.log(` ${rr.repoName.padEnd(25)} (${rr.groupPath}) \u2192 ${rr.results.length} result(s)`);
|
|
3401
4729
|
}
|
|
4730
|
+
console.log("");
|
|
3402
4731
|
});
|
|
3403
|
-
groupCmd.command("status <name>").description("Check
|
|
4732
|
+
groupCmd.command("status <name>").description("Check index freshness and sync status of all repos in a group").addHelpText("after", `
|
|
4733
|
+
Examples:
|
|
4734
|
+
$ code-intel group status my-platform
|
|
4735
|
+
`).action((name) => {
|
|
3404
4736
|
const group = loadGroup(name);
|
|
3405
4737
|
if (!group) {
|
|
3406
|
-
console.error(`
|
|
4738
|
+
console.error(`
|
|
4739
|
+
\u2717 Group "${name}" not found.
|
|
4740
|
+
`);
|
|
3407
4741
|
process.exit(1);
|
|
3408
4742
|
}
|
|
3409
4743
|
const registry = loadRegistry();
|
|
3410
4744
|
const now = Date.now();
|
|
3411
4745
|
console.log(`
|
|
3412
|
-
\
|
|
4746
|
+
\u25C8 Group "${name}" \u2014 status
|
|
3413
4747
|
`);
|
|
3414
4748
|
if (group.lastSync) {
|
|
3415
4749
|
const age = Math.round((now - new Date(group.lastSync).getTime()) / 6e4);
|
|
3416
|
-
console.log(`Last sync: ${group.lastSync} (${age} min ago)`);
|
|
4750
|
+
console.log(` Last sync : ${group.lastSync} (${age} min ago)`);
|
|
3417
4751
|
} else {
|
|
3418
|
-
console.log(
|
|
4752
|
+
console.log(` Last sync : never \u2192 run \`code-intel group sync ${name}\``);
|
|
3419
4753
|
}
|
|
3420
4754
|
console.log(`
|
|
3421
|
-
Members (${group.members.length}):
|
|
4755
|
+
Members (${group.members.length}):
|
|
3422
4756
|
`);
|
|
3423
4757
|
for (const m of group.members) {
|
|
3424
4758
|
const regEntry = registry.find((r) => r.name === m.registryName);
|
|
3425
4759
|
if (!regEntry) {
|
|
3426
|
-
console.log(` \u2717
|
|
4760
|
+
console.log(` \u2717 ${m.groupPath.padEnd(35)} [${m.registryName}] \u2014 NOT IN REGISTRY`);
|
|
3427
4761
|
continue;
|
|
3428
4762
|
}
|
|
3429
|
-
const metaPath =
|
|
3430
|
-
let indexedAt = regEntry.indexedAt;
|
|
4763
|
+
const metaPath = path16.join(regEntry.path, ".code-intel", "meta.json");
|
|
3431
4764
|
try {
|
|
3432
|
-
const meta = JSON.parse(
|
|
3433
|
-
indexedAt = meta.indexedAt;
|
|
4765
|
+
const meta = JSON.parse(fs14.readFileSync(metaPath, "utf-8"));
|
|
4766
|
+
const indexedAt = meta.indexedAt;
|
|
3434
4767
|
const ageMin = Math.round((now - new Date(indexedAt).getTime()) / 6e4);
|
|
3435
|
-
const stale = ageMin > 1440 ? "
|
|
3436
|
-
console.log(` \u2713
|
|
3437
|
-
console.log(`
|
|
3438
|
-
console.log(`
|
|
4768
|
+
const stale = ageMin > 1440 ? " \u26A0 STALE (>24h)" : "";
|
|
4769
|
+
console.log(` \u2713 ${m.groupPath.padEnd(35)} [${m.registryName}]${stale}`);
|
|
4770
|
+
console.log(` indexed ${indexedAt} (${ageMin} min ago)`);
|
|
4771
|
+
console.log(` ${meta.stats.nodes} nodes \xB7 ${meta.stats.edges} edges \xB7 ${meta.stats.files} files`);
|
|
4772
|
+
console.log(` ${regEntry.path}
|
|
4773
|
+
`);
|
|
3439
4774
|
} catch {
|
|
3440
|
-
console.log(` \u2717
|
|
4775
|
+
console.log(` \u2717 ${m.groupPath.padEnd(35)} [${m.registryName}] \u2014 NOT INDEXED`);
|
|
4776
|
+
console.log(` run: code-intel analyze ${regEntry.path}
|
|
4777
|
+
`);
|
|
3441
4778
|
}
|
|
3442
4779
|
}
|
|
3443
4780
|
});
|