@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/dist/index.js CHANGED
@@ -1,14 +1,17 @@
1
- import fs8 from 'fs';
1
+ import fs6 from 'fs';
2
2
  import path6 from 'path';
3
- import os2 from 'os';
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(fs8.readFileSync(groupFile(name), "utf-8"));
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
- fs8.mkdirSync(GROUPS_DIR, { recursive: true });
96
- fs8.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
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 fs8.readdirSync(GROUPS_DIR)) {
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
- fs8.readFileSync(path6.join(GROUPS_DIR, file), "utf-8")
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
- fs8.unlinkSync(groupFile(name));
120
+ fs6.unlinkSync(groupFile(name));
118
121
  } catch {
119
122
  }
120
123
  try {
121
- fs8.unlinkSync(path6.join(GROUPS_DIR, `${name}.sync.json`));
124
+ fs6.unlinkSync(path6.join(GROUPS_DIR, `${name}.sync.json`));
122
125
  } catch {
123
126
  }
124
127
  }
125
128
  function groupExists(name) {
126
- return fs8.existsSync(groupFile(name));
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
- fs8.mkdirSync(GROUPS_DIR, { recursive: true });
153
- fs8.writeFileSync(
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
- fs8.readFileSync(path6.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
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(os2.homedir(), ".code-intel", "groups");
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, path17) {
1670
+ function dfs(name, path19) {
1668
1671
  if (visiting.has(name)) {
1669
- const cycleStart = path17.indexOf(name);
1670
- const cycle = path17.slice(cycleStart).concat(name);
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
- path17.push(name);
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, path17)) return true;
1683
+ if (dfs(dep, path19)) return true;
1681
1684
  }
1682
1685
  }
1683
1686
  visiting.delete(name);
1684
1687
  visited.add(name);
1685
- path17.pop();
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 = fs8.readFileSync(path6.join(workspaceRoot, ".codeintelignore"), "utf-8");
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 = fs8.readdirSync(dir, { withFileTypes: true });
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
- walk(fullPath);
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 ext = path6.extname(entry.name);
1832
- const fullName = entry.name;
1833
- if (fullName.endsWith(".d.ts") || fullName.endsWith(".js.map") || fullName.endsWith(".d.ts.map")) continue;
1834
- if (extensions.has(ext)) {
1835
- filePaths.push(fullPath);
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
- for (const filePath of context.filePaths) {
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
- console.log(` [parse] skipped (no parser): ${relativePath2}`);
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
- if (seen.has(extracted.name + ":" + extracted.kind)) continue;
1925
- seen.add(extracted.name + ":" + extracted.kind);
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 (["function", "class", "interface", "method", "enum", "type_alias", "variable", "constant", "struct", "trait"].includes(node.kind)) {
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
- let source;
2145
- try {
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
- const asPath = cleaned.replace(/\./g, "/");
2177
- for (const ext of ["", ".ts", ".js", ".py", ".java", ".go", "/index.ts", "/__init__.py"]) {
2178
- if (fileIndex.has(asPath + ext)) {
2179
- resolvedRelPath = asPath + ext;
2180
- break;
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 = findEnclosingFunction(graph, relativePath, call.line);
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(...namedMatch[1].split(",").map((n) => n.trim().split(/\s+as\s+/).pop().trim()).filter(Boolean));
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
- names.push(defaultMatch[1]);
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 (["if", "for", "while", "switch", "catch", "return", "throw", "typeof", "instanceof", "delete", "void", "new", "import", "export", "from", "const", "let", "var", "function", "class", "interface", "type", "enum", "extends", "implements"].includes(name)) continue;
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 findEnclosingFunction(graph, filePath, line) {
2723
+ function findEnclosingFunctionFast(funcs, line) {
2724
+ let lo = 0;
2725
+ let hi = funcs.length - 1;
2433
2726
  let best = null;
2434
- for (const node of graph.allNodes()) {
2435
- if (node.filePath !== filePath) continue;
2436
- if (!["function", "method"].includes(node.kind)) continue;
2437
- if (!node.startLine) continue;
2438
- if (node.startLine <= line) {
2439
- if (!best || node.startLine > best.startLine) {
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?.id ?? null;
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
- for (const [dir, members] of nodesByDir) {
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
- for (const ep of entryPoints.slice(0, 20)) {
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: path17 } = queue.shift();
2534
- if (path17.length > maxDepth) continue;
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 && path17.length >= 3) {
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: path17, entryPoint: ep.name }
2845
+ metadata: { steps: path19, entryPoint: ep.name }
2544
2846
  });
2545
- for (let i = 0; i < path17.length; i++) {
2847
+ for (let i = 0; i < path19.length; i++) {
2546
2848
  graph.addEdge({
2547
- id: generateEdgeId(path17[i], flowId, `step_of_${i}`),
2548
- source: path17[i],
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: [...path17, edge.target] });
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: path17 } = queue.shift();
2691
- if (path17.length > maxDepth) continue;
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 && path17.length >= 3) {
2694
- flows.push({ entryPointId: entryId, steps: [...path17] });
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: [...path17, edge.target] });
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
- function createMcpServer(graph, repoName) {
2862
- const server = new Server(
2863
- { name: "code-intel", version: "0.1.0" },
2864
- { capabilities: { tools: {}, resources: {} } }
2865
- );
2866
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
2867
- tools: [
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
- var DbManager = class {
3073
- db = null;
3074
- conn = null;
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
- fs8.mkdirSync(path6.dirname(this.dbPath), { recursive: true });
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
- console.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
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 (!fs8.existsSync(dbPath)) {
3471
- console.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
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
- console.warn(` \u26A0 Could not load graph for "${member.registryName}": ${err instanceof Error ? err.message : err}`);
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
- console.log(` \u2713 ${member.registryName} (${member.groupPath}): ${contracts.length} contracts`);
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 (!fs8.existsSync(dbPath)) continue;
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
- // src/http/app.ts
3535
- var __dirname$1 = path6.dirname(fileURLToPath(import.meta.url));
3536
- var WEB_DIST = path6.resolve(__dirname$1, "..", "..", "..", "web", "dist");
3537
- function createApp(graph, repoName, workspaceRoot) {
3538
- const app = express();
3539
- app.use(cors({ origin: true }));
3540
- app.use(express.json({ limit: "10mb" }));
3541
- let vectorIndex = null;
3542
- let vectorIndexBuilding = false;
3543
- let vectorIndexReady = false;
3544
- async function ensureVectorIndex() {
3545
- if (vectorIndexReady && vectorIndex) return vectorIndex;
3546
- if (!workspaceRoot || vectorIndexBuilding) return null;
3547
- vectorIndexBuilding = true;
3548
- try {
3549
- const { embedNodes: embedNodes2 } = await Promise.resolve().then(() => (init_embedder(), embedder_exports));
3550
- const dbPath = getVectorDbPath(workspaceRoot);
3551
- const db = new DbManager(dbPath);
3552
- await db.init();
3553
- const idx = new VectorIndex(db);
3554
- await idx.init();
3555
- const alreadyBuilt = await idx.isBuilt();
3556
- if (!alreadyBuilt) {
3557
- console.log(" [vector] Building embeddings\u2026");
3558
- const nodes = await embedNodes2(graph, {
3559
- onProgress: (done, total) => {
3560
- if (done % 50 === 0 || done === total) process.stdout.write(`\r [vector] ${done}/${total}`);
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
- console.log("");
3564
- await idx.buildIndex(nodes);
3565
- console.log(` [vector] Index built: ${nodes.length} embeddings`);
3566
- } else {
3567
- console.log(" [vector] Index already exists, skipping rebuild.");
3568
- }
3569
- vectorIndex = idx;
3570
- vectorIndexReady = true;
3571
- return idx;
3572
- } catch (err) {
3573
- console.warn(" [vector] Index build failed:", err instanceof Error ? err.message : err);
3574
- return null;
3575
- } finally {
3576
- vectorIndexBuilding = false;
3577
- }
3578
- }
3579
- if (workspaceRoot) {
3580
- setImmediate(() => ensureVectorIndex().catch(() => {
3581
- }));
3582
- }
3583
- app.get("/api/health", (_req, res) => {
3584
- res.json({ status: "ok", nodes: graph.size.nodes, edges: graph.size.edges });
3585
- });
3586
- app.get("/api/repos", (_req, res) => {
3587
- res.json([{ name: repoName, nodes: graph.size.nodes, edges: graph.size.edges }]);
3588
- });
3589
- app.get("/api/graph/:repo", (_req, res) => {
3590
- const nodes = [...graph.allNodes()];
3591
- const edges = [...graph.allEdges()];
3592
- res.json({ nodes, edges });
3593
- });
3594
- app.post("/api/search", (req, res) => {
3595
- const { query, limit } = req.body;
3596
- const results = textSearch(graph, query, limit ?? 20);
3597
- res.json({ results });
3598
- });
3599
- app.post("/api/vector-search", async (req, res) => {
3600
- const { query, limit = 10 } = req.body;
3601
- if (!query) {
3602
- res.status(400).json({ error: "Missing query" });
3603
- return;
3604
- }
3605
- const idx = await ensureVectorIndex();
3606
- if (!idx) {
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 = fs8.readFileSync(file_path, "utf-8");
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 node = graph.getNode(nodeId);
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 = [...graph.findEdgesTo(nodeId)];
3723
- const outgoing = [...graph.findEdgesFrom(nodeId)];
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: graph.getNode(e.source)?.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: graph.getNode(e.target)?.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: graph.getNode(e.target)?.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: graph.getNode(e.source)?.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: graph.getNode(e.target)?.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: graph.getNode(e.target)?.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: graph.getNode(e.target)?.name,
3755
- kind: graph.getNode(e.target)?.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) => graph.getNode(e.target)?.name)[0]
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 graph.allNodes()) {
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 = graph.getNode(id);
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 graph.findEdgesTo(id)) {
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 graph.findEdgesFrom(id)) {
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", (_req, res) => {
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 graph.allNodes()) {
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", (_req, res) => {
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 graph.allNodes()) {
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 (!fs8.existsSync(dbPath)) continue;
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 (fs8.existsSync(WEB_DIST)) {
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
- console.log(`Code Intelligence server running at http://localhost:${port}`);
3927
- console.log(` Graph: ${graph.size.nodes} nodes, ${graph.size.edges} edges`);
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