@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/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs6 from 'fs';
|
|
2
2
|
import path6 from 'path';
|
|
3
|
-
import
|
|
3
|
+
import os3 from 'os';
|
|
4
4
|
import { Parser, Language, Query } from 'web-tree-sitter';
|
|
5
|
+
import winston from 'winston';
|
|
6
|
+
import DailyRotateFile from 'winston-daily-rotate-file';
|
|
5
7
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
9
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { Database, Connection } from '@ladybugdb/core';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
8
12
|
import express from 'express';
|
|
9
13
|
import cors from 'cors';
|
|
10
14
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import { Database, Connection } from '@ladybugdb/core';
|
|
12
15
|
|
|
13
16
|
var __defProp = Object.defineProperty;
|
|
14
17
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -86,23 +89,23 @@ function groupFile(name) {
|
|
|
86
89
|
}
|
|
87
90
|
function loadGroup(name) {
|
|
88
91
|
try {
|
|
89
|
-
return JSON.parse(
|
|
92
|
+
return JSON.parse(fs6.readFileSync(groupFile(name), "utf-8"));
|
|
90
93
|
} catch {
|
|
91
94
|
return null;
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
function saveGroup(group) {
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
fs6.mkdirSync(GROUPS_DIR, { recursive: true });
|
|
99
|
+
fs6.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
|
|
97
100
|
}
|
|
98
101
|
function listGroups() {
|
|
99
102
|
const groups = [];
|
|
100
103
|
try {
|
|
101
|
-
for (const file of
|
|
104
|
+
for (const file of fs6.readdirSync(GROUPS_DIR)) {
|
|
102
105
|
if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
|
|
103
106
|
try {
|
|
104
107
|
const g = JSON.parse(
|
|
105
|
-
|
|
108
|
+
fs6.readFileSync(path6.join(GROUPS_DIR, file), "utf-8")
|
|
106
109
|
);
|
|
107
110
|
groups.push(g);
|
|
108
111
|
} catch {
|
|
@@ -114,16 +117,16 @@ function listGroups() {
|
|
|
114
117
|
}
|
|
115
118
|
function deleteGroup(name) {
|
|
116
119
|
try {
|
|
117
|
-
|
|
120
|
+
fs6.unlinkSync(groupFile(name));
|
|
118
121
|
} catch {
|
|
119
122
|
}
|
|
120
123
|
try {
|
|
121
|
-
|
|
124
|
+
fs6.unlinkSync(path6.join(GROUPS_DIR, `${name}.sync.json`));
|
|
122
125
|
} catch {
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
function groupExists(name) {
|
|
126
|
-
return
|
|
129
|
+
return fs6.existsSync(groupFile(name));
|
|
127
130
|
}
|
|
128
131
|
function addMember(groupName, member) {
|
|
129
132
|
const group = loadGroup(groupName);
|
|
@@ -149,8 +152,8 @@ function removeMember(groupName, groupPath) {
|
|
|
149
152
|
return group;
|
|
150
153
|
}
|
|
151
154
|
function saveSyncResult(result) {
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
fs6.mkdirSync(GROUPS_DIR, { recursive: true });
|
|
156
|
+
fs6.writeFileSync(
|
|
154
157
|
path6.join(GROUPS_DIR, `${result.groupName}.sync.json`),
|
|
155
158
|
JSON.stringify(result, null, 2) + "\n"
|
|
156
159
|
);
|
|
@@ -158,7 +161,7 @@ function saveSyncResult(result) {
|
|
|
158
161
|
function loadSyncResult(groupName) {
|
|
159
162
|
try {
|
|
160
163
|
return JSON.parse(
|
|
161
|
-
|
|
164
|
+
fs6.readFileSync(path6.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
|
|
162
165
|
);
|
|
163
166
|
} catch {
|
|
164
167
|
return null;
|
|
@@ -167,7 +170,7 @@ function loadSyncResult(groupName) {
|
|
|
167
170
|
var GROUPS_DIR;
|
|
168
171
|
var init_group_registry = __esm({
|
|
169
172
|
"src/multi-repo/group-registry.ts"() {
|
|
170
|
-
GROUPS_DIR = path6.join(
|
|
173
|
+
GROUPS_DIR = path6.join(os3.homedir(), ".code-intel", "groups");
|
|
171
174
|
}
|
|
172
175
|
});
|
|
173
176
|
|
|
@@ -1664,25 +1667,25 @@ function validateDAG(phases) {
|
|
|
1664
1667
|
const visiting = /* @__PURE__ */ new Set();
|
|
1665
1668
|
const visited = /* @__PURE__ */ new Set();
|
|
1666
1669
|
const phaseMap = new Map(phases.map((p) => [p.name, p]));
|
|
1667
|
-
function dfs(name,
|
|
1670
|
+
function dfs(name, path19) {
|
|
1668
1671
|
if (visiting.has(name)) {
|
|
1669
|
-
const cycleStart =
|
|
1670
|
-
const cycle =
|
|
1672
|
+
const cycleStart = path19.indexOf(name);
|
|
1673
|
+
const cycle = path19.slice(cycleStart).concat(name);
|
|
1671
1674
|
errors.push({ type: "cycle", message: `Cycle detected: ${cycle.join(" \u2192 ")}` });
|
|
1672
1675
|
return true;
|
|
1673
1676
|
}
|
|
1674
1677
|
if (visited.has(name)) return false;
|
|
1675
1678
|
visiting.add(name);
|
|
1676
|
-
|
|
1679
|
+
path19.push(name);
|
|
1677
1680
|
const phase = phaseMap.get(name);
|
|
1678
1681
|
if (phase) {
|
|
1679
1682
|
for (const dep of phase.dependencies) {
|
|
1680
|
-
if (dfs(dep,
|
|
1683
|
+
if (dfs(dep, path19)) return true;
|
|
1681
1684
|
}
|
|
1682
1685
|
}
|
|
1683
1686
|
visiting.delete(name);
|
|
1684
1687
|
visited.add(name);
|
|
1685
|
-
|
|
1688
|
+
path19.pop();
|
|
1686
1689
|
return false;
|
|
1687
1690
|
}
|
|
1688
1691
|
for (const phase of phases) {
|
|
@@ -1790,11 +1793,18 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1790
1793
|
".cache",
|
|
1791
1794
|
"tmp",
|
|
1792
1795
|
"temp",
|
|
1793
|
-
".parcel-cache"
|
|
1796
|
+
".parcel-cache",
|
|
1797
|
+
".venv",
|
|
1798
|
+
"venv",
|
|
1799
|
+
".env",
|
|
1800
|
+
"env",
|
|
1801
|
+
"__snapshots__",
|
|
1802
|
+
".nyc_output",
|
|
1803
|
+
"storybook-static"
|
|
1794
1804
|
]);
|
|
1795
1805
|
function loadIgnorePatterns(workspaceRoot) {
|
|
1796
1806
|
try {
|
|
1797
|
-
const raw =
|
|
1807
|
+
const raw = fs6.readFileSync(path6.join(workspaceRoot, ".codeintelignore"), "utf-8");
|
|
1798
1808
|
const extras = /* @__PURE__ */ new Set();
|
|
1799
1809
|
for (const line of raw.split("\n")) {
|
|
1800
1810
|
const trimmed = line.trim();
|
|
@@ -1805,6 +1815,8 @@ function loadIgnorePatterns(workspaceRoot) {
|
|
|
1805
1815
|
return /* @__PURE__ */ new Set();
|
|
1806
1816
|
}
|
|
1807
1817
|
}
|
|
1818
|
+
var IGNORED_FILE_SUFFIXES = [".d.ts", ".js.map", ".d.ts.map", ".min.js", ".min.css"];
|
|
1819
|
+
var MAX_FILE_SIZE_BYTES = 512 * 1024;
|
|
1808
1820
|
var scanPhase = {
|
|
1809
1821
|
name: "scan",
|
|
1810
1822
|
dependencies: [],
|
|
@@ -1816,29 +1828,35 @@ var scanPhase = {
|
|
|
1816
1828
|
function walk(dir) {
|
|
1817
1829
|
let entries;
|
|
1818
1830
|
try {
|
|
1819
|
-
entries =
|
|
1831
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1820
1832
|
} catch {
|
|
1821
1833
|
return;
|
|
1822
1834
|
}
|
|
1823
1835
|
for (const entry of entries) {
|
|
1824
|
-
if (entry.name.startsWith(".") && entry.isDirectory()) continue;
|
|
1825
|
-
if (IGNORED_DIRS.has(entry.name) && entry.isDirectory()) continue;
|
|
1826
|
-
if (extraIgnore.has(entry.name) && entry.isDirectory()) continue;
|
|
1827
|
-
const fullPath = path6.join(dir, entry.name);
|
|
1828
1836
|
if (entry.isDirectory()) {
|
|
1829
|
-
|
|
1837
|
+
if (entry.name.startsWith(".")) continue;
|
|
1838
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
1839
|
+
if (extraIgnore.has(entry.name)) continue;
|
|
1840
|
+
walk(path6.join(dir, entry.name));
|
|
1830
1841
|
} else if (entry.isFile()) {
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
if (extensions.has(ext))
|
|
1835
|
-
|
|
1842
|
+
const name = entry.name;
|
|
1843
|
+
if (IGNORED_FILE_SUFFIXES.some((s) => name.endsWith(s))) continue;
|
|
1844
|
+
const ext = path6.extname(name);
|
|
1845
|
+
if (!extensions.has(ext)) continue;
|
|
1846
|
+
const fullPath = path6.join(dir, name);
|
|
1847
|
+
try {
|
|
1848
|
+
const stat = fs6.statSync(fullPath);
|
|
1849
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) continue;
|
|
1850
|
+
} catch {
|
|
1851
|
+
continue;
|
|
1836
1852
|
}
|
|
1853
|
+
filePaths.push(fullPath);
|
|
1837
1854
|
}
|
|
1838
1855
|
}
|
|
1839
1856
|
}
|
|
1840
1857
|
walk(context.workspaceRoot);
|
|
1841
1858
|
context.filePaths.push(...filePaths);
|
|
1859
|
+
context.onPhaseProgress?.("scan", filePaths.length, filePaths.length);
|
|
1842
1860
|
return {
|
|
1843
1861
|
status: "completed",
|
|
1844
1862
|
duration: Date.now() - start,
|
|
@@ -1852,6 +1870,7 @@ var structurePhase = {
|
|
|
1852
1870
|
async execute(context) {
|
|
1853
1871
|
const start = Date.now();
|
|
1854
1872
|
const dirs = /* @__PURE__ */ new Set();
|
|
1873
|
+
let structDone = 0;
|
|
1855
1874
|
for (const filePath of context.filePaths) {
|
|
1856
1875
|
const relativePath = path6.relative(context.workspaceRoot, filePath);
|
|
1857
1876
|
const lang = detectLanguage(filePath);
|
|
@@ -1868,6 +1887,8 @@ var structurePhase = {
|
|
|
1868
1887
|
dirs.add(dir);
|
|
1869
1888
|
dir = path6.dirname(dir);
|
|
1870
1889
|
}
|
|
1890
|
+
structDone++;
|
|
1891
|
+
context.onPhaseProgress?.("structure", structDone, context.filePaths.length);
|
|
1871
1892
|
}
|
|
1872
1893
|
for (const dir of dirs) {
|
|
1873
1894
|
context.graph.addNode({
|
|
@@ -1884,29 +1905,266 @@ var structurePhase = {
|
|
|
1884
1905
|
};
|
|
1885
1906
|
}
|
|
1886
1907
|
};
|
|
1908
|
+
var SENSITIVE_KEYS = [
|
|
1909
|
+
"password",
|
|
1910
|
+
"passwd",
|
|
1911
|
+
"pass",
|
|
1912
|
+
"pwd",
|
|
1913
|
+
"secret",
|
|
1914
|
+
"secretkey",
|
|
1915
|
+
"secret_key",
|
|
1916
|
+
"secretaccesskey",
|
|
1917
|
+
"accesskeyid",
|
|
1918
|
+
"credentials",
|
|
1919
|
+
"auth",
|
|
1920
|
+
"authentication",
|
|
1921
|
+
"login",
|
|
1922
|
+
"api_key",
|
|
1923
|
+
"apikey",
|
|
1924
|
+
"api",
|
|
1925
|
+
"access_key",
|
|
1926
|
+
"access_token",
|
|
1927
|
+
"accesskey",
|
|
1928
|
+
"auth_key",
|
|
1929
|
+
"auth_token",
|
|
1930
|
+
"authkey",
|
|
1931
|
+
"token",
|
|
1932
|
+
"jwt",
|
|
1933
|
+
"bearer_token",
|
|
1934
|
+
"refresh_token",
|
|
1935
|
+
"session_token",
|
|
1936
|
+
"session_key",
|
|
1937
|
+
"oauth_token",
|
|
1938
|
+
"connection_string",
|
|
1939
|
+
"conn_string",
|
|
1940
|
+
"db_uri",
|
|
1941
|
+
"db_url",
|
|
1942
|
+
"database_url",
|
|
1943
|
+
"mongodb_uri",
|
|
1944
|
+
"mysql_uri",
|
|
1945
|
+
"postgres_uri",
|
|
1946
|
+
"sql_uri",
|
|
1947
|
+
"db_username",
|
|
1948
|
+
"db_password",
|
|
1949
|
+
"db_host",
|
|
1950
|
+
"db_port",
|
|
1951
|
+
"db_name",
|
|
1952
|
+
"encryption_key",
|
|
1953
|
+
"crypto_key",
|
|
1954
|
+
"private_key",
|
|
1955
|
+
"public_key",
|
|
1956
|
+
"ssl_key",
|
|
1957
|
+
"ssh_key",
|
|
1958
|
+
"pgp_key",
|
|
1959
|
+
"rsa_key",
|
|
1960
|
+
"aes_key",
|
|
1961
|
+
"email",
|
|
1962
|
+
"phone",
|
|
1963
|
+
"telephone",
|
|
1964
|
+
"mobile",
|
|
1965
|
+
"ssn",
|
|
1966
|
+
"social_security",
|
|
1967
|
+
"credit_card",
|
|
1968
|
+
"cc_number",
|
|
1969
|
+
"card_number",
|
|
1970
|
+
"cvv",
|
|
1971
|
+
"expiry_date",
|
|
1972
|
+
"birth_date",
|
|
1973
|
+
"dob",
|
|
1974
|
+
"address",
|
|
1975
|
+
"zip_code",
|
|
1976
|
+
"postal_code",
|
|
1977
|
+
"bank_account",
|
|
1978
|
+
"iban",
|
|
1979
|
+
"swift_code",
|
|
1980
|
+
"routing_number",
|
|
1981
|
+
"tax_id",
|
|
1982
|
+
"vat_number",
|
|
1983
|
+
"financial_id",
|
|
1984
|
+
"certificate",
|
|
1985
|
+
"client_cert",
|
|
1986
|
+
"server_cert",
|
|
1987
|
+
"ca_cert",
|
|
1988
|
+
"aws_key",
|
|
1989
|
+
"aws_secret",
|
|
1990
|
+
"azure_key",
|
|
1991
|
+
"gcp_key",
|
|
1992
|
+
"s3_key",
|
|
1993
|
+
"cloudinary_key",
|
|
1994
|
+
"stripe_key",
|
|
1995
|
+
"paypal_key",
|
|
1996
|
+
"twilio_key",
|
|
1997
|
+
"app_secret",
|
|
1998
|
+
"client_secret",
|
|
1999
|
+
"consumer_secret",
|
|
2000
|
+
"encryption_secret",
|
|
2001
|
+
"master_key",
|
|
2002
|
+
"root_password",
|
|
2003
|
+
"admin_password",
|
|
2004
|
+
"config_secret",
|
|
2005
|
+
"env_secret",
|
|
2006
|
+
"deploy_key",
|
|
2007
|
+
"ci_key",
|
|
2008
|
+
"session_id",
|
|
2009
|
+
"cookie_secret",
|
|
2010
|
+
"csrf_token",
|
|
2011
|
+
"xsrf_token",
|
|
2012
|
+
"license_key",
|
|
2013
|
+
"product_key",
|
|
2014
|
+
"serial_number",
|
|
2015
|
+
"activation_code"
|
|
2016
|
+
];
|
|
2017
|
+
var SENSITIVE_PATTERNS = [
|
|
2018
|
+
/(?:password|passwd|secret|api_key|access_token|auth_token|token)\s*[:=]\s*([^\s,]+)/gi,
|
|
2019
|
+
/\b\d{16}\b/gi,
|
|
2020
|
+
/\b\d{3}-\d{2}-\d{4}\b/gi,
|
|
2021
|
+
/\b[A-Za-z0-9]{32}\b/gi,
|
|
2022
|
+
/\b[A-Za-z0-9_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b/gi,
|
|
2023
|
+
/\b\d{10}\b/gi,
|
|
2024
|
+
/\b[A-Za-z0-9]{64}\b/gi,
|
|
2025
|
+
/(?:connection_string|db_uri|db_url|mongodb_uri)\s*[:=]\s*([^\s,]+)/gi,
|
|
2026
|
+
/(?:apikey|api_key|auth_key)\s*[:=]\s*([^\s,]+)/gi,
|
|
2027
|
+
/(?:bearer\s+)[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+/gi
|
|
2028
|
+
];
|
|
2029
|
+
var SENSITIVE_KEYS_REGEX = new RegExp(`^(${SENSITIVE_KEYS.join("|")})$`, "i");
|
|
2030
|
+
var Logger = class _Logger {
|
|
2031
|
+
static instance = null;
|
|
2032
|
+
static maskSensitiveData(value) {
|
|
2033
|
+
if (typeof value === "string" && value.length > 5) {
|
|
2034
|
+
const firstChar = value.at(0);
|
|
2035
|
+
const lastChar = value.at(-1);
|
|
2036
|
+
return firstChar + "*".repeat(value.length - 2) + lastChar;
|
|
2037
|
+
}
|
|
2038
|
+
return value;
|
|
2039
|
+
}
|
|
2040
|
+
static maskSensitive(message, args = []) {
|
|
2041
|
+
const maskString = (input) => {
|
|
2042
|
+
if (typeof input !== "string") return input;
|
|
2043
|
+
return SENSITIVE_PATTERNS.reduce((str, pattern) => {
|
|
2044
|
+
return str.replace(
|
|
2045
|
+
pattern,
|
|
2046
|
+
(match, value) => value ? match.replace(value, _Logger.maskSensitiveData(value)) : match
|
|
2047
|
+
);
|
|
2048
|
+
}, input);
|
|
2049
|
+
};
|
|
2050
|
+
const deepMask = (obj) => {
|
|
2051
|
+
if (typeof obj === "string") return maskString(obj);
|
|
2052
|
+
if (Array.isArray(obj)) return obj.map((item) => deepMask(item));
|
|
2053
|
+
if (typeof obj === "object" && obj !== null) {
|
|
2054
|
+
return Object.entries(obj).reduce(
|
|
2055
|
+
(acc, [key, value]) => {
|
|
2056
|
+
if (value === void 0) return acc;
|
|
2057
|
+
const isSensitiveKey = SENSITIVE_KEYS_REGEX.test(key);
|
|
2058
|
+
acc[key] = isSensitiveKey && typeof value === "string" ? _Logger.maskSensitiveData(value) : deepMask(value);
|
|
2059
|
+
return acc;
|
|
2060
|
+
},
|
|
2061
|
+
{}
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
return obj;
|
|
2065
|
+
};
|
|
2066
|
+
return {
|
|
2067
|
+
maskedMessage: maskString(message),
|
|
2068
|
+
maskedArgs: args.map((arg) => deepMask(arg))
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
/** Global log directory: ~/.code-intel/logs */
|
|
2072
|
+
static LOG_DIR = path6.join(os3.homedir(), ".code-intel", "logs");
|
|
2073
|
+
static getLogger() {
|
|
2074
|
+
if (!_Logger.instance) {
|
|
2075
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
2076
|
+
const logLevel = process.env.LOG_LEVEL ?? "info";
|
|
2077
|
+
const transports = [];
|
|
2078
|
+
transports.push(new winston.transports.Console());
|
|
2079
|
+
if (!isProduction) {
|
|
2080
|
+
try {
|
|
2081
|
+
if (!fs6.existsSync(_Logger.LOG_DIR)) {
|
|
2082
|
+
fs6.mkdirSync(_Logger.LOG_DIR, { recursive: true });
|
|
2083
|
+
}
|
|
2084
|
+
transports.push(
|
|
2085
|
+
new DailyRotateFile({
|
|
2086
|
+
filename: path6.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
|
|
2087
|
+
datePattern: "YYYY-MM-DD",
|
|
2088
|
+
maxSize: "20m",
|
|
2089
|
+
maxFiles: "14d"
|
|
2090
|
+
})
|
|
2091
|
+
);
|
|
2092
|
+
} catch {
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
_Logger.instance = winston.createLogger({
|
|
2096
|
+
level: logLevel,
|
|
2097
|
+
format: winston.format.combine(
|
|
2098
|
+
winston.format.timestamp(),
|
|
2099
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
2100
|
+
const args = meta[/* @__PURE__ */ Symbol.for("splat")] || [];
|
|
2101
|
+
const { maskedMessage, maskedArgs } = _Logger.maskSensitive(message, args);
|
|
2102
|
+
const formattedArgs = maskedArgs.map(
|
|
2103
|
+
(arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
|
|
2104
|
+
);
|
|
2105
|
+
const suffix = formattedArgs.length ? " " + formattedArgs.join(" ") : "";
|
|
2106
|
+
return `${timestamp} [${level.toUpperCase()}]: ${maskedMessage}${suffix}`;
|
|
2107
|
+
})
|
|
2108
|
+
),
|
|
2109
|
+
transports
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
return _Logger.instance;
|
|
2113
|
+
}
|
|
2114
|
+
static info(message, ...args) {
|
|
2115
|
+
_Logger.getLogger().info(message, ...args);
|
|
2116
|
+
}
|
|
2117
|
+
static warn(message, ...args) {
|
|
2118
|
+
_Logger.getLogger().warn(message, ...args);
|
|
2119
|
+
}
|
|
2120
|
+
static error(message, ...args) {
|
|
2121
|
+
_Logger.getLogger().error(message, ...args);
|
|
2122
|
+
}
|
|
2123
|
+
static debug(message, ...args) {
|
|
2124
|
+
_Logger.getLogger().debug(message, ...args);
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
var logger_default = Logger;
|
|
2128
|
+
Logger.getLogger();
|
|
2129
|
+
|
|
2130
|
+
// src/pipeline/phases/parse-phase.ts
|
|
1887
2131
|
var parsePhase = {
|
|
1888
2132
|
name: "parse",
|
|
1889
2133
|
dependencies: ["structure"],
|
|
1890
2134
|
async execute(context) {
|
|
1891
2135
|
const start = Date.now();
|
|
1892
2136
|
let symbolCount = 0;
|
|
1893
|
-
|
|
2137
|
+
if (!context.fileCache) context.fileCache = /* @__PURE__ */ new Map();
|
|
2138
|
+
if (!context.fileFunctionIndex) context.fileFunctionIndex = /* @__PURE__ */ new Map();
|
|
2139
|
+
const CONCURRENCY = 64;
|
|
2140
|
+
const filePaths = context.filePaths;
|
|
2141
|
+
let readDone = 0;
|
|
2142
|
+
for (let i = 0; i < filePaths.length; i += CONCURRENCY) {
|
|
2143
|
+
const batch = filePaths.slice(i, i + CONCURRENCY);
|
|
2144
|
+
await Promise.all(batch.map(async (filePath) => {
|
|
2145
|
+
try {
|
|
2146
|
+
const source = await fs6.promises.readFile(filePath, "utf-8");
|
|
2147
|
+
context.fileCache.set(filePath, source);
|
|
2148
|
+
} catch {
|
|
2149
|
+
}
|
|
2150
|
+
}));
|
|
2151
|
+
readDone += batch.length;
|
|
2152
|
+
context.onPhaseProgress?.("parse:read", readDone, filePaths.length);
|
|
2153
|
+
}
|
|
2154
|
+
let parseDone = 0;
|
|
2155
|
+
for (const filePath of filePaths) {
|
|
1894
2156
|
const lang = detectLanguage(filePath);
|
|
1895
2157
|
if (!lang) {
|
|
1896
2158
|
if (context.verbose) {
|
|
1897
2159
|
const relativePath2 = path6.relative(context.workspaceRoot, filePath);
|
|
1898
|
-
|
|
2160
|
+
logger_default.info(` [parse] skipped (no parser): ${relativePath2}`);
|
|
1899
2161
|
}
|
|
1900
2162
|
continue;
|
|
1901
2163
|
}
|
|
2164
|
+
const source = context.fileCache.get(filePath);
|
|
2165
|
+
if (!source) continue;
|
|
1902
2166
|
const relativePath = path6.relative(context.workspaceRoot, filePath);
|
|
1903
2167
|
const fileNodeId = generateNodeId("file", relativePath, relativePath);
|
|
1904
|
-
let source;
|
|
1905
|
-
try {
|
|
1906
|
-
source = fs8.readFileSync(filePath, "utf-8");
|
|
1907
|
-
} catch {
|
|
1908
|
-
continue;
|
|
1909
|
-
}
|
|
1910
2168
|
const fileNode = context.graph.getNode(fileNodeId);
|
|
1911
2169
|
if (fileNode) {
|
|
1912
2170
|
fileNode.content = source.slice(0, 2e3);
|
|
@@ -1921,15 +2179,18 @@ var parsePhase = {
|
|
|
1921
2179
|
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
1922
2180
|
const extracted = extractSymbol(trimmed, lang);
|
|
1923
2181
|
if (!extracted) continue;
|
|
1924
|
-
|
|
1925
|
-
seen.
|
|
2182
|
+
const dedupeKey = extracted.name + ":" + extracted.kind;
|
|
2183
|
+
if (seen.has(dedupeKey)) continue;
|
|
2184
|
+
seen.add(dedupeKey);
|
|
1926
2185
|
const nodeId = generateNodeId(extracted.kind, relativePath, extracted.name);
|
|
2186
|
+
const endLine = estimateEndLine(lines, i, lang);
|
|
1927
2187
|
nodes.push({
|
|
1928
2188
|
id: nodeId,
|
|
1929
2189
|
kind: extracted.kind,
|
|
1930
2190
|
name: extracted.name,
|
|
1931
2191
|
filePath: relativePath,
|
|
1932
2192
|
startLine: i + 1,
|
|
2193
|
+
endLine,
|
|
1933
2194
|
exported: extracted.exported,
|
|
1934
2195
|
content: extractBlock(lines, i, 20)
|
|
1935
2196
|
});
|
|
@@ -1956,11 +2217,17 @@ var parsePhase = {
|
|
|
1956
2217
|
}
|
|
1957
2218
|
for (const n of nodes) context.graph.addNode(n);
|
|
1958
2219
|
for (const e of edges) context.graph.addEdge(e);
|
|
2220
|
+
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);
|
|
2221
|
+
if (funcs.length > 0) {
|
|
2222
|
+
context.fileFunctionIndex.set(relativePath, funcs);
|
|
2223
|
+
}
|
|
2224
|
+
parseDone++;
|
|
2225
|
+
context.onPhaseProgress?.("parse", parseDone, filePaths.length);
|
|
1959
2226
|
}
|
|
1960
2227
|
return {
|
|
1961
2228
|
status: "completed",
|
|
1962
2229
|
duration: Date.now() - start,
|
|
1963
|
-
message: `Extracted ${symbolCount} symbols`
|
|
2230
|
+
message: `Extracted ${symbolCount} symbols from ${filePaths.length} files`
|
|
1964
2231
|
};
|
|
1965
2232
|
}
|
|
1966
2233
|
};
|
|
@@ -1984,12 +2251,6 @@ function extractSymbol(line, lang, _lineNum, _filePath) {
|
|
|
1984
2251
|
if (constVar && /^[A-Z_]+$/.test(constVar[1])) {
|
|
1985
2252
|
return { kind: "constant", name: constVar[1], exported: line.includes("export") };
|
|
1986
2253
|
}
|
|
1987
|
-
const method = line.match(/^(?:(?:public|private|protected|static|async|readonly)\s+)*(\w+)\s*\(/);
|
|
1988
|
-
if (method && !["if", "for", "while", "switch", "catch", "return", "constructor"].includes(method[1])) {
|
|
1989
|
-
if (method[1] === "constructor") {
|
|
1990
|
-
return { kind: "constructor", name: "constructor", exported: false };
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
2254
|
}
|
|
1994
2255
|
if (lang === "python" /* Python */) {
|
|
1995
2256
|
const func = line.match(/^(?:async\s+)?def\s+(\w+)/);
|
|
@@ -2101,16 +2362,73 @@ function extractSymbol(line, lang, _lineNum, _filePath) {
|
|
|
2101
2362
|
}
|
|
2102
2363
|
return null;
|
|
2103
2364
|
}
|
|
2365
|
+
function estimateEndLine(lines, startIdx, lang) {
|
|
2366
|
+
const MAX_SCAN = 200;
|
|
2367
|
+
const end = Math.min(startIdx + MAX_SCAN, lines.length);
|
|
2368
|
+
if (lang !== "python" /* Python */ && lang !== "ruby" /* Ruby */) {
|
|
2369
|
+
let depth = 0;
|
|
2370
|
+
let foundOpen = false;
|
|
2371
|
+
for (let i = startIdx; i < end; i++) {
|
|
2372
|
+
for (const ch of lines[i]) {
|
|
2373
|
+
if (ch === "{") {
|
|
2374
|
+
depth++;
|
|
2375
|
+
foundOpen = true;
|
|
2376
|
+
} else if (ch === "}") {
|
|
2377
|
+
depth--;
|
|
2378
|
+
if (foundOpen && depth === 0) return i + 1;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return void 0;
|
|
2383
|
+
}
|
|
2384
|
+
const startIndent = (lines[startIdx].match(/^(\s*)/) ?? ["", ""])[1].length;
|
|
2385
|
+
for (let i = startIdx + 1; i < end; i++) {
|
|
2386
|
+
const l = lines[i];
|
|
2387
|
+
if (l.trim() === "") continue;
|
|
2388
|
+
const indent = (l.match(/^(\s*)/) ?? ["", ""])[1].length;
|
|
2389
|
+
if (indent <= startIndent && l.trim() !== "") return i;
|
|
2390
|
+
}
|
|
2391
|
+
return void 0;
|
|
2392
|
+
}
|
|
2104
2393
|
function extractBlock(lines, startIdx, maxLines) {
|
|
2105
2394
|
const end = Math.min(startIdx + maxLines, lines.length);
|
|
2106
2395
|
return lines.slice(startIdx, end).join("\n");
|
|
2107
2396
|
}
|
|
2397
|
+
var CALL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2398
|
+
"if",
|
|
2399
|
+
"for",
|
|
2400
|
+
"while",
|
|
2401
|
+
"switch",
|
|
2402
|
+
"catch",
|
|
2403
|
+
"return",
|
|
2404
|
+
"throw",
|
|
2405
|
+
"typeof",
|
|
2406
|
+
"instanceof",
|
|
2407
|
+
"delete",
|
|
2408
|
+
"void",
|
|
2409
|
+
"new",
|
|
2410
|
+
"import",
|
|
2411
|
+
"export",
|
|
2412
|
+
"from",
|
|
2413
|
+
"const",
|
|
2414
|
+
"let",
|
|
2415
|
+
"var",
|
|
2416
|
+
"function",
|
|
2417
|
+
"class",
|
|
2418
|
+
"interface",
|
|
2419
|
+
"type",
|
|
2420
|
+
"enum",
|
|
2421
|
+
"extends",
|
|
2422
|
+
"implements"
|
|
2423
|
+
]);
|
|
2108
2424
|
var resolvePhase = {
|
|
2109
2425
|
name: "resolve",
|
|
2110
2426
|
dependencies: ["parse"],
|
|
2111
2427
|
async execute(context) {
|
|
2112
2428
|
const start = Date.now();
|
|
2113
2429
|
const { graph, workspaceRoot, filePaths } = context;
|
|
2430
|
+
const fileCache = context.fileCache ?? /* @__PURE__ */ new Map();
|
|
2431
|
+
const fileFunctionIndex = context.fileFunctionIndex ?? /* @__PURE__ */ new Map();
|
|
2114
2432
|
let importEdges = 0;
|
|
2115
2433
|
let callEdges = 0;
|
|
2116
2434
|
let heritageEdges = 0;
|
|
@@ -2126,7 +2444,18 @@ var resolvePhase = {
|
|
|
2126
2444
|
const symbolIndex = /* @__PURE__ */ new Map();
|
|
2127
2445
|
const fileSymbolIndex = /* @__PURE__ */ new Map();
|
|
2128
2446
|
for (const node of graph.allNodes()) {
|
|
2129
|
-
if ([
|
|
2447
|
+
if ([
|
|
2448
|
+
"function",
|
|
2449
|
+
"class",
|
|
2450
|
+
"interface",
|
|
2451
|
+
"method",
|
|
2452
|
+
"enum",
|
|
2453
|
+
"type_alias",
|
|
2454
|
+
"variable",
|
|
2455
|
+
"constant",
|
|
2456
|
+
"struct",
|
|
2457
|
+
"trait"
|
|
2458
|
+
].includes(node.kind)) {
|
|
2130
2459
|
symbolIndex.set(node.name, node.id);
|
|
2131
2460
|
let fileMap = fileSymbolIndex.get(node.filePath);
|
|
2132
2461
|
if (!fileMap) {
|
|
@@ -2136,17 +2465,14 @@ var resolvePhase = {
|
|
|
2136
2465
|
fileMap.set(node.name, node.id);
|
|
2137
2466
|
}
|
|
2138
2467
|
}
|
|
2468
|
+
let fileDone = 0;
|
|
2139
2469
|
for (const filePath of filePaths) {
|
|
2140
2470
|
const lang = detectLanguage(filePath);
|
|
2141
2471
|
if (!lang) continue;
|
|
2142
2472
|
const relativePath = path6.relative(workspaceRoot, filePath);
|
|
2143
2473
|
const fileNodeId = generateNodeId("file", relativePath, relativePath);
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
source = fs8.readFileSync(filePath, "utf-8");
|
|
2147
|
-
} catch {
|
|
2148
|
-
continue;
|
|
2149
|
-
}
|
|
2474
|
+
const source = fileCache.get(filePath);
|
|
2475
|
+
if (!source) continue;
|
|
2150
2476
|
const lines = source.split("\n");
|
|
2151
2477
|
const imports = extractImports(lines, lang === "python");
|
|
2152
2478
|
const calls = extractCalls(lines);
|
|
@@ -2173,11 +2499,13 @@ var resolvePhase = {
|
|
|
2173
2499
|
break;
|
|
2174
2500
|
}
|
|
2175
2501
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2502
|
+
if (!resolvedRelPath) {
|
|
2503
|
+
const asPath = cleaned.replace(/\./g, "/");
|
|
2504
|
+
for (const ext of ["", ".ts", ".js", ".py", ".java", ".go", "/index.ts", "/__init__.py"]) {
|
|
2505
|
+
if (fileIndex.has(asPath + ext)) {
|
|
2506
|
+
resolvedRelPath = asPath + ext;
|
|
2507
|
+
break;
|
|
2508
|
+
}
|
|
2181
2509
|
}
|
|
2182
2510
|
}
|
|
2183
2511
|
}
|
|
@@ -2200,6 +2528,7 @@ var resolvePhase = {
|
|
|
2200
2528
|
}
|
|
2201
2529
|
}
|
|
2202
2530
|
const localSymbols = fileSymbolIndex.get(relativePath);
|
|
2531
|
+
const funcList = fileFunctionIndex.get(relativePath);
|
|
2203
2532
|
for (const call of calls) {
|
|
2204
2533
|
let targetId = localSymbols?.get(call.name);
|
|
2205
2534
|
let confidence = 0.95;
|
|
@@ -2208,7 +2537,7 @@ var resolvePhase = {
|
|
|
2208
2537
|
confidence = 0.5;
|
|
2209
2538
|
}
|
|
2210
2539
|
if (targetId) {
|
|
2211
|
-
const callerNodeId =
|
|
2540
|
+
const callerNodeId = funcList ? findEnclosingFunctionFast(funcList, call.line) : null;
|
|
2212
2541
|
const sourceId = callerNodeId ?? fileNodeId;
|
|
2213
2542
|
if (sourceId !== targetId) {
|
|
2214
2543
|
const edgeId = generateEdgeId(sourceId, targetId, "calls");
|
|
@@ -2264,6 +2593,8 @@ var resolvePhase = {
|
|
|
2264
2593
|
}
|
|
2265
2594
|
}
|
|
2266
2595
|
}
|
|
2596
|
+
fileDone++;
|
|
2597
|
+
context.onPhaseProgress?.("resolve", fileDone, filePaths.length);
|
|
2267
2598
|
}
|
|
2268
2599
|
return {
|
|
2269
2600
|
status: "completed",
|
|
@@ -2281,18 +2612,13 @@ function extractImports(lines, isPython) {
|
|
|
2281
2612
|
const names = [];
|
|
2282
2613
|
const namedMatch = line.match(/\{([^}]+)\}/);
|
|
2283
2614
|
if (namedMatch) {
|
|
2284
|
-
names.push(
|
|
2615
|
+
names.push(
|
|
2616
|
+
...namedMatch[1].split(",").map((n) => n.trim().split(/\s+as\s+/).pop().trim()).filter(Boolean)
|
|
2617
|
+
);
|
|
2285
2618
|
}
|
|
2286
2619
|
const defaultMatch = line.match(/import\s+(\w+)/);
|
|
2287
|
-
if (defaultMatch && defaultMatch[1] !== "type")
|
|
2288
|
-
|
|
2289
|
-
}
|
|
2290
|
-
imports.push({
|
|
2291
|
-
rawPath: tsImport[1],
|
|
2292
|
-
localNames: names,
|
|
2293
|
-
isDefault: !namedMatch,
|
|
2294
|
-
line: i + 1
|
|
2295
|
-
});
|
|
2620
|
+
if (defaultMatch && defaultMatch[1] !== "type") names.push(defaultMatch[1]);
|
|
2621
|
+
imports.push({ rawPath: tsImport[1], localNames: names, isDefault: !namedMatch, line: i + 1 });
|
|
2296
2622
|
continue;
|
|
2297
2623
|
}
|
|
2298
2624
|
if (isPython) {
|
|
@@ -2316,64 +2642,34 @@ function extractImports(lines, isPython) {
|
|
|
2316
2642
|
const javaImport = line.match(/^import\s+(?:static\s+)?([\w.]+)/);
|
|
2317
2643
|
if (javaImport && !line.includes("from")) {
|
|
2318
2644
|
const parts = javaImport[1].split(".");
|
|
2319
|
-
imports.push({
|
|
2320
|
-
rawPath: javaImport[1],
|
|
2321
|
-
localNames: [parts[parts.length - 1]],
|
|
2322
|
-
isDefault: false,
|
|
2323
|
-
line: i + 1
|
|
2324
|
-
});
|
|
2645
|
+
imports.push({ rawPath: javaImport[1], localNames: [parts[parts.length - 1]], isDefault: false, line: i + 1 });
|
|
2325
2646
|
continue;
|
|
2326
2647
|
}
|
|
2327
2648
|
const goImport = line.match(/^\s*"([^"]+)"/);
|
|
2328
2649
|
if (goImport && (i > 0 && lines[i - 1]?.includes("import") || line.match(/^import\s+"/))) {
|
|
2329
2650
|
const parts = goImport[1].split("/");
|
|
2330
|
-
imports.push({
|
|
2331
|
-
rawPath: goImport[1],
|
|
2332
|
-
localNames: [parts[parts.length - 1]],
|
|
2333
|
-
isDefault: false,
|
|
2334
|
-
line: i + 1
|
|
2335
|
-
});
|
|
2651
|
+
imports.push({ rawPath: goImport[1], localNames: [parts[parts.length - 1]], isDefault: false, line: i + 1 });
|
|
2336
2652
|
continue;
|
|
2337
2653
|
}
|
|
2338
2654
|
const includeMatch = line.match(/#include\s+[<"]([^>"]+)[>"]/);
|
|
2339
2655
|
if (includeMatch) {
|
|
2340
|
-
imports.push({
|
|
2341
|
-
rawPath: includeMatch[1],
|
|
2342
|
-
localNames: [],
|
|
2343
|
-
isDefault: false,
|
|
2344
|
-
line: i + 1
|
|
2345
|
-
});
|
|
2656
|
+
imports.push({ rawPath: includeMatch[1], localNames: [], isDefault: false, line: i + 1 });
|
|
2346
2657
|
continue;
|
|
2347
2658
|
}
|
|
2348
2659
|
const rustUse = line.match(/^use\s+([\w:]+)/);
|
|
2349
2660
|
if (rustUse) {
|
|
2350
2661
|
const parts = rustUse[1].split("::");
|
|
2351
|
-
imports.push({
|
|
2352
|
-
rawPath: rustUse[1],
|
|
2353
|
-
localNames: [parts[parts.length - 1]],
|
|
2354
|
-
isDefault: false,
|
|
2355
|
-
line: i + 1
|
|
2356
|
-
});
|
|
2662
|
+
imports.push({ rawPath: rustUse[1], localNames: [parts[parts.length - 1]], isDefault: false, line: i + 1 });
|
|
2357
2663
|
continue;
|
|
2358
2664
|
}
|
|
2359
2665
|
const usingMatch = line.match(/^using\s+([\w.]+)/);
|
|
2360
2666
|
if (usingMatch) {
|
|
2361
2667
|
const parts = usingMatch[1].split(".");
|
|
2362
|
-
imports.push({
|
|
2363
|
-
rawPath: usingMatch[1],
|
|
2364
|
-
localNames: [parts[parts.length - 1]],
|
|
2365
|
-
isDefault: false,
|
|
2366
|
-
line: i + 1
|
|
2367
|
-
});
|
|
2668
|
+
imports.push({ rawPath: usingMatch[1], localNames: [parts[parts.length - 1]], isDefault: false, line: i + 1 });
|
|
2368
2669
|
}
|
|
2369
2670
|
const requireMatch = line.match(/require\s+['"]([^'"]+)['"]/);
|
|
2370
2671
|
if (requireMatch) {
|
|
2371
|
-
imports.push({
|
|
2372
|
-
rawPath: requireMatch[1],
|
|
2373
|
-
localNames: [],
|
|
2374
|
-
isDefault: false,
|
|
2375
|
-
line: i + 1
|
|
2376
|
-
});
|
|
2672
|
+
imports.push({ rawPath: requireMatch[1], localNames: [], isDefault: false, line: i + 1 });
|
|
2377
2673
|
}
|
|
2378
2674
|
}
|
|
2379
2675
|
return imports;
|
|
@@ -2394,19 +2690,14 @@ function extractCalls(lines) {
|
|
|
2394
2690
|
callRegex.lastIndex = 0;
|
|
2395
2691
|
while ((match = callRegex.exec(line)) !== null) {
|
|
2396
2692
|
const name = match[1];
|
|
2397
|
-
if (
|
|
2693
|
+
if (CALL_KEYWORDS.has(name)) continue;
|
|
2398
2694
|
const isNew = line.substring(Math.max(0, match.index - 4), match.index).includes("new");
|
|
2399
2695
|
calls.push({ name, isNew, line: i + 1 });
|
|
2400
2696
|
}
|
|
2401
2697
|
const memberCallRegex = /(\w+)\.(\w+)\s*\(/g;
|
|
2402
2698
|
memberCallRegex.lastIndex = 0;
|
|
2403
2699
|
while ((match = memberCallRegex.exec(line)) !== null) {
|
|
2404
|
-
calls.push({
|
|
2405
|
-
name: match[2],
|
|
2406
|
-
receiverText: match[1],
|
|
2407
|
-
isNew: false,
|
|
2408
|
-
line: i + 1
|
|
2409
|
-
});
|
|
2700
|
+
calls.push({ name: match[2], receiverText: match[1], isNew: false, line: i + 1 });
|
|
2410
2701
|
}
|
|
2411
2702
|
}
|
|
2412
2703
|
return calls;
|
|
@@ -2429,19 +2720,23 @@ function extractHeritage(lines) {
|
|
|
2429
2720
|
}
|
|
2430
2721
|
return heritages;
|
|
2431
2722
|
}
|
|
2432
|
-
function
|
|
2723
|
+
function findEnclosingFunctionFast(funcs, line) {
|
|
2724
|
+
let lo = 0;
|
|
2725
|
+
let hi = funcs.length - 1;
|
|
2433
2726
|
let best = null;
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
if (
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
best = { id: node.id, startLine: node.startLine };
|
|
2727
|
+
while (lo <= hi) {
|
|
2728
|
+
const mid = lo + hi >> 1;
|
|
2729
|
+
const fn = funcs[mid];
|
|
2730
|
+
if (fn.startLine <= line) {
|
|
2731
|
+
if (fn.endLine === void 0 || line <= fn.endLine) {
|
|
2732
|
+
best = fn.id;
|
|
2441
2733
|
}
|
|
2734
|
+
lo = mid + 1;
|
|
2735
|
+
} else {
|
|
2736
|
+
hi = mid - 1;
|
|
2442
2737
|
}
|
|
2443
2738
|
}
|
|
2444
|
-
return best
|
|
2739
|
+
return best;
|
|
2445
2740
|
}
|
|
2446
2741
|
|
|
2447
2742
|
// src/pipeline/phases/cluster-phase.ts
|
|
@@ -2464,7 +2759,11 @@ var clusterPhase = {
|
|
|
2464
2759
|
group.push({ id: node.id, name: node.name });
|
|
2465
2760
|
}
|
|
2466
2761
|
let clusterCount = 0;
|
|
2467
|
-
|
|
2762
|
+
const dirEntries = [...nodesByDir.entries()];
|
|
2763
|
+
let clusterDone = 0;
|
|
2764
|
+
for (const [dir, members] of dirEntries) {
|
|
2765
|
+
clusterDone++;
|
|
2766
|
+
context.onPhaseProgress?.("cluster", clusterDone, dirEntries.length);
|
|
2468
2767
|
if (members.length < 2) continue;
|
|
2469
2768
|
const clusterId = generateNodeId("cluster", dir, `cluster-${clusterCount}`);
|
|
2470
2769
|
const label = dir.split("/").filter(Boolean).pop() ?? `cluster-${clusterCount}`;
|
|
@@ -2525,27 +2824,30 @@ var flowPhase = {
|
|
|
2525
2824
|
const maxDepth = 10;
|
|
2526
2825
|
const maxBranching = 4;
|
|
2527
2826
|
let flowCount = 0;
|
|
2528
|
-
|
|
2827
|
+
const epSlice = entryPoints.slice(0, 20);
|
|
2828
|
+
for (let epIdx = 0; epIdx < epSlice.length; epIdx++) {
|
|
2829
|
+
const ep = epSlice[epIdx];
|
|
2830
|
+
context.onPhaseProgress?.("flow", epIdx + 1, epSlice.length);
|
|
2529
2831
|
if (flowCount >= maxFlows) break;
|
|
2530
2832
|
const queue = [{ nodeId: ep.id, path: [ep.id] }];
|
|
2531
2833
|
const visited = /* @__PURE__ */ new Set();
|
|
2532
2834
|
while (queue.length > 0 && flowCount < maxFlows) {
|
|
2533
|
-
const { nodeId, path:
|
|
2534
|
-
if (
|
|
2835
|
+
const { nodeId, path: path19 } = queue.shift();
|
|
2836
|
+
if (path19.length > maxDepth) continue;
|
|
2535
2837
|
const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
|
|
2536
|
-
if (callEdges.length === 0 &&
|
|
2838
|
+
if (callEdges.length === 0 && path19.length >= 3) {
|
|
2537
2839
|
const flowId = generateNodeId("flow", ep.filePath, `flow-${flowCount}`);
|
|
2538
2840
|
graph.addNode({
|
|
2539
2841
|
id: flowId,
|
|
2540
2842
|
kind: "flow",
|
|
2541
2843
|
name: `${ep.name} flow ${flowCount}`,
|
|
2542
2844
|
filePath: ep.filePath,
|
|
2543
|
-
metadata: { steps:
|
|
2845
|
+
metadata: { steps: path19, entryPoint: ep.name }
|
|
2544
2846
|
});
|
|
2545
|
-
for (let i = 0; i <
|
|
2847
|
+
for (let i = 0; i < path19.length; i++) {
|
|
2546
2848
|
graph.addEdge({
|
|
2547
|
-
id: generateEdgeId(
|
|
2548
|
-
source:
|
|
2849
|
+
id: generateEdgeId(path19[i], flowId, `step_of_${i}`),
|
|
2850
|
+
source: path19[i],
|
|
2549
2851
|
target: flowId,
|
|
2550
2852
|
kind: "step_of",
|
|
2551
2853
|
weight: 1,
|
|
@@ -2558,7 +2860,7 @@ var flowPhase = {
|
|
|
2558
2860
|
for (const edge of callEdges) {
|
|
2559
2861
|
if (visited.has(edge.target)) continue;
|
|
2560
2862
|
visited.add(edge.target);
|
|
2561
|
-
queue.push({ nodeId: edge.target, path: [...
|
|
2863
|
+
queue.push({ nodeId: edge.target, path: [...path19, edge.target] });
|
|
2562
2864
|
}
|
|
2563
2865
|
}
|
|
2564
2866
|
}
|
|
@@ -2687,17 +2989,17 @@ function traceFlow(entryId, graph, maxDepth = 10, maxBranching = 4) {
|
|
|
2687
2989
|
const queue = [{ nodeId: entryId, path: [entryId] }];
|
|
2688
2990
|
const visited = /* @__PURE__ */ new Set();
|
|
2689
2991
|
while (queue.length > 0 && flows.length < maxFlows) {
|
|
2690
|
-
const { nodeId, path:
|
|
2691
|
-
if (
|
|
2992
|
+
const { nodeId, path: path19 } = queue.shift();
|
|
2993
|
+
if (path19.length > maxDepth) continue;
|
|
2692
2994
|
const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
|
|
2693
|
-
if (callEdges.length === 0 &&
|
|
2694
|
-
flows.push({ entryPointId: entryId, steps: [...
|
|
2995
|
+
if (callEdges.length === 0 && path19.length >= 3) {
|
|
2996
|
+
flows.push({ entryPointId: entryId, steps: [...path19] });
|
|
2695
2997
|
continue;
|
|
2696
2998
|
}
|
|
2697
2999
|
for (const edge of callEdges) {
|
|
2698
3000
|
if (visited.has(edge.target)) continue;
|
|
2699
3001
|
visited.add(edge.target);
|
|
2700
|
-
queue.push({ nodeId: edge.target, path: [...
|
|
3002
|
+
queue.push({ nodeId: edge.target, path: [...path19, edge.target] });
|
|
2701
3003
|
}
|
|
2702
3004
|
}
|
|
2703
3005
|
}
|
|
@@ -2858,226 +3160,65 @@ var VectorIndex = class {
|
|
|
2858
3160
|
function esc(s) {
|
|
2859
3161
|
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
|
|
2860
3162
|
}
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
name: "repos",
|
|
2870
|
-
description: "List indexed repositories",
|
|
2871
|
-
inputSchema: { type: "object", properties: {} }
|
|
2872
|
-
},
|
|
2873
|
-
{
|
|
2874
|
-
name: "search",
|
|
2875
|
-
description: "Hybrid search across the codebase knowledge graph",
|
|
2876
|
-
inputSchema: {
|
|
2877
|
-
type: "object",
|
|
2878
|
-
properties: {
|
|
2879
|
-
query: { type: "string", description: "Search query" },
|
|
2880
|
-
limit: { type: "number", description: "Max results (default 20)" }
|
|
2881
|
-
},
|
|
2882
|
-
required: ["query"]
|
|
2883
|
-
}
|
|
2884
|
-
},
|
|
2885
|
-
{
|
|
2886
|
-
name: "inspect",
|
|
2887
|
-
description: "360\xB0 view of a symbol: definition, callers, callees, heritage, references",
|
|
2888
|
-
inputSchema: {
|
|
2889
|
-
type: "object",
|
|
2890
|
-
properties: {
|
|
2891
|
-
symbol_name: { type: "string", description: "Symbol name to inspect" }
|
|
2892
|
-
},
|
|
2893
|
-
required: ["symbol_name"]
|
|
2894
|
-
}
|
|
2895
|
-
},
|
|
2896
|
-
{
|
|
2897
|
-
name: "blast_radius",
|
|
2898
|
-
description: "Impact analysis: what depends on / is affected by this symbol",
|
|
2899
|
-
inputSchema: {
|
|
2900
|
-
type: "object",
|
|
2901
|
-
properties: {
|
|
2902
|
-
target: { type: "string", description: "Target symbol name" },
|
|
2903
|
-
direction: { type: "string", enum: ["callers", "callees", "both"], description: "Direction to trace" },
|
|
2904
|
-
max_hops: { type: "number", description: "Max hops (default 5)" }
|
|
2905
|
-
},
|
|
2906
|
-
required: ["target"]
|
|
2907
|
-
}
|
|
2908
|
-
},
|
|
2909
|
-
{
|
|
2910
|
-
name: "routes",
|
|
2911
|
-
description: "List route handler mappings in the codebase",
|
|
2912
|
-
inputSchema: { type: "object", properties: {} }
|
|
2913
|
-
},
|
|
2914
|
-
{
|
|
2915
|
-
name: "raw_query",
|
|
2916
|
-
description: "Execute a graph query (simplified Cypher-like)",
|
|
2917
|
-
inputSchema: {
|
|
2918
|
-
type: "object",
|
|
2919
|
-
properties: {
|
|
2920
|
-
cypher: { type: "string", description: "Query string (name='X' or :kind patterns)" }
|
|
2921
|
-
},
|
|
2922
|
-
required: ["cypher"]
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
]
|
|
2926
|
-
}));
|
|
2927
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2928
|
-
const { name, arguments: args } = request.params;
|
|
2929
|
-
const a = args ?? {};
|
|
2930
|
-
switch (name) {
|
|
2931
|
-
case "repos": {
|
|
2932
|
-
return { content: [{ type: "text", text: JSON.stringify([{ name: repoName, nodes: graph.size.nodes, edges: graph.size.edges }], null, 2) }] };
|
|
2933
|
-
}
|
|
2934
|
-
case "search": {
|
|
2935
|
-
const query = a.query;
|
|
2936
|
-
const limit = a.limit ?? 20;
|
|
2937
|
-
const results = textSearch(graph, query, limit);
|
|
2938
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
2939
|
-
}
|
|
2940
|
-
case "inspect": {
|
|
2941
|
-
const symbolName = a.symbol_name;
|
|
2942
|
-
const node = findNodeByName(graph, symbolName);
|
|
2943
|
-
if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found` }] };
|
|
2944
|
-
const incoming = [...graph.findEdgesTo(node.id)];
|
|
2945
|
-
const outgoing = [...graph.findEdgesFrom(node.id)];
|
|
2946
|
-
return {
|
|
2947
|
-
content: [{
|
|
2948
|
-
type: "text",
|
|
2949
|
-
text: JSON.stringify({
|
|
2950
|
-
node: { id: node.id, kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine, endLine: node.endLine, exported: node.exported },
|
|
2951
|
-
callers: incoming.filter((e) => e.kind === "calls").map((e) => ({ id: e.source, name: graph.getNode(e.source)?.name })),
|
|
2952
|
-
callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({ id: e.target, name: graph.getNode(e.target)?.name })),
|
|
2953
|
-
extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
|
|
2954
|
-
implements: outgoing.filter((e) => e.kind === "implements").map((e) => graph.getNode(e.target)?.name),
|
|
2955
|
-
members: outgoing.filter((e) => e.kind === "has_member").map((e) => ({ name: graph.getNode(e.target)?.name, kind: graph.getNode(e.target)?.kind })),
|
|
2956
|
-
cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0],
|
|
2957
|
-
content: node.content?.slice(0, 500)
|
|
2958
|
-
}, null, 2)
|
|
2959
|
-
}]
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
case "blast_radius": {
|
|
2963
|
-
const target = a.target;
|
|
2964
|
-
const direction = a.direction ?? "both";
|
|
2965
|
-
const maxHops = a.max_hops ?? 5;
|
|
2966
|
-
const node = findNodeByName(graph, target);
|
|
2967
|
-
if (!node) return { content: [{ type: "text", text: `Symbol "${target}" not found` }] };
|
|
2968
|
-
const affected = /* @__PURE__ */ new Set();
|
|
2969
|
-
const queue = [{ id: node.id, depth: 0 }];
|
|
2970
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2971
|
-
while (queue.length > 0) {
|
|
2972
|
-
const { id, depth } = queue.shift();
|
|
2973
|
-
if (visited.has(id) || depth > maxHops) continue;
|
|
2974
|
-
visited.add(id);
|
|
2975
|
-
affected.add(id);
|
|
2976
|
-
if (direction === "callers" || direction === "both") {
|
|
2977
|
-
for (const edge of graph.findEdgesTo(id)) {
|
|
2978
|
-
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.source, depth: depth + 1 });
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
if (direction === "callees" || direction === "both") {
|
|
2982
|
-
for (const edge of graph.findEdgesFrom(id)) {
|
|
2983
|
-
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.target, depth: depth + 1 });
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
const affectedDetails = [...affected].map((id) => {
|
|
2988
|
-
const n = graph.getNode(id);
|
|
2989
|
-
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
2990
|
-
});
|
|
2991
|
-
return { content: [{ type: "text", text: JSON.stringify({ target: node.name, affectedCount: affected.size, affected: affectedDetails }, null, 2) }] };
|
|
2992
|
-
}
|
|
2993
|
-
case "routes": {
|
|
2994
|
-
const routes = [];
|
|
2995
|
-
for (const node of graph.allNodes()) {
|
|
2996
|
-
if (node.kind === "route" || node.kind === "function" && /route|handler|controller/i.test(node.filePath)) {
|
|
2997
|
-
routes.push({ name: node.name, filePath: node.filePath });
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
return { content: [{ type: "text", text: JSON.stringify(routes, null, 2) }] };
|
|
3001
|
-
}
|
|
3002
|
-
case "raw_query": {
|
|
3003
|
-
const q = a.cypher;
|
|
3004
|
-
const nameMatch = q?.match(/name\s*=\s*['"]([^'"]+)['"]/i);
|
|
3005
|
-
if (nameMatch) {
|
|
3006
|
-
const results = [];
|
|
3007
|
-
for (const node of graph.allNodes()) {
|
|
3008
|
-
if (node.name === nameMatch[1]) results.push(node);
|
|
3009
|
-
}
|
|
3010
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
3011
|
-
}
|
|
3012
|
-
const kindMatch = q?.match(/:\s*(\w+)/);
|
|
3013
|
-
if (kindMatch) {
|
|
3014
|
-
const results = [];
|
|
3015
|
-
for (const node of graph.allNodes()) {
|
|
3016
|
-
if (node.kind === kindMatch[1]) results.push(node);
|
|
3017
|
-
if (results.length >= 50) break;
|
|
3018
|
-
}
|
|
3019
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
3020
|
-
}
|
|
3021
|
-
return { content: [{ type: "text", text: "Query not recognized" }] };
|
|
3022
|
-
}
|
|
3023
|
-
default:
|
|
3024
|
-
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
3025
|
-
}
|
|
3026
|
-
});
|
|
3027
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
3028
|
-
resources: [
|
|
3029
|
-
{ uri: `codeintel://repo/${repoName}/overview`, name: `${repoName} Overview`, mimeType: "application/json" },
|
|
3030
|
-
{ uri: `codeintel://repo/${repoName}/clusters`, name: `${repoName} Clusters`, mimeType: "application/json" },
|
|
3031
|
-
{ uri: `codeintel://repo/${repoName}/flows`, name: `${repoName} Flows`, mimeType: "application/json" }
|
|
3032
|
-
]
|
|
3033
|
-
}));
|
|
3034
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
3035
|
-
const { uri } = request.params;
|
|
3036
|
-
if (uri.endsWith("/overview")) {
|
|
3037
|
-
const kindCounts = {};
|
|
3038
|
-
for (const node of graph.allNodes()) {
|
|
3039
|
-
kindCounts[node.kind] = (kindCounts[node.kind] ?? 0) + 1;
|
|
3040
|
-
}
|
|
3041
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ repo: repoName, stats: graph.size, nodeCounts: kindCounts }) }] };
|
|
3042
|
-
}
|
|
3043
|
-
if (uri.endsWith("/clusters")) {
|
|
3044
|
-
const clusters = [];
|
|
3045
|
-
for (const node of graph.allNodes()) {
|
|
3046
|
-
if (node.kind === "cluster") clusters.push({ id: node.id, name: node.name, memberCount: node.metadata?.memberCount });
|
|
3047
|
-
}
|
|
3048
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(clusters) }] };
|
|
3049
|
-
}
|
|
3050
|
-
if (uri.endsWith("/flows")) {
|
|
3051
|
-
const flows = [];
|
|
3052
|
-
for (const node of graph.allNodes()) {
|
|
3053
|
-
if (node.kind === "flow") flows.push({ id: node.id, name: node.name, steps: node.metadata?.steps, entryPoint: node.metadata?.entryPoint });
|
|
3054
|
-
}
|
|
3055
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(flows) }] };
|
|
3056
|
-
}
|
|
3057
|
-
throw new Error(`Unknown resource: ${uri}`);
|
|
3058
|
-
});
|
|
3059
|
-
return server;
|
|
3060
|
-
}
|
|
3061
|
-
async function startMcpStdio(graph, repoName) {
|
|
3062
|
-
const server = createMcpServer(graph, repoName);
|
|
3063
|
-
const transport = new StdioServerTransport();
|
|
3064
|
-
await server.connect(transport);
|
|
3065
|
-
}
|
|
3066
|
-
function findNodeByName(graph, name) {
|
|
3067
|
-
for (const node of graph.allNodes()) {
|
|
3068
|
-
if (node.name === name) return node;
|
|
3163
|
+
var GLOBAL_DIR = path6.join(os3.homedir(), ".code-intel");
|
|
3164
|
+
var REPOS_FILE = path6.join(GLOBAL_DIR, "repos.json");
|
|
3165
|
+
function loadRegistry() {
|
|
3166
|
+
try {
|
|
3167
|
+
const data = fs6.readFileSync(REPOS_FILE, "utf-8");
|
|
3168
|
+
return JSON.parse(data);
|
|
3169
|
+
} catch {
|
|
3170
|
+
return [];
|
|
3069
3171
|
}
|
|
3070
|
-
return void 0;
|
|
3071
3172
|
}
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3173
|
+
function saveRegistry(entries) {
|
|
3174
|
+
fs6.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
3175
|
+
fs6.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
|
|
3176
|
+
}
|
|
3177
|
+
function upsertRepo(entry) {
|
|
3178
|
+
const entries = loadRegistry();
|
|
3179
|
+
const idx = entries.findIndex((e) => e.path === entry.path);
|
|
3180
|
+
if (idx >= 0) {
|
|
3181
|
+
entries[idx] = entry;
|
|
3182
|
+
} else {
|
|
3183
|
+
entries.push(entry);
|
|
3184
|
+
}
|
|
3185
|
+
saveRegistry(entries);
|
|
3186
|
+
}
|
|
3187
|
+
function removeRepo(repoPath) {
|
|
3188
|
+
const entries = loadRegistry().filter((e) => e.path !== repoPath);
|
|
3189
|
+
saveRegistry(entries);
|
|
3190
|
+
}
|
|
3191
|
+
function saveMetadata(repoDir, metadata) {
|
|
3192
|
+
const metaDir = path6.join(repoDir, ".code-intel");
|
|
3193
|
+
fs6.mkdirSync(metaDir, { recursive: true });
|
|
3194
|
+
fs6.writeFileSync(path6.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
|
|
3195
|
+
}
|
|
3196
|
+
function loadMetadata(repoDir) {
|
|
3197
|
+
try {
|
|
3198
|
+
const data = fs6.readFileSync(path6.join(repoDir, ".code-intel", "meta.json"), "utf-8");
|
|
3199
|
+
return JSON.parse(data);
|
|
3200
|
+
} catch {
|
|
3201
|
+
return null;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
function getDbPath(repoDir) {
|
|
3205
|
+
return path6.join(repoDir, ".code-intel", "graph.db");
|
|
3206
|
+
}
|
|
3207
|
+
function getVectorDbPath(repoDir) {
|
|
3208
|
+
return path6.join(repoDir, ".code-intel", "vector.db");
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
// src/mcp-server/server.ts
|
|
3212
|
+
init_group_registry();
|
|
3213
|
+
var DbManager = class {
|
|
3214
|
+
db = null;
|
|
3215
|
+
conn = null;
|
|
3075
3216
|
dbPath;
|
|
3076
3217
|
constructor(dbPath) {
|
|
3077
3218
|
this.dbPath = dbPath;
|
|
3078
3219
|
}
|
|
3079
3220
|
async init() {
|
|
3080
|
-
|
|
3221
|
+
fs6.mkdirSync(path6.dirname(this.dbPath), { recursive: true });
|
|
3081
3222
|
this.db = new Database(this.dbPath);
|
|
3082
3223
|
await this.db.init();
|
|
3083
3224
|
this.conn = new Connection(this.db);
|
|
@@ -3168,112 +3309,6 @@ function getCreateEdgeTableDDL() {
|
|
|
3168
3309
|
return ddls;
|
|
3169
3310
|
}
|
|
3170
3311
|
|
|
3171
|
-
// src/storage/graph-loader.ts
|
|
3172
|
-
async function loadGraphToDB(graph, dbManager) {
|
|
3173
|
-
for (const table of ALL_NODE_TABLES) {
|
|
3174
|
-
await dbManager.execute(getCreateNodeTableDDL(table));
|
|
3175
|
-
}
|
|
3176
|
-
const edgeDDLs = getCreateEdgeTableDDL();
|
|
3177
|
-
for (const ddl of edgeDDLs) {
|
|
3178
|
-
try {
|
|
3179
|
-
await dbManager.execute(ddl);
|
|
3180
|
-
} catch {
|
|
3181
|
-
}
|
|
3182
|
-
}
|
|
3183
|
-
let nodeCount = 0;
|
|
3184
|
-
for (const node of graph.allNodes()) {
|
|
3185
|
-
const table = NODE_TABLE_MAP[node.kind];
|
|
3186
|
-
const props = buildNodeProps(node);
|
|
3187
|
-
try {
|
|
3188
|
-
await dbManager.execute(`CREATE (:${table} ${props})`);
|
|
3189
|
-
nodeCount++;
|
|
3190
|
-
} catch {
|
|
3191
|
-
}
|
|
3192
|
-
}
|
|
3193
|
-
let edgeCount = 0;
|
|
3194
|
-
for (const edge of graph.allEdges()) {
|
|
3195
|
-
const sourceNode = graph.getNode(edge.source);
|
|
3196
|
-
const targetNode = graph.getNode(edge.target);
|
|
3197
|
-
if (!sourceNode || !targetNode) continue;
|
|
3198
|
-
const fromTable = NODE_TABLE_MAP[sourceNode.kind];
|
|
3199
|
-
const toTable = NODE_TABLE_MAP[targetNode.kind];
|
|
3200
|
-
try {
|
|
3201
|
-
await dbManager.execute(
|
|
3202
|
-
`MATCH (a:${fromTable} {id: '${escCypher(edge.source)}'}), (b:${toTable} {id: '${escCypher(edge.target)}'}) CREATE (a)-[:code_edges {kind: '${edge.kind}', weight: ${edge.weight ?? 1}, label: '${escCypher(edge.label ?? "")}'}]->(b)`
|
|
3203
|
-
);
|
|
3204
|
-
edgeCount++;
|
|
3205
|
-
} catch {
|
|
3206
|
-
}
|
|
3207
|
-
}
|
|
3208
|
-
return { nodeCount, edgeCount };
|
|
3209
|
-
}
|
|
3210
|
-
function buildNodeProps(node) {
|
|
3211
|
-
const parts = [
|
|
3212
|
-
`id: '${escCypher(node.id)}'`,
|
|
3213
|
-
`name: '${escCypher(node.name)}'`,
|
|
3214
|
-
`file_path: '${escCypher(node.filePath)}'`
|
|
3215
|
-
];
|
|
3216
|
-
if (node.startLine !== void 0) parts.push(`start_line: ${node.startLine}`);
|
|
3217
|
-
if (node.endLine !== void 0) parts.push(`end_line: ${node.endLine}`);
|
|
3218
|
-
if (node.exported !== void 0) parts.push(`exported: ${node.exported}`);
|
|
3219
|
-
if (node.content) parts.push(`content: '${escCypher(node.content.slice(0, 500))}'`);
|
|
3220
|
-
if (node.metadata) parts.push(`metadata: '${escCypher(JSON.stringify(node.metadata))}'`);
|
|
3221
|
-
return `{${parts.join(", ")}}`;
|
|
3222
|
-
}
|
|
3223
|
-
function escCypher(s) {
|
|
3224
|
-
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
|
|
3225
|
-
}
|
|
3226
|
-
var GLOBAL_DIR = path6.join(os2.homedir(), ".code-intel");
|
|
3227
|
-
var REPOS_FILE = path6.join(GLOBAL_DIR, "repos.json");
|
|
3228
|
-
function loadRegistry() {
|
|
3229
|
-
try {
|
|
3230
|
-
const data = fs8.readFileSync(REPOS_FILE, "utf-8");
|
|
3231
|
-
return JSON.parse(data);
|
|
3232
|
-
} catch {
|
|
3233
|
-
return [];
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
function saveRegistry(entries) {
|
|
3237
|
-
fs8.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
3238
|
-
fs8.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
|
|
3239
|
-
}
|
|
3240
|
-
function upsertRepo(entry) {
|
|
3241
|
-
const entries = loadRegistry();
|
|
3242
|
-
const idx = entries.findIndex((e) => e.path === entry.path);
|
|
3243
|
-
if (idx >= 0) {
|
|
3244
|
-
entries[idx] = entry;
|
|
3245
|
-
} else {
|
|
3246
|
-
entries.push(entry);
|
|
3247
|
-
}
|
|
3248
|
-
saveRegistry(entries);
|
|
3249
|
-
}
|
|
3250
|
-
function removeRepo(repoPath) {
|
|
3251
|
-
const entries = loadRegistry().filter((e) => e.path !== repoPath);
|
|
3252
|
-
saveRegistry(entries);
|
|
3253
|
-
}
|
|
3254
|
-
function saveMetadata(repoDir, metadata) {
|
|
3255
|
-
const metaDir = path6.join(repoDir, ".code-intel");
|
|
3256
|
-
fs8.mkdirSync(metaDir, { recursive: true });
|
|
3257
|
-
fs8.writeFileSync(path6.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
|
|
3258
|
-
}
|
|
3259
|
-
function loadMetadata(repoDir) {
|
|
3260
|
-
try {
|
|
3261
|
-
const data = fs8.readFileSync(path6.join(repoDir, ".code-intel", "meta.json"), "utf-8");
|
|
3262
|
-
return JSON.parse(data);
|
|
3263
|
-
} catch {
|
|
3264
|
-
return null;
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
function getDbPath(repoDir) {
|
|
3268
|
-
return path6.join(repoDir, ".code-intel", "graph.db");
|
|
3269
|
-
}
|
|
3270
|
-
function getVectorDbPath(repoDir) {
|
|
3271
|
-
return path6.join(repoDir, ".code-intel", "vector.db");
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
// src/http/app.ts
|
|
3275
|
-
init_group_registry();
|
|
3276
|
-
|
|
3277
3312
|
// src/multi-repo/graph-from-db.ts
|
|
3278
3313
|
var TABLE_TO_KIND = Object.fromEntries(
|
|
3279
3314
|
Object.entries(NODE_TABLE_MAP).map(([kind, table]) => [table, kind])
|
|
@@ -3463,12 +3498,12 @@ async function syncGroup(group) {
|
|
|
3463
3498
|
for (const member of group.members) {
|
|
3464
3499
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
3465
3500
|
if (!regEntry) {
|
|
3466
|
-
|
|
3501
|
+
logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
|
|
3467
3502
|
continue;
|
|
3468
3503
|
}
|
|
3469
3504
|
const dbPath = path6.join(regEntry.path, ".code-intel", "graph.db");
|
|
3470
|
-
if (!
|
|
3471
|
-
|
|
3505
|
+
if (!fs6.existsSync(dbPath)) {
|
|
3506
|
+
logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
|
|
3472
3507
|
continue;
|
|
3473
3508
|
}
|
|
3474
3509
|
const graph = createKnowledgeGraph();
|
|
@@ -3479,11 +3514,11 @@ async function syncGroup(group) {
|
|
|
3479
3514
|
db.close();
|
|
3480
3515
|
} catch (err) {
|
|
3481
3516
|
db.close();
|
|
3482
|
-
|
|
3517
|
+
logger_default.warn(` \u26A0 Could not load graph for "${member.registryName}": ${err instanceof Error ? err.message : err}`);
|
|
3483
3518
|
continue;
|
|
3484
3519
|
}
|
|
3485
3520
|
const contracts = extractContracts(graph, member.registryName, regEntry.path);
|
|
3486
|
-
|
|
3521
|
+
logger_default.info(` \u2713 ${member.registryName} (${member.groupPath}): ${contracts.length} contracts`);
|
|
3487
3522
|
allContracts.push(...contracts);
|
|
3488
3523
|
}
|
|
3489
3524
|
const links = matchContracts(allContracts);
|
|
@@ -3503,7 +3538,7 @@ async function queryGroup(group, query, limit = 20) {
|
|
|
3503
3538
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
3504
3539
|
if (!regEntry) continue;
|
|
3505
3540
|
const dbPath = path6.join(regEntry.path, ".code-intel", "graph.db");
|
|
3506
|
-
if (!
|
|
3541
|
+
if (!fs6.existsSync(dbPath)) continue;
|
|
3507
3542
|
const graph = createKnowledgeGraph();
|
|
3508
3543
|
const db = new DbManager(dbPath);
|
|
3509
3544
|
try {
|
|
@@ -3530,80 +3565,949 @@ async function queryGroup(group, query, limit = 20) {
|
|
|
3530
3565
|
const merged = reciprocalRankFusion(...allRankings).slice(0, limit);
|
|
3531
3566
|
return { perRepo, merged };
|
|
3532
3567
|
}
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3568
|
+
function createMcpServer(graph, repoName, workspaceRoot) {
|
|
3569
|
+
const server = new Server(
|
|
3570
|
+
{ name: "code-intel", version: "0.1.0" },
|
|
3571
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
3572
|
+
);
|
|
3573
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3574
|
+
tools: [
|
|
3575
|
+
// ── Core repo tools ──────────────────────────────────────────────────
|
|
3576
|
+
{
|
|
3577
|
+
name: "repos",
|
|
3578
|
+
description: "List all indexed repositories with node and edge counts",
|
|
3579
|
+
inputSchema: { type: "object", properties: {} }
|
|
3580
|
+
},
|
|
3581
|
+
{
|
|
3582
|
+
name: "overview",
|
|
3583
|
+
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.",
|
|
3584
|
+
inputSchema: { type: "object", properties: {} }
|
|
3585
|
+
},
|
|
3586
|
+
// ── Search & inspect ─────────────────────────────────────────────────
|
|
3587
|
+
{
|
|
3588
|
+
name: "search",
|
|
3589
|
+
description: "BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc.",
|
|
3590
|
+
inputSchema: {
|
|
3591
|
+
type: "object",
|
|
3592
|
+
properties: {
|
|
3593
|
+
query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
|
|
3594
|
+
limit: { type: "number", description: "Max results to return (default: 20)" }
|
|
3595
|
+
},
|
|
3596
|
+
required: ["query"]
|
|
3597
|
+
}
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
name: "inspect",
|
|
3601
|
+
description: "360\xB0 view of a symbol: definition location, callers, callees, heritage (extends/implements), members, cluster, and source preview (first 500 chars)",
|
|
3602
|
+
inputSchema: {
|
|
3603
|
+
type: "object",
|
|
3604
|
+
properties: {
|
|
3605
|
+
symbol_name: { type: "string", description: "Exact symbol name to inspect" }
|
|
3606
|
+
},
|
|
3607
|
+
required: ["symbol_name"]
|
|
3608
|
+
}
|
|
3609
|
+
},
|
|
3610
|
+
{
|
|
3611
|
+
name: "blast_radius",
|
|
3612
|
+
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).",
|
|
3613
|
+
inputSchema: {
|
|
3614
|
+
type: "object",
|
|
3615
|
+
properties: {
|
|
3616
|
+
target: { type: "string", description: "Target symbol name" },
|
|
3617
|
+
direction: {
|
|
3618
|
+
type: "string",
|
|
3619
|
+
enum: ["callers", "callees", "both"],
|
|
3620
|
+
description: "Which direction to trace \u2014 callers (who depends on it), callees (what it depends on), or both (default: both)"
|
|
3621
|
+
},
|
|
3622
|
+
max_hops: { type: "number", description: "Maximum traversal depth (default: 5)" }
|
|
3623
|
+
},
|
|
3624
|
+
required: ["target"]
|
|
3625
|
+
}
|
|
3626
|
+
},
|
|
3627
|
+
{
|
|
3628
|
+
name: "file_symbols",
|
|
3629
|
+
description: "List all symbols defined in a specific file \u2014 useful to understand what a file exports or contains without reading raw source.",
|
|
3630
|
+
inputSchema: {
|
|
3631
|
+
type: "object",
|
|
3632
|
+
properties: {
|
|
3633
|
+
file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' }
|
|
3634
|
+
},
|
|
3635
|
+
required: ["file_path"]
|
|
3636
|
+
}
|
|
3637
|
+
},
|
|
3638
|
+
{
|
|
3639
|
+
name: "find_path",
|
|
3640
|
+
description: "Find the shortest call/import path between two symbols. Useful for tracing how one module reaches another.",
|
|
3641
|
+
inputSchema: {
|
|
3642
|
+
type: "object",
|
|
3643
|
+
properties: {
|
|
3644
|
+
from: { type: "string", description: "Source symbol name" },
|
|
3645
|
+
to: { type: "string", description: "Target symbol name" },
|
|
3646
|
+
max_hops: { type: "number", description: "Maximum path length to search (default: 8)" }
|
|
3647
|
+
},
|
|
3648
|
+
required: ["from", "to"]
|
|
3649
|
+
}
|
|
3650
|
+
},
|
|
3651
|
+
{
|
|
3652
|
+
name: "list_exports",
|
|
3653
|
+
description: "List all exported symbols in the repository. Helps AI understand the public API surface of the codebase.",
|
|
3654
|
+
inputSchema: {
|
|
3655
|
+
type: "object",
|
|
3656
|
+
properties: {
|
|
3657
|
+
kind: {
|
|
3658
|
+
type: "string",
|
|
3659
|
+
description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
|
|
3660
|
+
},
|
|
3661
|
+
limit: { type: "number", description: "Max results (default: 100)" }
|
|
3561
3662
|
}
|
|
3562
|
-
}
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3663
|
+
}
|
|
3664
|
+
},
|
|
3665
|
+
// ── Routes, clusters, flows ──────────────────────────────────────────
|
|
3666
|
+
{
|
|
3667
|
+
name: "routes",
|
|
3668
|
+
description: "List all HTTP route handler mappings detected in the codebase (kind=route or route/handler/controller files)",
|
|
3669
|
+
inputSchema: { type: "object", properties: {} }
|
|
3670
|
+
},
|
|
3671
|
+
{
|
|
3672
|
+
name: "clusters",
|
|
3673
|
+
description: "List detected code clusters (directory-based communities) with member counts and top 10 symbols each. Useful for understanding code organisation.",
|
|
3674
|
+
inputSchema: {
|
|
3675
|
+
type: "object",
|
|
3676
|
+
properties: {
|
|
3677
|
+
limit: { type: "number", description: "Max clusters to return (default: 50)" }
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
},
|
|
3681
|
+
{
|
|
3682
|
+
name: "flows",
|
|
3683
|
+
description: "List all detected execution flows \u2014 entry points traced through the call graph. Each flow has a name, entry point, and ordered steps.",
|
|
3684
|
+
inputSchema: {
|
|
3685
|
+
type: "object",
|
|
3686
|
+
properties: {
|
|
3687
|
+
limit: { type: "number", description: "Max flows to return (default: 50)" }
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
},
|
|
3691
|
+
// ── Git change impact ─────────────────────────────────────────────────
|
|
3692
|
+
{
|
|
3693
|
+
name: "detect_changes",
|
|
3694
|
+
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.",
|
|
3695
|
+
inputSchema: {
|
|
3696
|
+
type: "object",
|
|
3697
|
+
properties: {
|
|
3698
|
+
base_ref: {
|
|
3699
|
+
type: "string",
|
|
3700
|
+
description: 'Git ref to diff against (default: HEAD). Examples: "HEAD~1", "main", a commit SHA.'
|
|
3701
|
+
},
|
|
3702
|
+
diff_text: {
|
|
3703
|
+
type: "string",
|
|
3704
|
+
description: "Raw unified diff text. If provided, base_ref is ignored and this diff is parsed directly."
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
},
|
|
3709
|
+
// ── Raw query ─────────────────────────────────────────────────────────
|
|
3710
|
+
{
|
|
3711
|
+
name: "raw_query",
|
|
3712
|
+
description: "Execute a simplified Cypher-like graph query. Supports: name='X' (exact name match) or :kind (list nodes of a kind, max 50)",
|
|
3713
|
+
inputSchema: {
|
|
3714
|
+
type: "object",
|
|
3715
|
+
properties: {
|
|
3716
|
+
cypher: { type: "string", description: "Query string \u2014 e.g. name='runPipeline' or :function" }
|
|
3717
|
+
},
|
|
3718
|
+
required: ["cypher"]
|
|
3719
|
+
}
|
|
3720
|
+
},
|
|
3721
|
+
// ── Group / multi-repo tools ──────────────────────────────────────────
|
|
3722
|
+
{
|
|
3723
|
+
name: "group_list",
|
|
3724
|
+
description: "List all configured repository groups, or show the full membership of one group. Repository groups track multiple repos as a logical system.",
|
|
3725
|
+
inputSchema: {
|
|
3726
|
+
type: "object",
|
|
3727
|
+
properties: {
|
|
3728
|
+
name: { type: "string", description: "Group name to inspect (optional \u2014 omit to list all groups)" }
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
},
|
|
3732
|
+
{
|
|
3733
|
+
name: "group_sync",
|
|
3734
|
+
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.",
|
|
3735
|
+
inputSchema: {
|
|
3736
|
+
type: "object",
|
|
3737
|
+
properties: {
|
|
3738
|
+
name: { type: "string", description: "Group name to sync" }
|
|
3739
|
+
},
|
|
3740
|
+
required: ["name"]
|
|
3741
|
+
}
|
|
3742
|
+
},
|
|
3743
|
+
{
|
|
3744
|
+
name: "group_contracts",
|
|
3745
|
+
description: "Inspect extracted contracts and confidence-ranked cross-repo links from the last group sync. Supports filtering by kind, repo, and minimum confidence.",
|
|
3746
|
+
inputSchema: {
|
|
3747
|
+
type: "object",
|
|
3748
|
+
properties: {
|
|
3749
|
+
name: { type: "string", description: "Group name" },
|
|
3750
|
+
kind: {
|
|
3751
|
+
type: "string",
|
|
3752
|
+
enum: ["export", "route", "schema", "event"],
|
|
3753
|
+
description: "Filter by contract kind (optional)"
|
|
3754
|
+
},
|
|
3755
|
+
repo: { type: "string", description: "Filter by registry name (optional)" },
|
|
3756
|
+
min_confidence: { type: "number", description: "Minimum link confidence 0\u20131 (default: 0)" }
|
|
3757
|
+
},
|
|
3758
|
+
required: ["name"]
|
|
3759
|
+
}
|
|
3760
|
+
},
|
|
3761
|
+
{
|
|
3762
|
+
name: "group_query",
|
|
3763
|
+
description: "BM25 search across all repos in a group, merged via Reciprocal Rank Fusion (RRF). Returns a unified ranked list plus per-repo breakdown.",
|
|
3764
|
+
inputSchema: {
|
|
3765
|
+
type: "object",
|
|
3766
|
+
properties: {
|
|
3767
|
+
name: { type: "string", description: "Group name" },
|
|
3768
|
+
query: { type: "string", description: "Search query" },
|
|
3769
|
+
limit: { type: "number", description: "Max results per repo (default: 10)" }
|
|
3770
|
+
},
|
|
3771
|
+
required: ["name", "query"]
|
|
3772
|
+
}
|
|
3773
|
+
},
|
|
3774
|
+
{
|
|
3775
|
+
name: "group_status",
|
|
3776
|
+
description: "Check index freshness and sync staleness for all repos in a group. Flags repos that have not been indexed or are stale (>24h).",
|
|
3777
|
+
inputSchema: {
|
|
3778
|
+
type: "object",
|
|
3779
|
+
properties: {
|
|
3780
|
+
name: { type: "string", description: "Group name" }
|
|
3781
|
+
},
|
|
3782
|
+
required: ["name"]
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
]
|
|
3786
|
+
}));
|
|
3787
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3788
|
+
const { name, arguments: args } = request.params;
|
|
3789
|
+
const a = args ?? {};
|
|
3790
|
+
switch (name) {
|
|
3791
|
+
// ── repos ──────────────────────────────────────────────────────────────
|
|
3792
|
+
case "repos": {
|
|
3793
|
+
const registry = loadRegistry();
|
|
3794
|
+
return {
|
|
3795
|
+
content: [{
|
|
3796
|
+
type: "text",
|
|
3797
|
+
text: JSON.stringify(
|
|
3798
|
+
registry.map((r) => ({ name: r.name, path: r.path, indexedAt: r.indexedAt, stats: r.stats })),
|
|
3799
|
+
null,
|
|
3800
|
+
2
|
|
3801
|
+
)
|
|
3802
|
+
}]
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
// ── overview ───────────────────────────────────────────────────────────
|
|
3806
|
+
case "overview": {
|
|
3807
|
+
const kindCounts = {};
|
|
3808
|
+
for (const node of graph.allNodes()) {
|
|
3809
|
+
kindCounts[node.kind] = (kindCounts[node.kind] ?? 0) + 1;
|
|
3810
|
+
}
|
|
3811
|
+
const edgeCounts = {};
|
|
3812
|
+
for (const edge of graph.allEdges()) {
|
|
3813
|
+
edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
|
|
3814
|
+
}
|
|
3815
|
+
return {
|
|
3816
|
+
content: [{
|
|
3817
|
+
type: "text",
|
|
3818
|
+
text: JSON.stringify({
|
|
3819
|
+
repo: repoName,
|
|
3820
|
+
stats: graph.size,
|
|
3821
|
+
nodeCounts: kindCounts,
|
|
3822
|
+
edgeCounts
|
|
3823
|
+
}, null, 2)
|
|
3824
|
+
}]
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
// ── search ─────────────────────────────────────────────────────────────
|
|
3828
|
+
case "search": {
|
|
3829
|
+
const query = a.query;
|
|
3830
|
+
const limit = a.limit ?? 20;
|
|
3831
|
+
const results = textSearch(graph, query, limit);
|
|
3832
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
3833
|
+
}
|
|
3834
|
+
// ── inspect ────────────────────────────────────────────────────────────
|
|
3835
|
+
case "inspect": {
|
|
3836
|
+
const symbolName = a.symbol_name;
|
|
3837
|
+
const node = findNodeByName(graph, symbolName);
|
|
3838
|
+
if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
|
|
3839
|
+
const incoming = [...graph.findEdgesTo(node.id)];
|
|
3840
|
+
const outgoing = [...graph.findEdgesFrom(node.id)];
|
|
3841
|
+
return {
|
|
3842
|
+
content: [{
|
|
3843
|
+
type: "text",
|
|
3844
|
+
text: JSON.stringify({
|
|
3845
|
+
node: {
|
|
3846
|
+
id: node.id,
|
|
3847
|
+
kind: node.kind,
|
|
3848
|
+
name: node.name,
|
|
3849
|
+
filePath: node.filePath,
|
|
3850
|
+
startLine: node.startLine,
|
|
3851
|
+
endLine: node.endLine,
|
|
3852
|
+
exported: node.exported
|
|
3853
|
+
},
|
|
3854
|
+
callers: incoming.filter((e) => e.kind === "calls").map((e) => ({
|
|
3855
|
+
id: e.source,
|
|
3856
|
+
name: graph.getNode(e.source)?.name,
|
|
3857
|
+
file: graph.getNode(e.source)?.filePath
|
|
3858
|
+
})),
|
|
3859
|
+
callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
3860
|
+
id: e.target,
|
|
3861
|
+
name: graph.getNode(e.target)?.name,
|
|
3862
|
+
file: graph.getNode(e.target)?.filePath
|
|
3863
|
+
})),
|
|
3864
|
+
imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
|
|
3865
|
+
importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
|
|
3866
|
+
extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
|
|
3867
|
+
implements: outgoing.filter((e) => e.kind === "implements").map((e) => graph.getNode(e.target)?.name),
|
|
3868
|
+
members: outgoing.filter((e) => e.kind === "has_member").map((e) => ({
|
|
3869
|
+
name: graph.getNode(e.target)?.name,
|
|
3870
|
+
kind: graph.getNode(e.target)?.kind
|
|
3871
|
+
})),
|
|
3872
|
+
cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0],
|
|
3873
|
+
content: node.content?.slice(0, 500)
|
|
3874
|
+
}, null, 2)
|
|
3875
|
+
}]
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
// ── blast_radius ───────────────────────────────────────────────────────
|
|
3879
|
+
case "blast_radius": {
|
|
3880
|
+
const target = a.target;
|
|
3881
|
+
const direction = a.direction ?? "both";
|
|
3882
|
+
const maxHops = a.max_hops ?? 5;
|
|
3883
|
+
const node = findNodeByName(graph, target);
|
|
3884
|
+
if (!node) return { content: [{ type: "text", text: `Symbol "${target}" not found.` }] };
|
|
3885
|
+
const affected = /* @__PURE__ */ new Set();
|
|
3886
|
+
const queue = [{ id: node.id, depth: 0 }];
|
|
3887
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3888
|
+
while (queue.length > 0) {
|
|
3889
|
+
const { id, depth } = queue.shift();
|
|
3890
|
+
if (visited.has(id) || depth > maxHops) continue;
|
|
3891
|
+
visited.add(id);
|
|
3892
|
+
affected.add(id);
|
|
3893
|
+
if (direction === "callers" || direction === "both") {
|
|
3894
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
3895
|
+
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.source, depth: depth + 1 });
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
if (direction === "callees" || direction === "both") {
|
|
3899
|
+
for (const edge of graph.findEdgesFrom(id)) {
|
|
3900
|
+
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.target, depth: depth + 1 });
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
const affectedDetails = [...affected].map((id) => {
|
|
3905
|
+
const n = graph.getNode(id);
|
|
3906
|
+
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
3907
|
+
});
|
|
3908
|
+
const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
|
|
3909
|
+
return {
|
|
3910
|
+
content: [{
|
|
3911
|
+
type: "text",
|
|
3912
|
+
text: JSON.stringify({
|
|
3913
|
+
target: node.name,
|
|
3914
|
+
affectedCount: affected.size,
|
|
3915
|
+
riskLevel: risk,
|
|
3916
|
+
affected: affectedDetails
|
|
3917
|
+
}, null, 2)
|
|
3918
|
+
}]
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
// ── file_symbols ───────────────────────────────────────────────────────
|
|
3922
|
+
case "file_symbols": {
|
|
3923
|
+
const filePath = a.file_path;
|
|
3924
|
+
const matches = [];
|
|
3925
|
+
for (const node of graph.allNodes()) {
|
|
3926
|
+
if (node.filePath && node.filePath.includes(filePath)) {
|
|
3927
|
+
matches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
if (matches.length === 0) {
|
|
3931
|
+
return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
|
|
3932
|
+
}
|
|
3933
|
+
matches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
|
|
3934
|
+
return { content: [{ type: "text", text: JSON.stringify(matches, null, 2) }] };
|
|
3935
|
+
}
|
|
3936
|
+
// ── find_path ──────────────────────────────────────────────────────────
|
|
3937
|
+
case "find_path": {
|
|
3938
|
+
const fromName = a.from;
|
|
3939
|
+
const toName = a.to;
|
|
3940
|
+
const maxHops = a.max_hops ?? 8;
|
|
3941
|
+
const fromNode = findNodeByName(graph, fromName);
|
|
3942
|
+
const toNode = findNodeByName(graph, toName);
|
|
3943
|
+
if (!fromNode) return { content: [{ type: "text", text: `Source symbol "${fromName}" not found.` }] };
|
|
3944
|
+
if (!toNode) return { content: [{ type: "text", text: `Target symbol "${toName}" not found.` }] };
|
|
3945
|
+
const queue = [{ id: fromNode.id, path: [fromNode.id] }];
|
|
3946
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3947
|
+
let foundPath = null;
|
|
3948
|
+
while (queue.length > 0) {
|
|
3949
|
+
const { id, path: currentPath } = queue.shift();
|
|
3950
|
+
if (visited.has(id)) continue;
|
|
3951
|
+
visited.add(id);
|
|
3952
|
+
if (id === toNode.id) {
|
|
3953
|
+
foundPath = currentPath;
|
|
3954
|
+
break;
|
|
3955
|
+
}
|
|
3956
|
+
if (currentPath.length > maxHops) continue;
|
|
3957
|
+
for (const edge of graph.findEdgesFrom(id)) {
|
|
3958
|
+
if ((edge.kind === "calls" || edge.kind === "imports") && !visited.has(edge.target)) {
|
|
3959
|
+
queue.push({ id: edge.target, path: [...currentPath, edge.target] });
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
if (!foundPath) {
|
|
3964
|
+
return { content: [{ type: "text", text: `No path found from "${fromName}" to "${toName}" within ${maxHops} hops.` }] };
|
|
3965
|
+
}
|
|
3966
|
+
const pathDetails = foundPath.map((id) => {
|
|
3967
|
+
const n = graph.getNode(id);
|
|
3968
|
+
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
3969
|
+
});
|
|
3970
|
+
return {
|
|
3971
|
+
content: [{
|
|
3972
|
+
type: "text",
|
|
3973
|
+
text: JSON.stringify({ from: fromName, to: toName, hops: foundPath.length - 1, path: pathDetails }, null, 2)
|
|
3974
|
+
}]
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3977
|
+
// ── list_exports ───────────────────────────────────────────────────────
|
|
3978
|
+
case "list_exports": {
|
|
3979
|
+
const kindFilter = a.kind;
|
|
3980
|
+
const limit = a.limit ?? 100;
|
|
3981
|
+
const exports$1 = [];
|
|
3982
|
+
for (const node of graph.allNodes()) {
|
|
3983
|
+
if (!node.exported) continue;
|
|
3984
|
+
if (kindFilter && node.kind !== kindFilter) continue;
|
|
3985
|
+
exports$1.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
|
|
3986
|
+
if (exports$1.length >= limit) break;
|
|
3987
|
+
}
|
|
3988
|
+
return { content: [{ type: "text", text: JSON.stringify({ total: exports$1.length, exports: exports$1 }, null, 2) }] };
|
|
3989
|
+
}
|
|
3990
|
+
// ── routes ─────────────────────────────────────────────────────────────
|
|
3991
|
+
case "routes": {
|
|
3992
|
+
const routes = [];
|
|
3993
|
+
for (const node of graph.allNodes()) {
|
|
3994
|
+
if (node.kind === "route" || node.kind === "function" && /route|handler|controller/i.test(node.filePath)) {
|
|
3995
|
+
routes.push({ name: node.name, filePath: node.filePath, startLine: node.startLine });
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
return { content: [{ type: "text", text: JSON.stringify(routes, null, 2) }] };
|
|
3999
|
+
}
|
|
4000
|
+
// ── clusters ───────────────────────────────────────────────────────────
|
|
4001
|
+
case "clusters": {
|
|
4002
|
+
const limit = a.limit ?? 50;
|
|
4003
|
+
const clusters = [];
|
|
4004
|
+
for (const node of graph.allNodes()) {
|
|
4005
|
+
if (node.kind === "cluster") {
|
|
4006
|
+
const members = [];
|
|
4007
|
+
for (const edge of graph.findEdgesTo(node.id)) {
|
|
4008
|
+
if (edge.kind === "belongs_to") {
|
|
4009
|
+
const member = graph.getNode(edge.source);
|
|
4010
|
+
if (member && member.kind !== "cluster") {
|
|
4011
|
+
members.push({ name: member.name, kind: member.kind });
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
clusters.push({
|
|
4016
|
+
id: node.id,
|
|
4017
|
+
name: node.name,
|
|
4018
|
+
memberCount: node.metadata?.memberCount ?? members.length,
|
|
4019
|
+
topSymbols: members.slice(0, 10)
|
|
4020
|
+
});
|
|
4021
|
+
if (clusters.length >= limit) break;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
return { content: [{ type: "text", text: JSON.stringify(clusters, null, 2) }] };
|
|
4025
|
+
}
|
|
4026
|
+
// ── flows ──────────────────────────────────────────────────────────────
|
|
4027
|
+
case "flows": {
|
|
4028
|
+
const limit = a.limit ?? 50;
|
|
4029
|
+
const flows = [];
|
|
4030
|
+
for (const node of graph.allNodes()) {
|
|
4031
|
+
if (node.kind === "flow") {
|
|
4032
|
+
const steps = node.metadata?.steps;
|
|
4033
|
+
flows.push({
|
|
4034
|
+
id: node.id,
|
|
4035
|
+
name: node.name,
|
|
4036
|
+
entryPoint: node.metadata?.entryPoint,
|
|
4037
|
+
steps: steps ?? [],
|
|
4038
|
+
stepCount: Array.isArray(steps) ? steps.length : 0
|
|
4039
|
+
});
|
|
4040
|
+
if (flows.length >= limit) break;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
return { content: [{ type: "text", text: JSON.stringify(flows, null, 2) }] };
|
|
4044
|
+
}
|
|
4045
|
+
// ── detect_changes ─────────────────────────────────────────────────────
|
|
4046
|
+
case "detect_changes": {
|
|
4047
|
+
const baseRef = a.base_ref ?? "HEAD";
|
|
4048
|
+
const diffTextInput = a.diff_text;
|
|
4049
|
+
let diffText;
|
|
4050
|
+
const repoRoot = workspaceRoot ?? process.cwd();
|
|
4051
|
+
if (diffTextInput) {
|
|
4052
|
+
diffText = diffTextInput;
|
|
4053
|
+
} else {
|
|
4054
|
+
try {
|
|
4055
|
+
diffText = execSync(`git diff ${baseRef}`, { cwd: repoRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
4056
|
+
if (!diffText.trim()) {
|
|
4057
|
+
diffText = execSync(`git diff HEAD`, { cwd: repoRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
4058
|
+
}
|
|
4059
|
+
} catch {
|
|
4060
|
+
return { content: [{ type: "text", text: `Could not run git diff in ${repoRoot}. Ensure the path is a Git repository.` }] };
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
if (!diffText.trim()) {
|
|
4064
|
+
return { content: [{ type: "text", text: "No changes detected in git diff." }] };
|
|
4065
|
+
}
|
|
4066
|
+
const changedFiles = parseDiff(diffText);
|
|
4067
|
+
const hitNodes = /* @__PURE__ */ new Set();
|
|
4068
|
+
for (const { filePath: changedFile, changedLines } of changedFiles) {
|
|
4069
|
+
for (const node of graph.allNodes()) {
|
|
4070
|
+
if (!node.filePath) continue;
|
|
4071
|
+
const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path6.sep, "");
|
|
4072
|
+
const normChanged = changedFile.replace(/^a\/|^b\//, "");
|
|
4073
|
+
if (!normNode.endsWith(normChanged) && !normChanged.endsWith(normNode)) continue;
|
|
4074
|
+
if (node.startLine !== void 0 && node.endLine !== void 0) {
|
|
4075
|
+
const overlaps = changedLines.some((l) => l >= node.startLine && l <= node.endLine);
|
|
4076
|
+
if (overlaps) hitNodes.add(node.id);
|
|
4077
|
+
} else if (node.startLine !== void 0) {
|
|
4078
|
+
const overlaps = changedLines.some((l) => Math.abs(l - node.startLine) <= 3);
|
|
4079
|
+
if (overlaps) hitNodes.add(node.id);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
const allAffected = /* @__PURE__ */ new Set();
|
|
4084
|
+
for (const startId of hitNodes) {
|
|
4085
|
+
const queue = [{ id: startId, depth: 0 }];
|
|
4086
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4087
|
+
while (queue.length > 0) {
|
|
4088
|
+
const { id, depth } = queue.shift();
|
|
4089
|
+
if (visited.has(id) || depth > 5) continue;
|
|
4090
|
+
visited.add(id);
|
|
4091
|
+
allAffected.add(id);
|
|
4092
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
4093
|
+
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.source, depth: depth + 1 });
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
const changedSymbols = [...hitNodes].map((id) => {
|
|
4098
|
+
const n = graph.getNode(id);
|
|
4099
|
+
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
4100
|
+
});
|
|
4101
|
+
const affectedSymbols = [...allAffected].filter((id) => !hitNodes.has(id)).map((id) => {
|
|
4102
|
+
const n = graph.getNode(id);
|
|
4103
|
+
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
4104
|
+
});
|
|
4105
|
+
const risk = allAffected.size > 10 ? "HIGH" : allAffected.size > 4 ? "MEDIUM" : "LOW";
|
|
4106
|
+
return {
|
|
4107
|
+
content: [{
|
|
4108
|
+
type: "text",
|
|
4109
|
+
text: JSON.stringify({
|
|
4110
|
+
baseRef,
|
|
4111
|
+
changedFiles: changedFiles.map((f) => f.filePath),
|
|
4112
|
+
directlyChangedSymbols: changedSymbols,
|
|
4113
|
+
transitivelyAffectedSymbols: affectedSymbols,
|
|
4114
|
+
totalAffected: allAffected.size,
|
|
4115
|
+
riskLevel: risk
|
|
4116
|
+
}, null, 2)
|
|
4117
|
+
}]
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
// ── raw_query ──────────────────────────────────────────────────────────
|
|
4121
|
+
case "raw_query": {
|
|
4122
|
+
const q = a.cypher;
|
|
4123
|
+
const nameMatch = q?.match(/name\s*=\s*['"]([^'"]+)['"]/i);
|
|
4124
|
+
if (nameMatch) {
|
|
4125
|
+
const results = [];
|
|
4126
|
+
for (const node of graph.allNodes()) {
|
|
4127
|
+
if (node.name === nameMatch[1]) results.push(node);
|
|
4128
|
+
}
|
|
4129
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
4130
|
+
}
|
|
4131
|
+
const kindMatch = q?.match(/:\s*(\w+)/);
|
|
4132
|
+
if (kindMatch) {
|
|
4133
|
+
const results = [];
|
|
4134
|
+
for (const node of graph.allNodes()) {
|
|
4135
|
+
if (node.kind === kindMatch[1]) results.push(node);
|
|
4136
|
+
if (results.length >= 50) break;
|
|
4137
|
+
}
|
|
4138
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
4139
|
+
}
|
|
4140
|
+
return { content: [{ type: "text", text: "Query not recognized. Use name='X' or :kind syntax." }] };
|
|
4141
|
+
}
|
|
4142
|
+
// ── group_list ─────────────────────────────────────────────────────────
|
|
4143
|
+
case "group_list": {
|
|
4144
|
+
const groupName = a.name;
|
|
4145
|
+
if (groupName) {
|
|
4146
|
+
const group = loadGroup(groupName);
|
|
4147
|
+
if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
|
|
4148
|
+
return { content: [{ type: "text", text: JSON.stringify(group, null, 2) }] };
|
|
4149
|
+
}
|
|
4150
|
+
const groups = listGroups();
|
|
4151
|
+
return {
|
|
4152
|
+
content: [{
|
|
4153
|
+
type: "text",
|
|
4154
|
+
text: JSON.stringify(
|
|
4155
|
+
groups.map((g) => ({ name: g.name, createdAt: g.createdAt, lastSync: g.lastSync, memberCount: g.members.length, members: g.members })),
|
|
4156
|
+
null,
|
|
4157
|
+
2
|
|
4158
|
+
)
|
|
4159
|
+
}]
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
// ── group_sync ─────────────────────────────────────────────────────────
|
|
4163
|
+
case "group_sync": {
|
|
4164
|
+
const groupName = a.name;
|
|
4165
|
+
const group = loadGroup(groupName);
|
|
4166
|
+
if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
|
|
4167
|
+
if (group.members.length === 0) return { content: [{ type: "text", text: `Group "${groupName}" has no members.` }] };
|
|
4168
|
+
const result = await syncGroup(group);
|
|
4169
|
+
saveSyncResult(result);
|
|
4170
|
+
group.lastSync = result.syncedAt;
|
|
4171
|
+
saveGroup(group);
|
|
4172
|
+
return {
|
|
4173
|
+
content: [{
|
|
4174
|
+
type: "text",
|
|
4175
|
+
text: JSON.stringify({
|
|
4176
|
+
groupName: result.groupName,
|
|
4177
|
+
syncedAt: result.syncedAt,
|
|
4178
|
+
memberCount: result.memberCount,
|
|
4179
|
+
contractCount: result.contracts.length,
|
|
4180
|
+
linkCount: result.links.length,
|
|
4181
|
+
topLinks: result.links.slice(0, 20)
|
|
4182
|
+
}, null, 2)
|
|
4183
|
+
}]
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
// ── group_contracts ────────────────────────────────────────────────────
|
|
4187
|
+
case "group_contracts": {
|
|
4188
|
+
const groupName = a.name;
|
|
4189
|
+
const kindFilter = a.kind;
|
|
4190
|
+
const repoFilter = a.repo;
|
|
4191
|
+
const minConf = a.min_confidence ?? 0;
|
|
4192
|
+
const result = loadSyncResult(groupName);
|
|
4193
|
+
if (!result) return { content: [{ type: "text", text: `No sync data for group "${groupName}". Run group_sync first.` }] };
|
|
4194
|
+
let contracts = result.contracts;
|
|
4195
|
+
if (kindFilter) contracts = contracts.filter((c) => c.kind === kindFilter);
|
|
4196
|
+
if (repoFilter) contracts = contracts.filter((c) => c.repoName === repoFilter);
|
|
4197
|
+
let links = result.links.filter((l) => l.confidence >= minConf);
|
|
4198
|
+
if (repoFilter) links = links.filter((l) => l.providerRepo === repoFilter || l.consumerRepo === repoFilter);
|
|
4199
|
+
return {
|
|
4200
|
+
content: [{
|
|
4201
|
+
type: "text",
|
|
4202
|
+
text: JSON.stringify({ syncedAt: result.syncedAt, contracts, links }, null, 2)
|
|
4203
|
+
}]
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
// ── group_query ────────────────────────────────────────────────────────
|
|
4207
|
+
case "group_query": {
|
|
4208
|
+
const groupName = a.name;
|
|
4209
|
+
const query = a.query;
|
|
4210
|
+
const limit = a.limit ?? 10;
|
|
4211
|
+
const group = loadGroup(groupName);
|
|
4212
|
+
if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
|
|
4213
|
+
const { perRepo, merged } = await queryGroup(group, query, limit);
|
|
4214
|
+
return {
|
|
4215
|
+
content: [{
|
|
4216
|
+
type: "text",
|
|
4217
|
+
text: JSON.stringify({ query, merged, perRepo }, null, 2)
|
|
4218
|
+
}]
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
4221
|
+
// ── group_status ───────────────────────────────────────────────────────
|
|
4222
|
+
case "group_status": {
|
|
4223
|
+
const groupName = a.name;
|
|
4224
|
+
const group = loadGroup(groupName);
|
|
4225
|
+
if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
|
|
4226
|
+
const registry = loadRegistry();
|
|
4227
|
+
const now = Date.now();
|
|
4228
|
+
const memberStatus = group.members.map((m) => {
|
|
4229
|
+
const regEntry = registry.find((r) => r.name === m.registryName);
|
|
4230
|
+
if (!regEntry) return { groupPath: m.groupPath, registryName: m.registryName, status: "NOT_IN_REGISTRY" };
|
|
4231
|
+
const meta = loadMetadata(regEntry.path);
|
|
4232
|
+
if (!meta) return { groupPath: m.groupPath, registryName: m.registryName, repoPath: regEntry.path, status: "NOT_INDEXED" };
|
|
4233
|
+
const ageMin = Math.round((now - new Date(meta.indexedAt).getTime()) / 6e4);
|
|
4234
|
+
const stale = ageMin > 1440;
|
|
4235
|
+
return {
|
|
4236
|
+
groupPath: m.groupPath,
|
|
4237
|
+
registryName: m.registryName,
|
|
4238
|
+
repoPath: regEntry.path,
|
|
4239
|
+
indexedAt: meta.indexedAt,
|
|
4240
|
+
ageMinutes: ageMin,
|
|
4241
|
+
status: stale ? "STALE" : "OK",
|
|
4242
|
+
stats: meta.stats
|
|
4243
|
+
};
|
|
4244
|
+
});
|
|
4245
|
+
const syncAge = group.lastSync ? Math.round((now - new Date(group.lastSync).getTime()) / 6e4) : null;
|
|
4246
|
+
return {
|
|
4247
|
+
content: [{
|
|
4248
|
+
type: "text",
|
|
4249
|
+
text: JSON.stringify({
|
|
4250
|
+
group: groupName,
|
|
4251
|
+
lastSync: group.lastSync ?? null,
|
|
4252
|
+
syncAgeMinutes: syncAge,
|
|
4253
|
+
members: memberStatus
|
|
4254
|
+
}, null, 2)
|
|
4255
|
+
}]
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4258
|
+
default:
|
|
4259
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
4263
|
+
resources: [
|
|
4264
|
+
{ uri: `codeintel://repo/${repoName}/overview`, name: `${repoName} Overview`, mimeType: "application/json" },
|
|
4265
|
+
{ uri: `codeintel://repo/${repoName}/clusters`, name: `${repoName} Clusters`, mimeType: "application/json" },
|
|
4266
|
+
{ uri: `codeintel://repo/${repoName}/flows`, name: `${repoName} Flows`, mimeType: "application/json" }
|
|
4267
|
+
]
|
|
4268
|
+
}));
|
|
4269
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
4270
|
+
const { uri } = request.params;
|
|
4271
|
+
if (uri.endsWith("/overview")) {
|
|
4272
|
+
const kindCounts = {};
|
|
4273
|
+
for (const node of graph.allNodes()) {
|
|
4274
|
+
kindCounts[node.kind] = (kindCounts[node.kind] ?? 0) + 1;
|
|
4275
|
+
}
|
|
4276
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ repo: repoName, stats: graph.size, nodeCounts: kindCounts }) }] };
|
|
4277
|
+
}
|
|
4278
|
+
if (uri.endsWith("/clusters")) {
|
|
4279
|
+
const clusters = [];
|
|
4280
|
+
for (const node of graph.allNodes()) {
|
|
4281
|
+
if (node.kind === "cluster") clusters.push({ id: node.id, name: node.name, memberCount: node.metadata?.memberCount });
|
|
4282
|
+
}
|
|
4283
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(clusters) }] };
|
|
4284
|
+
}
|
|
4285
|
+
if (uri.endsWith("/flows")) {
|
|
4286
|
+
const flows = [];
|
|
4287
|
+
for (const node of graph.allNodes()) {
|
|
4288
|
+
if (node.kind === "flow") flows.push({ id: node.id, name: node.name, steps: node.metadata?.steps, entryPoint: node.metadata?.entryPoint });
|
|
4289
|
+
}
|
|
4290
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(flows) }] };
|
|
4291
|
+
}
|
|
4292
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
4293
|
+
});
|
|
4294
|
+
return server;
|
|
4295
|
+
}
|
|
4296
|
+
async function startMcpStdio(graph, repoName, workspaceRoot) {
|
|
4297
|
+
const server = createMcpServer(graph, repoName, workspaceRoot);
|
|
4298
|
+
const transport = new StdioServerTransport();
|
|
4299
|
+
await server.connect(transport);
|
|
4300
|
+
}
|
|
4301
|
+
function findNodeByName(graph, name) {
|
|
4302
|
+
for (const node of graph.allNodes()) {
|
|
4303
|
+
if (node.name === name) return node;
|
|
4304
|
+
}
|
|
4305
|
+
return void 0;
|
|
4306
|
+
}
|
|
4307
|
+
function parseDiff(diffText) {
|
|
4308
|
+
const result = [];
|
|
4309
|
+
let currentFile = null;
|
|
4310
|
+
let currentNewLine = 0;
|
|
4311
|
+
const changedLinesMap = /* @__PURE__ */ new Map();
|
|
4312
|
+
for (const raw of diffText.split("\n")) {
|
|
4313
|
+
const fileMatch = raw.match(/^\+\+\+ b\/(.+)/);
|
|
4314
|
+
if (fileMatch) {
|
|
4315
|
+
currentFile = fileMatch[1];
|
|
4316
|
+
if (!changedLinesMap.has(currentFile)) changedLinesMap.set(currentFile, []);
|
|
4317
|
+
continue;
|
|
4318
|
+
}
|
|
4319
|
+
const hunkMatch = raw.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
4320
|
+
if (hunkMatch) {
|
|
4321
|
+
currentNewLine = parseInt(hunkMatch[1], 10);
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
if (!currentFile) continue;
|
|
4325
|
+
if (raw.startsWith("+") && !raw.startsWith("+++")) {
|
|
4326
|
+
changedLinesMap.get(currentFile).push(currentNewLine);
|
|
4327
|
+
currentNewLine++;
|
|
4328
|
+
} else if (raw.startsWith("-") && !raw.startsWith("---")) ; else if (!raw.startsWith("\\")) {
|
|
4329
|
+
currentNewLine++;
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
for (const [filePath, changedLines] of changedLinesMap) {
|
|
4333
|
+
result.push({ filePath, changedLines });
|
|
4334
|
+
}
|
|
4335
|
+
return result;
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
// src/storage/graph-loader.ts
|
|
4339
|
+
async function loadGraphToDB(graph, dbManager) {
|
|
4340
|
+
for (const table of ALL_NODE_TABLES) {
|
|
4341
|
+
await dbManager.execute(getCreateNodeTableDDL(table));
|
|
4342
|
+
}
|
|
4343
|
+
const edgeDDLs = getCreateEdgeTableDDL();
|
|
4344
|
+
for (const ddl of edgeDDLs) {
|
|
4345
|
+
try {
|
|
4346
|
+
await dbManager.execute(ddl);
|
|
4347
|
+
} catch {
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
let nodeCount = 0;
|
|
4351
|
+
for (const node of graph.allNodes()) {
|
|
4352
|
+
const table = NODE_TABLE_MAP[node.kind];
|
|
4353
|
+
const props = buildNodeProps(node);
|
|
4354
|
+
try {
|
|
4355
|
+
await dbManager.execute(`CREATE (:${table} ${props})`);
|
|
4356
|
+
nodeCount++;
|
|
4357
|
+
} catch {
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
let edgeCount = 0;
|
|
4361
|
+
for (const edge of graph.allEdges()) {
|
|
4362
|
+
const sourceNode = graph.getNode(edge.source);
|
|
4363
|
+
const targetNode = graph.getNode(edge.target);
|
|
4364
|
+
if (!sourceNode || !targetNode) continue;
|
|
4365
|
+
const fromTable = NODE_TABLE_MAP[sourceNode.kind];
|
|
4366
|
+
const toTable = NODE_TABLE_MAP[targetNode.kind];
|
|
4367
|
+
try {
|
|
4368
|
+
await dbManager.execute(
|
|
4369
|
+
`MATCH (a:${fromTable} {id: '${escCypher(edge.source)}'}), (b:${toTable} {id: '${escCypher(edge.target)}'}) CREATE (a)-[:code_edges {kind: '${edge.kind}', weight: ${edge.weight ?? 1}, label: '${escCypher(edge.label ?? "")}'}]->(b)`
|
|
4370
|
+
);
|
|
4371
|
+
edgeCount++;
|
|
4372
|
+
} catch {
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
return { nodeCount, edgeCount };
|
|
4376
|
+
}
|
|
4377
|
+
function buildNodeProps(node) {
|
|
4378
|
+
const parts = [
|
|
4379
|
+
`id: '${escCypher(node.id)}'`,
|
|
4380
|
+
`name: '${escCypher(node.name)}'`,
|
|
4381
|
+
`file_path: '${escCypher(node.filePath)}'`
|
|
4382
|
+
];
|
|
4383
|
+
if (node.startLine !== void 0) parts.push(`start_line: ${node.startLine}`);
|
|
4384
|
+
if (node.endLine !== void 0) parts.push(`end_line: ${node.endLine}`);
|
|
4385
|
+
if (node.exported !== void 0) parts.push(`exported: ${node.exported}`);
|
|
4386
|
+
if (node.content) parts.push(`content: '${escCypher(node.content.slice(0, 500))}'`);
|
|
4387
|
+
if (node.metadata) parts.push(`metadata: '${escCypher(JSON.stringify(node.metadata))}'`);
|
|
4388
|
+
return `{${parts.join(", ")}}`;
|
|
4389
|
+
}
|
|
4390
|
+
function escCypher(s) {
|
|
4391
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
// src/http/app.ts
|
|
4395
|
+
init_group_registry();
|
|
4396
|
+
var __dirname$1 = path6.dirname(fileURLToPath(import.meta.url));
|
|
4397
|
+
var WEB_DIST = path6.resolve(__dirname$1, "..", "..", "..", "web", "dist");
|
|
4398
|
+
function createApp(graph, repoName, workspaceRoot) {
|
|
4399
|
+
const app = express();
|
|
4400
|
+
app.use(cors({ origin: true }));
|
|
4401
|
+
app.use(express.json({ limit: "10mb" }));
|
|
4402
|
+
let vectorIndex = null;
|
|
4403
|
+
let vectorIndexBuilding = false;
|
|
4404
|
+
let vectorIndexReady = false;
|
|
4405
|
+
async function ensureVectorIndex() {
|
|
4406
|
+
if (vectorIndexReady && vectorIndex) return vectorIndex;
|
|
4407
|
+
if (!workspaceRoot || vectorIndexBuilding) return null;
|
|
4408
|
+
vectorIndexBuilding = true;
|
|
4409
|
+
try {
|
|
4410
|
+
const { embedNodes: embedNodes2 } = await Promise.resolve().then(() => (init_embedder(), embedder_exports));
|
|
4411
|
+
const dbPath = getVectorDbPath(workspaceRoot);
|
|
4412
|
+
const db = new DbManager(dbPath);
|
|
4413
|
+
await db.init();
|
|
4414
|
+
const idx = new VectorIndex(db);
|
|
4415
|
+
await idx.init();
|
|
4416
|
+
const alreadyBuilt = await idx.isBuilt();
|
|
4417
|
+
if (!alreadyBuilt) {
|
|
4418
|
+
logger_default.info(" [vector] Building embeddings\u2026");
|
|
4419
|
+
const nodes = await embedNodes2(graph, {
|
|
4420
|
+
onProgress: (done, total) => {
|
|
4421
|
+
if (done % 50 === 0 || done === total) process.stdout.write(`\r [vector] ${done}/${total}`);
|
|
4422
|
+
}
|
|
4423
|
+
});
|
|
4424
|
+
logger_default.info("");
|
|
4425
|
+
await idx.buildIndex(nodes);
|
|
4426
|
+
logger_default.info(` [vector] Index built: ${nodes.length} embeddings`);
|
|
4427
|
+
} else {
|
|
4428
|
+
logger_default.info(" [vector] Index already exists, skipping rebuild.");
|
|
4429
|
+
}
|
|
4430
|
+
vectorIndex = idx;
|
|
4431
|
+
vectorIndexReady = true;
|
|
4432
|
+
return idx;
|
|
4433
|
+
} catch (err) {
|
|
4434
|
+
logger_default.warn(" [vector] Index build failed:", err instanceof Error ? err.message : err);
|
|
4435
|
+
return null;
|
|
4436
|
+
} finally {
|
|
4437
|
+
vectorIndexBuilding = false;
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
if (workspaceRoot) {
|
|
4441
|
+
setImmediate(() => ensureVectorIndex().catch(() => {
|
|
4442
|
+
}));
|
|
4443
|
+
}
|
|
4444
|
+
app.get("/api/health", (_req, res) => {
|
|
4445
|
+
res.json({ status: "ok", nodes: graph.size.nodes, edges: graph.size.edges });
|
|
4446
|
+
});
|
|
4447
|
+
app.get("/api/repos", (_req, res) => {
|
|
4448
|
+
const registry = loadRegistry();
|
|
4449
|
+
if (registry.length === 0) {
|
|
4450
|
+
res.json([{ name: repoName, path: workspaceRoot ?? "", nodes: graph.size.nodes, edges: graph.size.edges, indexedAt: null }]);
|
|
4451
|
+
return;
|
|
4452
|
+
}
|
|
4453
|
+
res.json(registry.map((r) => ({
|
|
4454
|
+
name: r.name,
|
|
4455
|
+
path: r.path,
|
|
4456
|
+
nodes: r.stats.nodes,
|
|
4457
|
+
edges: r.stats.edges,
|
|
4458
|
+
indexedAt: r.indexedAt,
|
|
4459
|
+
active: r.path === workspaceRoot
|
|
4460
|
+
})));
|
|
4461
|
+
});
|
|
4462
|
+
async function loadRepoGraph(requestedRepo) {
|
|
4463
|
+
if (requestedRepo === repoName) return graph;
|
|
4464
|
+
const registry = loadRegistry();
|
|
4465
|
+
const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
|
|
4466
|
+
if (!entry) return null;
|
|
4467
|
+
const dbPath = path6.join(entry.path, ".code-intel", "graph.db");
|
|
4468
|
+
if (!fs6.existsSync(dbPath)) return null;
|
|
4469
|
+
const repoGraph = createKnowledgeGraph();
|
|
4470
|
+
const db = new DbManager(dbPath);
|
|
4471
|
+
try {
|
|
4472
|
+
await db.init();
|
|
4473
|
+
await loadGraphFromDB(repoGraph, db);
|
|
4474
|
+
db.close();
|
|
4475
|
+
return repoGraph;
|
|
4476
|
+
} catch {
|
|
4477
|
+
db.close();
|
|
4478
|
+
return null;
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
app.get("/api/graph/:repo", async (req, res) => {
|
|
4482
|
+
const requestedRepo = decodeURIComponent(req.params.repo);
|
|
4483
|
+
const g = await loadRepoGraph(requestedRepo);
|
|
4484
|
+
if (!g) {
|
|
4485
|
+
res.status(404).json({ error: `Repo "${requestedRepo}" not found or not indexed. Run: code-intel analyze <path>` });
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
const nodes = [...g.allNodes()];
|
|
4489
|
+
const edges = [...g.allEdges()];
|
|
4490
|
+
res.json({ nodes, edges });
|
|
4491
|
+
});
|
|
4492
|
+
async function getGraphForRepo(requestedRepo) {
|
|
4493
|
+
if (!requestedRepo || requestedRepo === repoName) return graph;
|
|
4494
|
+
const g = await loadRepoGraph(requestedRepo);
|
|
4495
|
+
return g ?? graph;
|
|
4496
|
+
}
|
|
4497
|
+
app.post("/api/search", async (req, res) => {
|
|
4498
|
+
const { query, limit, repo } = req.body;
|
|
4499
|
+
const g = await getGraphForRepo(repo);
|
|
4500
|
+
const results = textSearch(g, query, limit ?? 20);
|
|
4501
|
+
res.json({ results });
|
|
4502
|
+
});
|
|
4503
|
+
app.post("/api/vector-search", async (req, res) => {
|
|
4504
|
+
const { query, limit = 10 } = req.body;
|
|
4505
|
+
if (!query) {
|
|
4506
|
+
res.status(400).json({ error: "Missing query" });
|
|
4507
|
+
return;
|
|
4508
|
+
}
|
|
4509
|
+
const idx = await ensureVectorIndex();
|
|
4510
|
+
if (!idx) {
|
|
3607
4511
|
const results = textSearch(graph, query, limit);
|
|
3608
4512
|
res.json({ results, source: "text-fallback", vectorReady: false });
|
|
3609
4513
|
return;
|
|
@@ -3633,7 +4537,7 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
3633
4537
|
app.post("/api/files/read", (req, res) => {
|
|
3634
4538
|
const { file_path } = req.body;
|
|
3635
4539
|
try {
|
|
3636
|
-
const content =
|
|
4540
|
+
const content = fs6.readFileSync(file_path, "utf-8");
|
|
3637
4541
|
res.json({ content });
|
|
3638
4542
|
} catch {
|
|
3639
4543
|
res.status(404).json({ error: "File not found" });
|
|
@@ -3712,55 +4616,57 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
3712
4616
|
res.status(400).json({ error: "Invalid query" });
|
|
3713
4617
|
}
|
|
3714
4618
|
});
|
|
3715
|
-
app.get("/api/nodes/:id", (req, res) => {
|
|
4619
|
+
app.get("/api/nodes/:id", async (req, res) => {
|
|
3716
4620
|
const nodeId = decodeURIComponent(req.params.id);
|
|
3717
|
-
const
|
|
4621
|
+
const g = await getGraphForRepo(req.query.repo);
|
|
4622
|
+
const node = g.getNode(nodeId);
|
|
3718
4623
|
if (!node) {
|
|
3719
4624
|
res.status(404).json({ error: "Node not found" });
|
|
3720
4625
|
return;
|
|
3721
4626
|
}
|
|
3722
|
-
const incoming = [...
|
|
3723
|
-
const outgoing = [...
|
|
4627
|
+
const incoming = [...g.findEdgesTo(nodeId)];
|
|
4628
|
+
const outgoing = [...g.findEdgesFrom(nodeId)];
|
|
3724
4629
|
res.json({
|
|
3725
4630
|
node,
|
|
3726
4631
|
callers: incoming.filter((e) => e.kind === "calls").map((e) => ({
|
|
3727
4632
|
id: e.source,
|
|
3728
|
-
name:
|
|
4633
|
+
name: g.getNode(e.source)?.name,
|
|
3729
4634
|
weight: e.weight
|
|
3730
4635
|
})),
|
|
3731
4636
|
callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
3732
4637
|
id: e.target,
|
|
3733
|
-
name:
|
|
4638
|
+
name: g.getNode(e.target)?.name,
|
|
3734
4639
|
weight: e.weight
|
|
3735
4640
|
})),
|
|
3736
4641
|
imports: outgoing.filter((e) => e.kind === "imports").map((e) => ({
|
|
3737
4642
|
id: e.target,
|
|
3738
|
-
name:
|
|
4643
|
+
name: g.getNode(e.target)?.name
|
|
3739
4644
|
})),
|
|
3740
4645
|
importedBy: incoming.filter((e) => e.kind === "imports").map((e) => ({
|
|
3741
4646
|
id: e.source,
|
|
3742
|
-
name:
|
|
4647
|
+
name: g.getNode(e.source)?.name
|
|
3743
4648
|
})),
|
|
3744
4649
|
extends: outgoing.filter((e) => e.kind === "extends").map((e) => ({
|
|
3745
4650
|
id: e.target,
|
|
3746
|
-
name:
|
|
4651
|
+
name: g.getNode(e.target)?.name
|
|
3747
4652
|
})),
|
|
3748
4653
|
implementsEdges: outgoing.filter((e) => e.kind === "implements").map((e) => ({
|
|
3749
4654
|
id: e.target,
|
|
3750
|
-
name:
|
|
4655
|
+
name: g.getNode(e.target)?.name
|
|
3751
4656
|
})),
|
|
3752
4657
|
members: outgoing.filter((e) => e.kind === "has_member").map((e) => ({
|
|
3753
4658
|
id: e.target,
|
|
3754
|
-
name:
|
|
3755
|
-
kind:
|
|
4659
|
+
name: g.getNode(e.target)?.name,
|
|
4660
|
+
kind: g.getNode(e.target)?.kind
|
|
3756
4661
|
})),
|
|
3757
|
-
cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) =>
|
|
4662
|
+
cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => g.getNode(e.target)?.name)[0]
|
|
3758
4663
|
});
|
|
3759
4664
|
});
|
|
3760
|
-
app.post("/api/blast-radius", (req, res) => {
|
|
3761
|
-
const { target, direction = "both", max_hops = 5 } = req.body;
|
|
4665
|
+
app.post("/api/blast-radius", async (req, res) => {
|
|
4666
|
+
const { target, direction = "both", max_hops = 5, repo } = req.body;
|
|
4667
|
+
const g = await getGraphForRepo(repo);
|
|
3762
4668
|
let targetNode = null;
|
|
3763
|
-
for (const node of
|
|
4669
|
+
for (const node of g.allNodes()) {
|
|
3764
4670
|
if (node.name === target || node.id === target) {
|
|
3765
4671
|
targetNode = node;
|
|
3766
4672
|
break;
|
|
@@ -3777,22 +4683,16 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
3777
4683
|
const { id, depth } = queue.shift();
|
|
3778
4684
|
if (visited.has(id) || depth > max_hops) continue;
|
|
3779
4685
|
visited.add(id);
|
|
3780
|
-
const node =
|
|
3781
|
-
if (node) {
|
|
3782
|
-
affected.set(id, { name: node.name, kind: node.kind, depth });
|
|
3783
|
-
}
|
|
4686
|
+
const node = g.getNode(id);
|
|
4687
|
+
if (node) affected.set(id, { name: node.name, kind: node.kind, depth });
|
|
3784
4688
|
if (direction === "callers" || direction === "both") {
|
|
3785
|
-
for (const edge of
|
|
3786
|
-
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
3787
|
-
queue.push({ id: edge.source, depth: depth + 1 });
|
|
3788
|
-
}
|
|
4689
|
+
for (const edge of g.findEdgesTo(id)) {
|
|
4690
|
+
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.source, depth: depth + 1 });
|
|
3789
4691
|
}
|
|
3790
4692
|
}
|
|
3791
4693
|
if (direction === "callees" || direction === "both") {
|
|
3792
|
-
for (const edge of
|
|
3793
|
-
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
3794
|
-
queue.push({ id: edge.target, depth: depth + 1 });
|
|
3795
|
-
}
|
|
4694
|
+
for (const edge of g.findEdgesFrom(id)) {
|
|
4695
|
+
if (edge.kind === "calls" || edge.kind === "imports") queue.push({ id: edge.target, depth: depth + 1 });
|
|
3796
4696
|
}
|
|
3797
4697
|
}
|
|
3798
4698
|
}
|
|
@@ -3802,28 +4702,20 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
3802
4702
|
affected: [...affected.entries()].map(([id, info]) => ({ id, ...info })).filter((a) => a.depth > 0)
|
|
3803
4703
|
});
|
|
3804
4704
|
});
|
|
3805
|
-
app.get("/api/flows", (
|
|
4705
|
+
app.get("/api/flows", async (req, res) => {
|
|
4706
|
+
const g = await getGraphForRepo(req.query.repo);
|
|
3806
4707
|
const flows = [];
|
|
3807
|
-
for (const node of
|
|
3808
|
-
if (node.kind === "flow") {
|
|
3809
|
-
flows.push({
|
|
3810
|
-
id: node.id,
|
|
3811
|
-
name: node.name,
|
|
3812
|
-
steps: node.metadata?.steps
|
|
3813
|
-
});
|
|
3814
|
-
}
|
|
4708
|
+
for (const node of g.allNodes()) {
|
|
4709
|
+
if (node.kind === "flow") flows.push({ id: node.id, name: node.name, steps: node.metadata?.steps });
|
|
3815
4710
|
}
|
|
3816
4711
|
res.json({ flows });
|
|
3817
4712
|
});
|
|
3818
|
-
app.get("/api/clusters", (
|
|
4713
|
+
app.get("/api/clusters", async (req, res) => {
|
|
4714
|
+
const g = await getGraphForRepo(req.query.repo);
|
|
3819
4715
|
const clusters = [];
|
|
3820
|
-
for (const node of
|
|
4716
|
+
for (const node of g.allNodes()) {
|
|
3821
4717
|
if (node.kind === "cluster") {
|
|
3822
|
-
clusters.push({
|
|
3823
|
-
id: node.id,
|
|
3824
|
-
name: node.name,
|
|
3825
|
-
memberCount: node.metadata?.memberCount ?? 0
|
|
3826
|
-
});
|
|
4718
|
+
clusters.push({ id: node.id, name: node.name, memberCount: node.metadata?.memberCount ?? 0 });
|
|
3827
4719
|
}
|
|
3828
4720
|
}
|
|
3829
4721
|
res.json({ clusters });
|
|
@@ -3900,7 +4792,7 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
3900
4792
|
const regEntry = registry.find((r) => r.name === member.registryName);
|
|
3901
4793
|
if (!regEntry) continue;
|
|
3902
4794
|
const dbPath = path6.join(regEntry.path, ".code-intel", "graph.db");
|
|
3903
|
-
if (!
|
|
4795
|
+
if (!fs6.existsSync(dbPath)) continue;
|
|
3904
4796
|
const db = new DbManager(dbPath);
|
|
3905
4797
|
try {
|
|
3906
4798
|
await db.init();
|
|
@@ -3912,7 +4804,7 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
3912
4804
|
}
|
|
3913
4805
|
res.json({ nodes: [...mergedGraph.allNodes()], edges: [...mergedGraph.allEdges()] });
|
|
3914
4806
|
});
|
|
3915
|
-
if (
|
|
4807
|
+
if (fs6.existsSync(WEB_DIST)) {
|
|
3916
4808
|
app.use(express.static(WEB_DIST));
|
|
3917
4809
|
app.get("/{*path}", (_req, res) => {
|
|
3918
4810
|
res.sendFile(path6.join(WEB_DIST, "index.html"));
|
|
@@ -3923,8 +4815,8 @@ function createApp(graph, repoName, workspaceRoot) {
|
|
|
3923
4815
|
function startHttpServer(graph, repoName, port = 4747, workspaceRoot) {
|
|
3924
4816
|
const app = createApp(graph, repoName, workspaceRoot);
|
|
3925
4817
|
app.listen(port, () => {
|
|
3926
|
-
|
|
3927
|
-
|
|
4818
|
+
logger_default.info(`Code Intelligence server running at http://localhost:${port}`);
|
|
4819
|
+
logger_default.info(` Graph: ${graph.size.nodes} nodes, ${graph.size.edges} edges`);
|
|
3928
4820
|
});
|
|
3929
4821
|
}
|
|
3930
4822
|
|