@vohongtho.infotech/code-intel 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'module';
2
2
  import { fileURLToPath } from 'url';
3
- import path26 from 'path';
3
+ import path27 from 'path';
4
4
  import fs19, { existsSync } from 'fs';
5
5
  import { Parser, Language, Query } from 'web-tree-sitter';
6
6
  import { NodeSDK } from '@opentelemetry/sdk-node';
@@ -26,6 +26,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSche
26
26
  import { Database, Connection } from '@ladybugdb/core';
27
27
  import { execSync } from 'child_process';
28
28
  import express from 'express';
29
+ import compression from 'compression';
29
30
  import cors from 'cors';
30
31
  import helmet from 'helmet';
31
32
  import cookieParser from 'cookie-parser';
@@ -125,11 +126,11 @@ var init_shared = __esm({
125
126
  }
126
127
  });
127
128
  function findBundledWasmDir() {
128
- const fileDir = path26.dirname(fileURLToPath(import.meta.url));
129
+ const fileDir = path27.dirname(fileURLToPath(import.meta.url));
129
130
  const candidates = [
130
- path26.join(fileDir, "wasm"),
131
+ path27.join(fileDir, "wasm"),
131
132
  // dist/index.js → dist/wasm/
132
- path26.join(fileDir, "../wasm")
133
+ path27.join(fileDir, "../wasm")
133
134
  // dist/cli/main.js → dist/wasm/
134
135
  ];
135
136
  for (const candidate of candidates) {
@@ -170,7 +171,7 @@ function wasmPath(lang) {
170
171
  }
171
172
  const bundled = BUNDLED_WASM_MAP[lang];
172
173
  if (bundled) {
173
- const bundledPath = path26.join(_bundledWasmDir, bundled);
174
+ const bundledPath = path27.join(_bundledWasmDir, bundled);
174
175
  if (existsSync(bundledPath)) return bundledPath;
175
176
  }
176
177
  return null;
@@ -1161,7 +1162,7 @@ var init_logger = __esm({
1161
1162
  };
1162
1163
  }
1163
1164
  /** Global log directory: ~/.code-intel/logs */
1164
- static LOG_DIR = path26.join(os12.homedir(), ".code-intel", "logs");
1165
+ static LOG_DIR = path27.join(os12.homedir(), ".code-intel", "logs");
1165
1166
  static getLogger() {
1166
1167
  if (!_Logger.instance) {
1167
1168
  const isProduction = process.env.NODE_ENV === "production";
@@ -1175,7 +1176,7 @@ var init_logger = __esm({
1175
1176
  }
1176
1177
  transports.push(
1177
1178
  new DailyRotateFile({
1178
- filename: path26.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
1179
+ filename: path27.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
1179
1180
  datePattern: "YYYY-MM-DD",
1180
1181
  maxSize: "20m",
1181
1182
  maxFiles: "14d"
@@ -1973,14 +1974,14 @@ var init_parse_phase = __esm({
1973
1974
  const lang = detectLanguage(filePath);
1974
1975
  if (!lang) {
1975
1976
  if (context2.verbose) {
1976
- const relativePath2 = path26.relative(context2.workspaceRoot, filePath);
1977
+ const relativePath2 = path27.relative(context2.workspaceRoot, filePath);
1977
1978
  logger_default.info(` [parse] skipped (no parser): ${relativePath2}`);
1978
1979
  }
1979
1980
  continue;
1980
1981
  }
1981
1982
  const source = context2.fileCache.get(filePath);
1982
1983
  if (!source) continue;
1983
- const relativePath = path26.relative(context2.workspaceRoot, filePath);
1984
+ const relativePath = path27.relative(context2.workspaceRoot, filePath);
1984
1985
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
1985
1986
  const fileNode = context2.graph.getNode(fileNodeId);
1986
1987
  if (fileNode) {
@@ -2220,11 +2221,11 @@ var init_resolve_phase = __esm({
2220
2221
  let heritageEdges = 0;
2221
2222
  const fileIndex = /* @__PURE__ */ new Map();
2222
2223
  for (const fp of filePaths) {
2223
- const rel = path26.relative(workspaceRoot, fp);
2224
+ const rel = path27.relative(workspaceRoot, fp);
2224
2225
  fileIndex.set(rel, fp);
2225
2226
  const noExt = rel.replace(/\.\w+$/, "");
2226
2227
  if (!fileIndex.has(noExt)) fileIndex.set(noExt, fp);
2227
- const base = path26.basename(rel, path26.extname(rel));
2228
+ const base = path27.basename(rel, path27.extname(rel));
2228
2229
  if (!fileIndex.has(base)) fileIndex.set(base, fp);
2229
2230
  }
2230
2231
  const symbolIndex = /* @__PURE__ */ new Map();
@@ -2255,7 +2256,7 @@ var init_resolve_phase = __esm({
2255
2256
  for (const filePath of filePaths) {
2256
2257
  const lang = detectLanguage(filePath);
2257
2258
  if (!lang) continue;
2258
- const relativePath = path26.relative(workspaceRoot, filePath);
2259
+ const relativePath = path27.relative(workspaceRoot, filePath);
2259
2260
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
2260
2261
  const source = fileCache.get(filePath);
2261
2262
  if (!source) continue;
@@ -2268,13 +2269,13 @@ var init_resolve_phase = __esm({
2268
2269
  let resolvedRelPath = null;
2269
2270
  if (cleaned.startsWith(".")) {
2270
2271
  const cleanedNoJs = cleaned.replace(/\.(js|jsx)$/, "");
2271
- const fromDir = path26.dirname(relativePath);
2272
+ const fromDir = path27.dirname(relativePath);
2272
2273
  for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", "/index.ts", "/index.js"]) {
2273
- const candidate = path26.join(fromDir, cleanedNoJs + ext);
2274
- const normalized = path26.normalize(candidate);
2274
+ const candidate = path27.join(fromDir, cleanedNoJs + ext);
2275
+ const normalized = path27.normalize(candidate);
2275
2276
  if (fileIndex.has(normalized)) {
2276
2277
  const absPath = fileIndex.get(normalized);
2277
- resolvedRelPath = path26.relative(workspaceRoot, absPath);
2278
+ resolvedRelPath = path27.relative(workspaceRoot, absPath);
2278
2279
  break;
2279
2280
  }
2280
2281
  }
@@ -2588,7 +2589,7 @@ __export(group_registry_exports, {
2588
2589
  saveSyncResult: () => saveSyncResult
2589
2590
  });
2590
2591
  function groupFile(name) {
2591
- return path26.join(GROUPS_DIR, `${name}.json`);
2592
+ return path27.join(GROUPS_DIR, `${name}.json`);
2592
2593
  }
2593
2594
  function loadGroup(name) {
2594
2595
  try {
@@ -2608,7 +2609,7 @@ function listGroups() {
2608
2609
  if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
2609
2610
  try {
2610
2611
  const g = JSON.parse(
2611
- fs19.readFileSync(path26.join(GROUPS_DIR, file), "utf-8")
2612
+ fs19.readFileSync(path27.join(GROUPS_DIR, file), "utf-8")
2612
2613
  );
2613
2614
  groups.push(g);
2614
2615
  } catch {
@@ -2624,7 +2625,7 @@ function deleteGroup(name) {
2624
2625
  } catch {
2625
2626
  }
2626
2627
  try {
2627
- fs19.unlinkSync(path26.join(GROUPS_DIR, `${name}.sync.json`));
2628
+ fs19.unlinkSync(path27.join(GROUPS_DIR, `${name}.sync.json`));
2628
2629
  } catch {
2629
2630
  }
2630
2631
  }
@@ -2657,14 +2658,14 @@ function removeMember(groupName, groupPath) {
2657
2658
  function saveSyncResult(result) {
2658
2659
  fs19.mkdirSync(GROUPS_DIR, { recursive: true });
2659
2660
  fs19.writeFileSync(
2660
- path26.join(GROUPS_DIR, `${result.groupName}.sync.json`),
2661
+ path27.join(GROUPS_DIR, `${result.groupName}.sync.json`),
2661
2662
  JSON.stringify(result, null, 2) + "\n"
2662
2663
  );
2663
2664
  }
2664
2665
  function loadSyncResult(groupName) {
2665
2666
  try {
2666
2667
  return JSON.parse(
2667
- fs19.readFileSync(path26.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
2668
+ fs19.readFileSync(path27.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
2668
2669
  );
2669
2670
  } catch {
2670
2671
  return null;
@@ -2673,7 +2674,7 @@ function loadSyncResult(groupName) {
2673
2674
  var GROUPS_DIR;
2674
2675
  var init_group_registry = __esm({
2675
2676
  "src/multi-repo/group-registry.ts"() {
2676
- GROUPS_DIR = path26.join(os12.homedir(), ".code-intel", "groups");
2677
+ GROUPS_DIR = path27.join(os12.homedir(), ".code-intel", "groups");
2677
2678
  }
2678
2679
  });
2679
2680
 
@@ -2926,9 +2927,9 @@ var init_orphan_files = __esm({
2926
2927
  // src/health/health-score.ts
2927
2928
  var health_score_exports = {};
2928
2929
  __export(health_score_exports, {
2929
- computeHealthReport: () => computeHealthReport
2930
+ computeHealthReport: () => computeHealthReport2
2930
2931
  });
2931
- function computeHealthReport(graph, godNodeConfig) {
2932
+ function computeHealthReport2(graph, godNodeConfig) {
2932
2933
  const deadCode = detectDeadCode(graph);
2933
2934
  const cycles = detectCircularDeps(graph);
2934
2935
  const godNodes = detectGodNodes(graph, godNodeConfig);
@@ -2954,6 +2955,753 @@ var init_health_score = __esm({
2954
2955
  }
2955
2956
  });
2956
2957
 
2958
+ // src/query/gql-parser.ts
2959
+ var gql_parser_exports = {};
2960
+ __export(gql_parser_exports, {
2961
+ isGQLParseError: () => isGQLParseError,
2962
+ parseGQL: () => parseGQL
2963
+ });
2964
+ function isGQLParseError(v) {
2965
+ return v.type === "GQLParseError";
2966
+ }
2967
+ function tokenize(input) {
2968
+ const tokens = [];
2969
+ let i = 0;
2970
+ const len = input.length;
2971
+ while (i < len) {
2972
+ if (/\s/.test(input[i])) {
2973
+ i++;
2974
+ continue;
2975
+ }
2976
+ if (input[i] === "#") {
2977
+ while (i < len && input[i] !== "\n") i++;
2978
+ continue;
2979
+ }
2980
+ const pos = i;
2981
+ if (input[i] === '"' || input[i] === "'") {
2982
+ const quote = input[i];
2983
+ i++;
2984
+ let str = "";
2985
+ while (i < len && input[i] !== quote) {
2986
+ if (input[i] === "\\") {
2987
+ i++;
2988
+ if (i < len) {
2989
+ const esc = input[i];
2990
+ str += esc === "n" ? "\n" : esc === "t" ? " " : esc;
2991
+ i++;
2992
+ }
2993
+ } else {
2994
+ str += input[i++];
2995
+ }
2996
+ }
2997
+ if (i >= len) {
2998
+ return { type: "GQLParseError", message: `Unterminated string at position ${pos}`, pos };
2999
+ }
3000
+ i++;
3001
+ tokens.push({ kind: "STRING", value: str, pos });
3002
+ continue;
3003
+ }
3004
+ if (/[0-9]/.test(input[i])) {
3005
+ let num = "";
3006
+ while (i < len && /[0-9]/.test(input[i])) num += input[i++];
3007
+ tokens.push({ kind: "NUMBER", value: num, pos });
3008
+ continue;
3009
+ }
3010
+ if (input[i] === "[") {
3011
+ tokens.push({ kind: "LBRACKET", value: "[", pos });
3012
+ i++;
3013
+ continue;
3014
+ }
3015
+ if (input[i] === "]") {
3016
+ tokens.push({ kind: "RBRACKET", value: "]", pos });
3017
+ i++;
3018
+ continue;
3019
+ }
3020
+ if (input[i] === "*") {
3021
+ tokens.push({ kind: "STAR", value: "*", pos });
3022
+ i++;
3023
+ continue;
3024
+ }
3025
+ if (input[i] === "!" && input[i + 1] === "=") {
3026
+ tokens.push({ kind: "OPERATOR", value: "!=", pos });
3027
+ i += 2;
3028
+ continue;
3029
+ }
3030
+ if (input[i] === "=") {
3031
+ tokens.push({ kind: "OPERATOR", value: "=", pos });
3032
+ i++;
3033
+ continue;
3034
+ }
3035
+ if (/[a-zA-Z_]/.test(input[i])) {
3036
+ let ident = "";
3037
+ while (i < len && /[a-zA-Z0-9_]/.test(input[i])) ident += input[i++];
3038
+ const upper = ident.toUpperCase();
3039
+ if (upper === "CONTAINS" || upper === "STARTS_WITH" || upper === "IN") {
3040
+ tokens.push({ kind: "OPERATOR", value: upper, pos });
3041
+ } else if (KEYWORDS.has(upper)) {
3042
+ tokens.push({ kind: "KEYWORD", value: upper, pos });
3043
+ } else {
3044
+ tokens.push({ kind: "IDENT", value: ident, pos });
3045
+ }
3046
+ continue;
3047
+ }
3048
+ if (input[i] === ",") {
3049
+ i++;
3050
+ continue;
3051
+ }
3052
+ return {
3053
+ type: "GQLParseError",
3054
+ message: `Unexpected character '${input[i]}' at position ${i}`,
3055
+ pos: i
3056
+ };
3057
+ }
3058
+ tokens.push({ kind: "EOF", value: "", pos: len });
3059
+ return tokens;
3060
+ }
3061
+ function parseGQL(input) {
3062
+ const tokens = tokenize(input.trim());
3063
+ if (!Array.isArray(tokens)) return tokens;
3064
+ const parser = new Parser2(tokens);
3065
+ return parser.parse();
3066
+ }
3067
+ var KEYWORDS, Parser2;
3068
+ var init_gql_parser = __esm({
3069
+ "src/query/gql-parser.ts"() {
3070
+ KEYWORDS = /* @__PURE__ */ new Set([
3071
+ "FIND",
3072
+ "TRAVERSE",
3073
+ "PATH",
3074
+ "COUNT",
3075
+ "WHERE",
3076
+ "FROM",
3077
+ "TO",
3078
+ "IN",
3079
+ "BY",
3080
+ "AND",
3081
+ "NOT",
3082
+ "LIMIT",
3083
+ "OFFSET",
3084
+ "DEPTH",
3085
+ "GROUP",
3086
+ "CONTAINS",
3087
+ "STARTS_WITH",
3088
+ "CALLS",
3089
+ "IMPORTS",
3090
+ "EXTENDS",
3091
+ "IMPLEMENTS",
3092
+ "HAS_MEMBER",
3093
+ "ACCESSES",
3094
+ "OVERRIDES",
3095
+ "BELONGS_TO",
3096
+ "STEP_OF",
3097
+ "HANDLES",
3098
+ "CONTAINS_EDGE",
3099
+ "OUTGOING",
3100
+ "INCOMING",
3101
+ "BOTH"
3102
+ ]);
3103
+ Parser2 = class {
3104
+ tokens;
3105
+ pos = 0;
3106
+ constructor(tokens) {
3107
+ this.tokens = tokens;
3108
+ }
3109
+ peek() {
3110
+ return this.tokens[this.pos];
3111
+ }
3112
+ consume() {
3113
+ return this.tokens[this.pos++];
3114
+ }
3115
+ expect(kind, value) {
3116
+ const tok = this.peek();
3117
+ if (tok.kind !== kind) {
3118
+ return {
3119
+ type: "GQLParseError",
3120
+ message: `Expected ${value ?? kind} but got '${tok.value}' at position ${tok.pos}`,
3121
+ pos: tok.pos,
3122
+ expected: value ?? kind,
3123
+ got: tok.value
3124
+ };
3125
+ }
3126
+ if (value !== void 0 && tok.value !== value) {
3127
+ return {
3128
+ type: "GQLParseError",
3129
+ message: `Expected '${value}' but got '${tok.value}' at position ${tok.pos}`,
3130
+ pos: tok.pos,
3131
+ expected: value,
3132
+ got: tok.value
3133
+ };
3134
+ }
3135
+ return this.consume();
3136
+ }
3137
+ matchKeyword(...values) {
3138
+ const tok = this.peek();
3139
+ return tok.kind === "KEYWORD" && values.includes(tok.value);
3140
+ }
3141
+ optionalKeyword(...values) {
3142
+ if (this.matchKeyword(...values)) {
3143
+ return this.consume();
3144
+ }
3145
+ return null;
3146
+ }
3147
+ /** Parse the node kind filter (IDENT, KEYWORD that's a kind, or STAR) */
3148
+ parseNodeKind() {
3149
+ const tok = this.peek();
3150
+ if (tok.kind === "STAR") {
3151
+ this.consume();
3152
+ return "*";
3153
+ }
3154
+ if (tok.kind === "IDENT" || tok.kind === "KEYWORD") {
3155
+ this.consume();
3156
+ return tok.value.toLowerCase();
3157
+ }
3158
+ return {
3159
+ type: "GQLParseError",
3160
+ message: `Expected node kind or '*' at position ${tok.pos}`,
3161
+ pos: tok.pos
3162
+ };
3163
+ }
3164
+ /** Parse a string value (STRING or IDENT) */
3165
+ parseStringValue() {
3166
+ const tok = this.peek();
3167
+ if (tok.kind === "STRING") {
3168
+ this.consume();
3169
+ return tok.value;
3170
+ }
3171
+ if (tok.kind === "IDENT" || tok.kind === "KEYWORD") {
3172
+ this.consume();
3173
+ return tok.value;
3174
+ }
3175
+ return {
3176
+ type: "GQLParseError",
3177
+ message: `Expected string value at position ${tok.pos}`,
3178
+ pos: tok.pos
3179
+ };
3180
+ }
3181
+ /** Parse an IN list: [ value, value, ... ] */
3182
+ parseInList() {
3183
+ const lb = this.expect("LBRACKET");
3184
+ if (isGQLParseError(lb)) return lb;
3185
+ const values = [];
3186
+ while (!this.matchKeyword() && this.peek().kind !== "RBRACKET" && this.peek().kind !== "EOF") {
3187
+ const v = this.parseStringValue();
3188
+ if (typeof v !== "string") return v;
3189
+ values.push(v);
3190
+ }
3191
+ const rb = this.expect("RBRACKET");
3192
+ if (isGQLParseError(rb)) return rb;
3193
+ return values;
3194
+ }
3195
+ /** Parse a single WHERE expression */
3196
+ parseWhereExpr() {
3197
+ const propTok = this.peek();
3198
+ if (propTok.kind !== "IDENT" && propTok.kind !== "KEYWORD") {
3199
+ return {
3200
+ type: "GQLParseError",
3201
+ message: `Expected property name at position ${propTok.pos}`,
3202
+ pos: propTok.pos
3203
+ };
3204
+ }
3205
+ this.consume();
3206
+ const property = propTok.value.toLowerCase();
3207
+ const opTok = this.peek();
3208
+ if (opTok.kind !== "OPERATOR") {
3209
+ return {
3210
+ type: "GQLParseError",
3211
+ message: `Expected operator (=, !=, CONTAINS, STARTS_WITH, IN) at position ${opTok.pos}`,
3212
+ pos: opTok.pos,
3213
+ expected: "operator",
3214
+ got: opTok.value
3215
+ };
3216
+ }
3217
+ this.consume();
3218
+ const operator = opTok.value;
3219
+ if (operator === "IN") {
3220
+ const list = this.parseInList();
3221
+ if (!Array.isArray(list)) return list;
3222
+ return { property, operator, value: list };
3223
+ }
3224
+ const val = this.parseStringValue();
3225
+ if (typeof val !== "string") return val;
3226
+ return { property, operator, value: val };
3227
+ }
3228
+ /** Parse WHERE clause: WHERE expr (AND expr)* */
3229
+ parseWhereClause() {
3230
+ const kw = this.expect("KEYWORD", "WHERE");
3231
+ if (isGQLParseError(kw)) return kw;
3232
+ const exprs = [];
3233
+ const first = this.parseWhereExpr();
3234
+ if ("type" in first && first.type === "GQLParseError") return first;
3235
+ exprs.push(first);
3236
+ while (this.matchKeyword("AND")) {
3237
+ this.consume();
3238
+ const expr = this.parseWhereExpr();
3239
+ if ("type" in expr && expr.type === "GQLParseError") return expr;
3240
+ exprs.push(expr);
3241
+ }
3242
+ return { exprs };
3243
+ }
3244
+ /** Parse FIND statement */
3245
+ parseFindStatement() {
3246
+ this.consume();
3247
+ const kind = this.parseNodeKind();
3248
+ if (typeof kind !== "string") return kind;
3249
+ let where;
3250
+ if (this.matchKeyword("WHERE")) {
3251
+ const w = this.parseWhereClause();
3252
+ if ("type" in w && w.type === "GQLParseError") return w;
3253
+ where = w;
3254
+ }
3255
+ let limit;
3256
+ let offset;
3257
+ while (this.matchKeyword("LIMIT", "OFFSET")) {
3258
+ const kw = this.consume();
3259
+ const numTok = this.peek();
3260
+ if (numTok.kind !== "NUMBER") {
3261
+ return {
3262
+ type: "GQLParseError",
3263
+ message: `Expected number after ${kw.value} at position ${numTok.pos}`,
3264
+ pos: numTok.pos
3265
+ };
3266
+ }
3267
+ this.consume();
3268
+ const n = parseInt(numTok.value, 10);
3269
+ if (kw.value === "LIMIT") limit = n;
3270
+ else offset = n;
3271
+ }
3272
+ return { type: "FIND", target: kind, where, limit, offset };
3273
+ }
3274
+ /** Parse TRAVERSE statement */
3275
+ parseTraverseStatement() {
3276
+ this.consume();
3277
+ const edgeTok = this.peek();
3278
+ if (edgeTok.kind !== "KEYWORD" && edgeTok.kind !== "IDENT") {
3279
+ return {
3280
+ type: "GQLParseError",
3281
+ message: `Expected edge kind after TRAVERSE at position ${edgeTok.pos}`,
3282
+ pos: edgeTok.pos
3283
+ };
3284
+ }
3285
+ this.consume();
3286
+ const edgeKind = edgeTok.value.toLowerCase();
3287
+ const fromKw = this.expect("KEYWORD", "FROM");
3288
+ if (isGQLParseError(fromKw)) return fromKw;
3289
+ const fromVal = this.parseStringValue();
3290
+ if (typeof fromVal !== "string") return fromVal;
3291
+ let depth;
3292
+ let direction;
3293
+ if (this.matchKeyword("DEPTH")) {
3294
+ this.consume();
3295
+ const numTok = this.peek();
3296
+ if (numTok.kind !== "NUMBER") {
3297
+ return {
3298
+ type: "GQLParseError",
3299
+ message: `Expected number after DEPTH at position ${numTok.pos}`,
3300
+ pos: numTok.pos
3301
+ };
3302
+ }
3303
+ this.consume();
3304
+ depth = parseInt(numTok.value, 10);
3305
+ }
3306
+ if (this.matchKeyword("OUTGOING", "INCOMING", "BOTH")) {
3307
+ direction = this.consume().value;
3308
+ }
3309
+ return { type: "TRAVERSE", edgeKind, from: fromVal, depth, direction };
3310
+ }
3311
+ /** Parse PATH statement */
3312
+ parsePathStatement() {
3313
+ this.consume();
3314
+ const fromKw = this.expect("KEYWORD", "FROM");
3315
+ if (isGQLParseError(fromKw)) return fromKw;
3316
+ const fromVal = this.parseStringValue();
3317
+ if (typeof fromVal !== "string") return fromVal;
3318
+ const toKw = this.expect("KEYWORD", "TO");
3319
+ if (isGQLParseError(toKw)) return toKw;
3320
+ const toVal = this.parseStringValue();
3321
+ if (typeof toVal !== "string") return toVal;
3322
+ return { type: "PATH", from: fromVal, to: toVal };
3323
+ }
3324
+ /** Parse COUNT statement */
3325
+ parseCountStatement() {
3326
+ this.consume();
3327
+ const kind = this.parseNodeKind();
3328
+ if (typeof kind !== "string") return kind;
3329
+ let where;
3330
+ if (this.matchKeyword("WHERE")) {
3331
+ const w = this.parseWhereClause();
3332
+ if ("type" in w && w.type === "GQLParseError") return w;
3333
+ where = w;
3334
+ }
3335
+ let groupBy;
3336
+ if (this.matchKeyword("GROUP")) {
3337
+ this.consume();
3338
+ const byKw = this.expect("KEYWORD", "BY");
3339
+ if (isGQLParseError(byKw)) return byKw;
3340
+ const propTok = this.peek();
3341
+ if (propTok.kind !== "IDENT" && propTok.kind !== "KEYWORD") {
3342
+ return {
3343
+ type: "GQLParseError",
3344
+ message: `Expected property name after GROUP BY at position ${propTok.pos}`,
3345
+ pos: propTok.pos
3346
+ };
3347
+ }
3348
+ this.consume();
3349
+ groupBy = propTok.value.toLowerCase();
3350
+ }
3351
+ return { type: "COUNT", target: kind, where, groupBy };
3352
+ }
3353
+ parse() {
3354
+ const tok = this.peek();
3355
+ if (tok.kind !== "KEYWORD") {
3356
+ return {
3357
+ type: "GQLParseError",
3358
+ message: `Expected FIND, TRAVERSE, PATH, or COUNT at position ${tok.pos}`,
3359
+ pos: tok.pos,
3360
+ expected: "FIND | TRAVERSE | PATH | COUNT",
3361
+ got: tok.value
3362
+ };
3363
+ }
3364
+ let result;
3365
+ switch (tok.value) {
3366
+ case "FIND":
3367
+ result = this.parseFindStatement();
3368
+ break;
3369
+ case "TRAVERSE":
3370
+ result = this.parseTraverseStatement();
3371
+ break;
3372
+ case "PATH":
3373
+ result = this.parsePathStatement();
3374
+ break;
3375
+ case "COUNT":
3376
+ result = this.parseCountStatement();
3377
+ break;
3378
+ default:
3379
+ return {
3380
+ type: "GQLParseError",
3381
+ message: `Unknown statement type '${tok.value}' at position ${tok.pos}`,
3382
+ pos: tok.pos,
3383
+ expected: "FIND | TRAVERSE | PATH | COUNT",
3384
+ got: tok.value
3385
+ };
3386
+ }
3387
+ if (isGQLParseError(result)) return result;
3388
+ const remaining = this.peek();
3389
+ if (remaining.kind !== "EOF") {
3390
+ return {
3391
+ type: "GQLParseError",
3392
+ message: `Unexpected token '${remaining.value}' at position ${remaining.pos}`,
3393
+ pos: remaining.pos,
3394
+ got: remaining.value
3395
+ };
3396
+ }
3397
+ return result;
3398
+ }
3399
+ };
3400
+ }
3401
+ });
3402
+
3403
+ // src/query/gql-executor.ts
3404
+ var gql_executor_exports = {};
3405
+ __export(gql_executor_exports, {
3406
+ executeGQL: () => executeGQL
3407
+ });
3408
+ function getNodeProperty(node, property) {
3409
+ switch (property) {
3410
+ case "name":
3411
+ return node.name;
3412
+ case "kind":
3413
+ return node.kind;
3414
+ case "filepath":
3415
+ case "filePath":
3416
+ return node.filePath;
3417
+ case "exported":
3418
+ return node.exported;
3419
+ case "language":
3420
+ return node.metadata?.language ?? void 0;
3421
+ case "cluster":
3422
+ return node.metadata?.cluster ?? void 0;
3423
+ default:
3424
+ return node.metadata?.[property] ?? void 0;
3425
+ }
3426
+ }
3427
+ function evaluateExpr(node, expr) {
3428
+ const val = getNodeProperty(node, expr.property);
3429
+ if (val === void 0) return false;
3430
+ const strVal = String(val).toLowerCase();
3431
+ switch (expr.operator) {
3432
+ case "=":
3433
+ if (typeof expr.value === "string") {
3434
+ return strVal === expr.value.toLowerCase();
3435
+ }
3436
+ return false;
3437
+ case "!=":
3438
+ if (typeof expr.value === "string") {
3439
+ return strVal !== expr.value.toLowerCase();
3440
+ }
3441
+ return true;
3442
+ case "CONTAINS":
3443
+ if (typeof expr.value === "string") {
3444
+ return strVal.includes(expr.value.toLowerCase());
3445
+ }
3446
+ return false;
3447
+ case "STARTS_WITH":
3448
+ if (typeof expr.value === "string") {
3449
+ return strVal.startsWith(expr.value.toLowerCase());
3450
+ }
3451
+ return false;
3452
+ case "IN":
3453
+ if (Array.isArray(expr.value)) {
3454
+ return expr.value.some((v) => strVal === v.toLowerCase());
3455
+ }
3456
+ return false;
3457
+ default:
3458
+ return false;
3459
+ }
3460
+ }
3461
+ function evaluateWhere(node, where) {
3462
+ return where.exprs.every((expr) => evaluateExpr(node, expr));
3463
+ }
3464
+ function executeFIND(stmt, graph) {
3465
+ const start = Date.now();
3466
+ const limit = stmt.limit ?? 1e3;
3467
+ const offset = stmt.offset ?? 0;
3468
+ let totalCount = 0;
3469
+ let truncated = false;
3470
+ const allMatching = [];
3471
+ const deadline = start + EXECUTION_TIMEOUT_MS;
3472
+ for (const node of graph.allNodes()) {
3473
+ if (Date.now() > deadline) {
3474
+ truncated = true;
3475
+ break;
3476
+ }
3477
+ if (stmt.target !== "*" && node.kind !== stmt.target) continue;
3478
+ if (stmt.where && !evaluateWhere(node, stmt.where)) continue;
3479
+ allMatching.push(node);
3480
+ }
3481
+ totalCount = allMatching.length;
3482
+ const paginated = allMatching.slice(offset, offset + limit);
3483
+ return {
3484
+ nodes: paginated,
3485
+ executionTimeMs: Date.now() - start,
3486
+ truncated,
3487
+ totalCount
3488
+ };
3489
+ }
3490
+ function executeTRAVERSE(stmt, graph) {
3491
+ const start = Date.now();
3492
+ const maxDepth = stmt.depth ?? 5;
3493
+ const edgeKind = stmt.edgeKind;
3494
+ const direction = stmt.direction ?? "OUTGOING";
3495
+ const deadline = start + EXECUTION_TIMEOUT_MS;
3496
+ let startNode;
3497
+ for (const node of graph.allNodes()) {
3498
+ if (node.name === stmt.from) {
3499
+ startNode = node;
3500
+ break;
3501
+ }
3502
+ }
3503
+ if (!startNode) {
3504
+ return {
3505
+ nodes: [],
3506
+ edges: [],
3507
+ executionTimeMs: Date.now() - start,
3508
+ truncated: false,
3509
+ totalCount: 0
3510
+ };
3511
+ }
3512
+ const visitedNodes = /* @__PURE__ */ new Set();
3513
+ const visitedEdges = /* @__PURE__ */ new Set();
3514
+ const resultNodes = [];
3515
+ const resultEdges = [];
3516
+ const queue = [{ id: startNode.id, depth: 0 }];
3517
+ visitedNodes.add(startNode.id);
3518
+ resultNodes.push(startNode);
3519
+ let truncated = false;
3520
+ while (queue.length > 0) {
3521
+ if (Date.now() > deadline) {
3522
+ truncated = true;
3523
+ break;
3524
+ }
3525
+ const { id, depth } = queue.shift();
3526
+ if (depth >= maxDepth) continue;
3527
+ const nextEdges = [];
3528
+ if (direction === "OUTGOING" || direction === "BOTH") {
3529
+ for (const edge of graph.findEdgesFrom(id)) {
3530
+ if (!edgeKind || edge.kind === edgeKind) nextEdges.push(edge);
3531
+ }
3532
+ }
3533
+ if (direction === "INCOMING" || direction === "BOTH") {
3534
+ for (const edge of graph.findEdgesTo(id)) {
3535
+ if (!edgeKind || edge.kind === edgeKind) nextEdges.push(edge);
3536
+ }
3537
+ }
3538
+ for (const edge of nextEdges) {
3539
+ if (!visitedEdges.has(edge.id)) {
3540
+ visitedEdges.add(edge.id);
3541
+ resultEdges.push(edge);
3542
+ }
3543
+ const neighborId = direction === "INCOMING" ? edge.source : edge.target;
3544
+ const effectiveNeighborId = direction === "BOTH" ? edge.source === id ? edge.target : edge.source : neighborId;
3545
+ if (!visitedNodes.has(effectiveNeighborId)) {
3546
+ visitedNodes.add(effectiveNeighborId);
3547
+ const neighborNode = graph.getNode(effectiveNeighborId);
3548
+ if (neighborNode) {
3549
+ resultNodes.push(neighborNode);
3550
+ queue.push({ id: effectiveNeighborId, depth: depth + 1 });
3551
+ }
3552
+ }
3553
+ }
3554
+ }
3555
+ return {
3556
+ nodes: resultNodes,
3557
+ edges: resultEdges,
3558
+ executionTimeMs: Date.now() - start,
3559
+ truncated,
3560
+ totalCount: resultNodes.length
3561
+ };
3562
+ }
3563
+ function executePATH(stmt, graph) {
3564
+ const start = Date.now();
3565
+ const deadline = start + EXECUTION_TIMEOUT_MS;
3566
+ let startNode;
3567
+ let endNode;
3568
+ for (const node of graph.allNodes()) {
3569
+ if (node.name === stmt.from) startNode = node;
3570
+ if (node.name === stmt.to) endNode = node;
3571
+ if (startNode && endNode) break;
3572
+ }
3573
+ if (!startNode || !endNode) {
3574
+ return {
3575
+ path: null,
3576
+ nodes: [],
3577
+ executionTimeMs: Date.now() - start,
3578
+ truncated: false,
3579
+ totalCount: 0
3580
+ };
3581
+ }
3582
+ const visited = /* @__PURE__ */ new Set();
3583
+ const parent = /* @__PURE__ */ new Map();
3584
+ const queue = [startNode.id];
3585
+ visited.add(startNode.id);
3586
+ let found = false;
3587
+ let truncated = false;
3588
+ outer: while (queue.length > 0) {
3589
+ if (Date.now() > deadline) {
3590
+ truncated = true;
3591
+ break;
3592
+ }
3593
+ const current2 = queue.shift();
3594
+ for (const edge of graph.findEdgesFrom(current2)) {
3595
+ const next = edge.target;
3596
+ if (!visited.has(next)) {
3597
+ visited.add(next);
3598
+ parent.set(next, { nodeId: current2, edgeId: edge.id });
3599
+ if (next === endNode.id) {
3600
+ found = true;
3601
+ break outer;
3602
+ }
3603
+ queue.push(next);
3604
+ }
3605
+ }
3606
+ for (const edge of graph.findEdgesTo(current2)) {
3607
+ const next = edge.source;
3608
+ if (!visited.has(next)) {
3609
+ visited.add(next);
3610
+ parent.set(next, { nodeId: current2, edgeId: edge.id });
3611
+ if (next === endNode.id) {
3612
+ found = true;
3613
+ break outer;
3614
+ }
3615
+ queue.push(next);
3616
+ }
3617
+ }
3618
+ }
3619
+ if (!found) {
3620
+ return {
3621
+ path: null,
3622
+ nodes: [],
3623
+ executionTimeMs: Date.now() - start,
3624
+ truncated,
3625
+ totalCount: 0
3626
+ };
3627
+ }
3628
+ const pathNodeIds = [];
3629
+ const pathEdgeIds = [];
3630
+ let current = endNode.id;
3631
+ while (current !== startNode.id) {
3632
+ pathNodeIds.unshift(current);
3633
+ const p = parent.get(current);
3634
+ pathEdgeIds.unshift(p.edgeId);
3635
+ current = p.nodeId;
3636
+ }
3637
+ pathNodeIds.unshift(startNode.id);
3638
+ const pathNodes = pathNodeIds.map((id) => graph.getNode(id)).filter(Boolean);
3639
+ const pathEdges = pathEdgeIds.map((id) => graph.getEdge(id)).filter(Boolean);
3640
+ return {
3641
+ path: pathNodes,
3642
+ nodes: pathNodes,
3643
+ edges: pathEdges,
3644
+ executionTimeMs: Date.now() - start,
3645
+ truncated,
3646
+ totalCount: pathNodes.length
3647
+ };
3648
+ }
3649
+ function executeCOUNT(stmt, graph) {
3650
+ const start = Date.now();
3651
+ const deadline = start + EXECUTION_TIMEOUT_MS;
3652
+ let truncated = false;
3653
+ const groups = /* @__PURE__ */ new Map();
3654
+ let total = 0;
3655
+ for (const node of graph.allNodes()) {
3656
+ if (Date.now() > deadline) {
3657
+ truncated = true;
3658
+ break;
3659
+ }
3660
+ if (stmt.target !== "*" && node.kind !== stmt.target) continue;
3661
+ if (stmt.where && !evaluateWhere(node, stmt.where)) continue;
3662
+ total++;
3663
+ if (stmt.groupBy) {
3664
+ const key = String(getNodeProperty(node, stmt.groupBy) ?? "(none)");
3665
+ groups.set(key, (groups.get(key) ?? 0) + 1);
3666
+ } else {
3667
+ groups.set("total", (groups.get("total") ?? 0) + 1);
3668
+ }
3669
+ }
3670
+ const groupList = [...groups.entries()].map(([key, count]) => ({ key, count }));
3671
+ groupList.sort((a, b) => b.count - a.count);
3672
+ return {
3673
+ groups: groupList,
3674
+ executionTimeMs: Date.now() - start,
3675
+ truncated,
3676
+ totalCount: total
3677
+ };
3678
+ }
3679
+ function executeGQL(ast, graph) {
3680
+ switch (ast.type) {
3681
+ case "FIND":
3682
+ return executeFIND(ast, graph);
3683
+ case "TRAVERSE":
3684
+ return executeTRAVERSE(ast, graph);
3685
+ case "PATH":
3686
+ return executePATH(ast, graph);
3687
+ case "COUNT":
3688
+ return executeCOUNT(ast, graph);
3689
+ default:
3690
+ return {
3691
+ nodes: [],
3692
+ executionTimeMs: 0,
3693
+ truncated: false,
3694
+ totalCount: 0
3695
+ };
3696
+ }
3697
+ }
3698
+ var EXECUTION_TIMEOUT_MS;
3699
+ var init_gql_executor = __esm({
3700
+ "src/query/gql-executor.ts"() {
3701
+ EXECUTION_TIMEOUT_MS = 1e4;
3702
+ }
3703
+ });
3704
+
2957
3705
  // src/errors/codes.ts
2958
3706
  var ErrorCodes, AppError;
2959
3707
  var init_codes = __esm({
@@ -3008,7 +3756,7 @@ function tightenDbFiles(dir) {
3008
3756
  for (const name of fs19.readdirSync(dir)) {
3009
3757
  if (name.endsWith(".db") || name.endsWith(".db-wal") || name.endsWith(".db-shm")) {
3010
3758
  try {
3011
- fs19.chmodSync(path26.join(dir, name), SECURE_FILE_MODE);
3759
+ fs19.chmodSync(path27.join(dir, name), SECURE_FILE_MODE);
3012
3760
  } catch {
3013
3761
  }
3014
3762
  }
@@ -3022,7 +3770,7 @@ var init_fs_secure = __esm({
3022
3770
  }
3023
3771
  });
3024
3772
  function getUsersDBPath() {
3025
- return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path26.join(os12.homedir(), ".code-intel", "users.db");
3773
+ return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path27.join(os12.homedir(), ".code-intel", "users.db");
3026
3774
  }
3027
3775
  function getOrCreateUsersDB() {
3028
3776
  if (!_usersDB) {
@@ -3038,7 +3786,7 @@ var init_users_db = __esm({
3038
3786
  UsersDB = class {
3039
3787
  db;
3040
3788
  constructor(dbPath) {
3041
- const dir = path26.dirname(dbPath);
3789
+ const dir = path27.dirname(dbPath);
3042
3790
  secureMkdir(dir);
3043
3791
  this.db = new Database3(dbPath);
3044
3792
  this.db.pragma("journal_mode = WAL");
@@ -3315,7 +4063,7 @@ function getScryptN() {
3315
4063
  return Number.isInteger(v) && v >= 1024 ? v : 1 << 14;
3316
4064
  }
3317
4065
  function getSecretsPath() {
3318
- return process.env["CODE_INTEL_SECRETS_PATH"] ?? path26.join(os12.homedir(), ".code-intel", ".secrets");
4066
+ return process.env["CODE_INTEL_SECRETS_PATH"] ?? path27.join(os12.homedir(), ".code-intel", ".secrets");
3319
4067
  }
3320
4068
  function getMasterPassword() {
3321
4069
  const fromEnv = process.env["CODE_INTEL_SECRET_KEY"];
@@ -3904,7 +4652,7 @@ init_shared();
3904
4652
  init_shared();
3905
4653
  init_typescript();
3906
4654
  function resolveRelative(rawPath, fromFile, workspace) {
3907
- const fromDir = path26.dirname(fromFile);
4655
+ const fromDir = path27.dirname(fromFile);
3908
4656
  const cleaned = rawPath.replace(/['"]/g, "");
3909
4657
  const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
3910
4658
  const resolved = workspace.resolve(fromDir, cleaned);
@@ -3956,7 +4704,7 @@ var pythonModule = {
3956
4704
  resolveImport(rawPath, fromFile, workspace) {
3957
4705
  const cleaned = rawPath.replace(/['"]/g, "");
3958
4706
  const parts = cleaned.split(".");
3959
- const fromDir = path26.dirname(fromFile);
4707
+ const fromDir = path27.dirname(fromFile);
3960
4708
  const relPath = parts.join("/");
3961
4709
  for (const suffix of ["/__init__.py", ".py"]) {
3962
4710
  const r = workspace.resolve(fromDir, relPath + suffix);
@@ -4035,7 +4783,7 @@ var cModule = {
4035
4783
  inheritanceStrategy: "none",
4036
4784
  resolveImport(rawPath, fromFile, workspace) {
4037
4785
  const cleaned = rawPath.replace(/[<>"']/g, "");
4038
- const fromDir = path26.dirname(fromFile);
4786
+ const fromDir = path27.dirname(fromFile);
4039
4787
  return workspace.resolve(fromDir, cleaned);
4040
4788
  },
4041
4789
  isExported(_node) {
@@ -4058,7 +4806,7 @@ var cppModule = {
4058
4806
  inheritanceStrategy: "depth-first",
4059
4807
  resolveImport(rawPath, fromFile, workspace) {
4060
4808
  const cleaned = rawPath.replace(/[<>"']/g, "");
4061
- const fromDir = path26.dirname(fromFile);
4809
+ const fromDir = path27.dirname(fromFile);
4062
4810
  return workspace.resolve(fromDir, cleaned);
4063
4811
  },
4064
4812
  isExported(_node) {
@@ -4220,7 +4968,7 @@ var dartModule = {
4220
4968
  const pkg = cleaned.replace("package:", "");
4221
4969
  return workspace.findByPackage(pkg);
4222
4970
  }
4223
- const fromDir = path26.dirname(fromFile);
4971
+ const fromDir = path27.dirname(fromFile);
4224
4972
  return workspace.resolve(fromDir, cleaned);
4225
4973
  },
4226
4974
  isExported(node) {
@@ -4806,7 +5554,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
4806
5554
  ]);
4807
5555
  function loadIgnorePatterns(workspaceRoot) {
4808
5556
  try {
4809
- const raw = fs19.readFileSync(path26.join(workspaceRoot, ".codeintelignore"), "utf-8");
5557
+ const raw = fs19.readFileSync(path27.join(workspaceRoot, ".codeintelignore"), "utf-8");
4810
5558
  const extras = /* @__PURE__ */ new Set();
4811
5559
  for (const line of raw.split("\n")) {
4812
5560
  const trimmed = line.trim();
@@ -4839,13 +5587,13 @@ var scanPhase = {
4839
5587
  if (entry.name.startsWith(".")) continue;
4840
5588
  if (IGNORED_DIRS.has(entry.name)) continue;
4841
5589
  if (extraIgnore.has(entry.name)) continue;
4842
- walk(path26.join(dir, entry.name));
5590
+ walk(path27.join(dir, entry.name));
4843
5591
  } else if (entry.isFile()) {
4844
5592
  const name = entry.name;
4845
5593
  if (IGNORED_FILE_SUFFIXES.some((s) => name.endsWith(s))) continue;
4846
- const ext = path26.extname(name);
5594
+ const ext = path27.extname(name);
4847
5595
  if (!extensions.has(ext)) continue;
4848
- const fullPath = path26.join(dir, name);
5596
+ const fullPath = path27.join(dir, name);
4849
5597
  try {
4850
5598
  const stat = fs19.statSync(fullPath);
4851
5599
  if (stat.size > MAX_FILE_SIZE_BYTES) continue;
@@ -4874,20 +5622,20 @@ var structurePhase = {
4874
5622
  const dirs = /* @__PURE__ */ new Set();
4875
5623
  let structDone = 0;
4876
5624
  for (const filePath of context2.filePaths) {
4877
- const relativePath = path26.relative(context2.workspaceRoot, filePath);
5625
+ const relativePath = path27.relative(context2.workspaceRoot, filePath);
4878
5626
  const lang = detectLanguage(filePath);
4879
5627
  context2.graph.addNode({
4880
5628
  id: generateNodeId("file", relativePath, relativePath),
4881
5629
  kind: "file",
4882
- name: path26.basename(filePath),
5630
+ name: path27.basename(filePath),
4883
5631
  filePath: relativePath,
4884
5632
  metadata: lang ? { language: lang } : void 0
4885
5633
  });
4886
- let dir = path26.dirname(relativePath);
5634
+ let dir = path27.dirname(relativePath);
4887
5635
  while (dir && dir !== "." && dir !== "") {
4888
5636
  if (dirs.has(dir)) break;
4889
5637
  dirs.add(dir);
4890
- dir = path26.dirname(dir);
5638
+ dir = path27.dirname(dir);
4891
5639
  }
4892
5640
  structDone++;
4893
5641
  context2.onPhaseProgress?.("structure", structDone, context2.filePaths.length);
@@ -4896,7 +5644,7 @@ var structurePhase = {
4896
5644
  context2.graph.addNode({
4897
5645
  id: generateNodeId("directory", dir, dir),
4898
5646
  kind: "directory",
4899
- name: path26.basename(dir),
5647
+ name: path27.basename(dir),
4900
5648
  filePath: dir
4901
5649
  });
4902
5650
  }
@@ -5053,7 +5801,7 @@ var LLMGovernanceLogger = class {
5053
5801
  }
5054
5802
  /** Path to the JSONL log file. */
5055
5803
  getLogPath() {
5056
- return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path26.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
5804
+ return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path27.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
5057
5805
  }
5058
5806
  /**
5059
5807
  * Append an entry to the governance log.
@@ -5069,7 +5817,7 @@ var LLMGovernanceLogger = class {
5069
5817
  ...entry
5070
5818
  };
5071
5819
  const logPath = this.getLogPath();
5072
- fs19.mkdirSync(path26.dirname(logPath), { recursive: true });
5820
+ fs19.mkdirSync(path27.dirname(logPath), { recursive: true });
5073
5821
  fs19.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
5074
5822
  } catch {
5075
5823
  }
@@ -5525,7 +6273,7 @@ var DbManager = class {
5525
6273
  this.dbPath = dbPath;
5526
6274
  }
5527
6275
  async init() {
5528
- fs19.mkdirSync(path26.dirname(this.dbPath), { recursive: true });
6276
+ fs19.mkdirSync(path27.dirname(this.dbPath), { recursive: true });
5529
6277
  this.db = new Database(this.dbPath);
5530
6278
  await this.db.init();
5531
6279
  this.conn = new Connection(this.db);
@@ -5582,7 +6330,8 @@ var NODE_TABLE_MAP = {
5582
6330
  constant: "const_nodes",
5583
6331
  route: "route_nodes",
5584
6332
  cluster: "cluster_nodes",
5585
- flow: "flow_nodes"
6333
+ flow: "flow_nodes",
6334
+ vulnerability: "vuln_nodes"
5586
6335
  };
5587
6336
  var ALL_NODE_TABLES = [...new Set(Object.values(NODE_TABLE_MAP))];
5588
6337
  function getCreateNodeTableDDL(tableName) {
@@ -5622,7 +6371,7 @@ function writeNodeCSVs(graph, outputDir) {
5622
6371
  const table = NODE_TABLE_MAP[node.kind];
5623
6372
  if (!tableBuffers.has(table)) {
5624
6373
  tableBuffers.set(table, [header]);
5625
- tableFilePaths.set(table, path26.join(outputDir, `${table}.csv`));
6374
+ tableFilePaths.set(table, path27.join(outputDir, `${table}.csv`));
5626
6375
  }
5627
6376
  tableBuffers.get(table).push(
5628
6377
  csvRow([
@@ -5658,7 +6407,7 @@ function writeEdgeCSV(graph, outputDir) {
5658
6407
  const toTable = NODE_TABLE_MAP[targetNode.kind];
5659
6408
  const key = `${fromTable}->${toTable}`;
5660
6409
  if (!groups.has(key)) {
5661
- const filePath = path26.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
6410
+ const filePath = path27.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
5662
6411
  groups.set(key, { lines: [header], from: fromTable, to: toTable, filePath });
5663
6412
  }
5664
6413
  groups.get(key).lines.push(
@@ -5701,7 +6450,7 @@ async function loadGraphToDB(graph, dbManager) {
5701
6450
  } catch {
5702
6451
  }
5703
6452
  }
5704
- const tmpDir = fs19.mkdtempSync(path26.join(os12.tmpdir(), "code-intel-csv-"));
6453
+ const tmpDir = fs19.mkdtempSync(path27.join(os12.tmpdir(), "code-intel-csv-"));
5705
6454
  try {
5706
6455
  const nodeTableFiles = writeNodeCSVs(graph, tmpDir);
5707
6456
  const edgeGroups = writeEdgeCSV(graph, tmpDir);
@@ -5801,8 +6550,8 @@ function buildNodeProps(node) {
5801
6550
  function escCypher(s) {
5802
6551
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
5803
6552
  }
5804
- var GLOBAL_DIR = path26.join(os12.homedir(), ".code-intel");
5805
- var REPOS_FILE = path26.join(GLOBAL_DIR, "repos.json");
6553
+ var GLOBAL_DIR = path27.join(os12.homedir(), ".code-intel");
6554
+ var REPOS_FILE = path27.join(GLOBAL_DIR, "repos.json");
5806
6555
  function loadRegistry() {
5807
6556
  try {
5808
6557
  const data = fs19.readFileSync(REPOS_FILE, "utf-8");
@@ -5830,23 +6579,23 @@ function removeRepo(repoPath) {
5830
6579
  saveRegistry(entries);
5831
6580
  }
5832
6581
  function saveMetadata(repoDir, metadata) {
5833
- const metaDir = path26.join(repoDir, ".code-intel");
6582
+ const metaDir = path27.join(repoDir, ".code-intel");
5834
6583
  fs19.mkdirSync(metaDir, { recursive: true });
5835
- fs19.writeFileSync(path26.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
6584
+ fs19.writeFileSync(path27.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
5836
6585
  }
5837
6586
  function loadMetadata(repoDir) {
5838
6587
  try {
5839
- const data = fs19.readFileSync(path26.join(repoDir, ".code-intel", "meta.json"), "utf-8");
6588
+ const data = fs19.readFileSync(path27.join(repoDir, ".code-intel", "meta.json"), "utf-8");
5840
6589
  return JSON.parse(data);
5841
6590
  } catch {
5842
6591
  return null;
5843
6592
  }
5844
6593
  }
5845
6594
  function getDbPath(repoDir) {
5846
- return path26.join(repoDir, ".code-intel", "graph.db");
6595
+ return path27.join(repoDir, ".code-intel", "graph.db");
5847
6596
  }
5848
6597
  function getVectorDbPath(repoDir) {
5849
- return path26.join(repoDir, ".code-intel", "vector.db");
6598
+ return path27.join(repoDir, ".code-intel", "vector.db");
5850
6599
  }
5851
6600
 
5852
6601
  // src/mcp-server/server.ts
@@ -6045,7 +6794,7 @@ async function syncGroup(group) {
6045
6794
  logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
6046
6795
  continue;
6047
6796
  }
6048
- const dbPath = path26.join(regEntry.path, ".code-intel", "graph.db");
6797
+ const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
6049
6798
  if (!fs19.existsSync(dbPath)) {
6050
6799
  logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
6051
6800
  continue;
@@ -6081,7 +6830,7 @@ async function queryGroup(group, query, limit = 20) {
6081
6830
  for (const member of group.members) {
6082
6831
  const regEntry = registry.find((r) => r.name === member.registryName);
6083
6832
  if (!regEntry) continue;
6084
- const dbPath = path26.join(regEntry.path, ".code-intel", "graph.db");
6833
+ const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
6085
6834
  if (!fs19.existsSync(dbPath)) continue;
6086
6835
  const graph = createKnowledgeGraph();
6087
6836
  const db = new DbManager(dbPath);
@@ -6112,65 +6861,684 @@ async function queryGroup(group, query, limit = 20) {
6112
6861
 
6113
6862
  // src/mcp-server/server.ts
6114
6863
  init_tracing();
6115
- function createMcpServer(graph, repoName, workspaceRoot) {
6116
- const server = new Server(
6117
- { name: "code-intel", version: "0.1.0" },
6118
- { capabilities: { tools: {}, resources: {} } }
6119
- );
6120
- const _tokenProp = {
6121
- _token: { type: "string", description: "Required if CODE_INTEL_TOKEN is configured" }
6122
- };
6123
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
6124
- tools: [
6125
- // ── Core repo tools ──────────────────────────────────────────────────
6126
- {
6127
- name: "repos",
6128
- description: "List all indexed repositories with node and edge counts",
6129
- inputSchema: { type: "object", properties: { ..._tokenProp } }
6130
- },
6131
- {
6132
- name: "overview",
6133
- 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.",
6134
- inputSchema: { type: "object", properties: { ..._tokenProp } }
6135
- },
6136
- // ── Search & inspect ─────────────────────────────────────────────────
6137
- {
6138
- name: "search",
6139
- description: "BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc.",
6140
- inputSchema: {
6141
- type: "object",
6142
- properties: {
6143
- query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
6144
- limit: { type: "number", description: "Max results to return (default: 20)" },
6145
- ..._tokenProp
6146
- },
6147
- required: ["query"]
6864
+
6865
+ // src/query/explain-relationship.ts
6866
+ function explainRelationship(graph, from, to) {
6867
+ const allNodes = [...graph.allNodes()];
6868
+ const fromNode = allNodes.find((n) => n.name === from);
6869
+ if (!fromNode) {
6870
+ const firstChar = from[0]?.toLowerCase() ?? "";
6871
+ const fromLower = from.toLowerCase();
6872
+ const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(fromLower)).slice(0, 5).map((n) => n.name);
6873
+ return { error: `Symbol not found: ${from}`, suggestions };
6874
+ }
6875
+ const toNode = allNodes.find((n) => n.name === to);
6876
+ if (!toNode) {
6877
+ const firstChar = to[0]?.toLowerCase() ?? "";
6878
+ const toLower = to.toLowerCase();
6879
+ const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(toLower)).slice(0, 5).map((n) => n.name);
6880
+ return { error: `Symbol not found: ${to}`, suggestions };
6881
+ }
6882
+ const paths = [];
6883
+ const queue = [{
6884
+ id: fromNode.id,
6885
+ nodeNames: [fromNode.name],
6886
+ lastEdgeKind: "",
6887
+ visited: /* @__PURE__ */ new Set([fromNode.id])
6888
+ }];
6889
+ while (queue.length > 0 && paths.length < 10) {
6890
+ const entry = queue.shift();
6891
+ const { id, nodeNames, visited } = entry;
6892
+ if (nodeNames.length > 6) continue;
6893
+ for (const edge of graph.findEdgesFrom(id)) {
6894
+ const targetNode = graph.getNode(edge.target);
6895
+ if (!targetNode) continue;
6896
+ if (visited.has(edge.target)) continue;
6897
+ const newNames = [...nodeNames, targetNode.name];
6898
+ if (edge.target === toNode.id) {
6899
+ paths.push({ hops: newNames.length - 1, nodes: newNames, edgeKind: edge.kind });
6900
+ if (paths.length >= 10) break;
6901
+ continue;
6902
+ }
6903
+ if (newNames.length < 6) {
6904
+ const newVisited = new Set(visited);
6905
+ newVisited.add(edge.target);
6906
+ queue.push({ id: edge.target, nodeNames: newNames, lastEdgeKind: edge.kind, visited: newVisited });
6907
+ }
6908
+ }
6909
+ }
6910
+ const fromImports = /* @__PURE__ */ new Set();
6911
+ for (const edge of graph.findEdgesFrom(fromNode.id)) {
6912
+ if (edge.kind === "imports") fromImports.add(edge.target);
6913
+ }
6914
+ const sharedImportIds = [];
6915
+ for (const edge of graph.findEdgesFrom(toNode.id)) {
6916
+ if (edge.kind === "imports" && fromImports.has(edge.target)) {
6917
+ sharedImportIds.push(edge.target);
6918
+ }
6919
+ }
6920
+ const sharedImports = sharedImportIds.map((id) => graph.getNode(id)?.name ?? id);
6921
+ let heritage = null;
6922
+ for (const edge of graph.findEdgesFrom(fromNode.id)) {
6923
+ if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === toNode.id) {
6924
+ heritage = `${from} ${edge.kind} ${to}`;
6925
+ break;
6926
+ }
6927
+ }
6928
+ if (!heritage) {
6929
+ for (const edge of graph.findEdgesFrom(toNode.id)) {
6930
+ if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === fromNode.id) {
6931
+ heritage = `${to} ${edge.kind} ${from}`;
6932
+ break;
6933
+ }
6934
+ }
6935
+ }
6936
+ const sharedStr = sharedImports.length > 0 ? sharedImports.join(", ") : "none";
6937
+ const heritageStr = heritage ?? "none";
6938
+ const connectionStr = paths.length === 0 ? "No connection found." : `${from} \u2192 ${to} via ${paths.length} path(s).`;
6939
+ const summary = `${connectionStr} Shared imports: [${sharedStr}]. Heritage: ${heritageStr}.`;
6940
+ return { paths, sharedImports, heritage, summary };
6941
+ }
6942
+
6943
+ // src/query/pr-impact.ts
6944
+ function parseDiffFiles(diff) {
6945
+ const files = [];
6946
+ for (const line of diff.split("\n")) {
6947
+ const match = line.match(/^\+\+\+ b\/(.+)/);
6948
+ if (match) {
6949
+ files.push(match[1]);
6950
+ }
6951
+ }
6952
+ return files;
6953
+ }
6954
+ function computePRImpact(graph, changedFiles, maxHops) {
6955
+ const changedSymbolIds = /* @__PURE__ */ new Set();
6956
+ for (const node of graph.allNodes()) {
6957
+ if (!node.filePath) continue;
6958
+ for (const changedFile of changedFiles) {
6959
+ if (node.filePath === changedFile || node.filePath.endsWith(changedFile) || changedFile.endsWith(node.filePath)) {
6960
+ changedSymbolIds.add(node.id);
6961
+ break;
6962
+ }
6963
+ }
6964
+ }
6965
+ const allBlastRadiusNodes = /* @__PURE__ */ new Set();
6966
+ const changedSymbols = [];
6967
+ for (const symbolId of changedSymbolIds) {
6968
+ const symbolNode = graph.getNode(symbolId);
6969
+ if (!symbolNode) continue;
6970
+ const blastRadius = /* @__PURE__ */ new Set();
6971
+ const queue = [{ id: symbolId, depth: 0 }];
6972
+ const visited = /* @__PURE__ */ new Set();
6973
+ while (queue.length > 0) {
6974
+ const { id, depth } = queue.shift();
6975
+ if (visited.has(id) || depth > maxHops) continue;
6976
+ visited.add(id);
6977
+ if (id !== symbolId) blastRadius.add(id);
6978
+ for (const edge of graph.findEdgesTo(id)) {
6979
+ if (edge.kind === "calls" || edge.kind === "imports") {
6980
+ queue.push({ id: edge.source, depth: depth + 1 });
6148
6981
  }
6149
- },
6150
- {
6151
- name: "inspect",
6152
- description: "360\xB0 view of a symbol: definition location, callers, callees, heritage (extends/implements), members, cluster, and source preview (first 500 chars)",
6153
- inputSchema: {
6154
- type: "object",
6155
- properties: {
6156
- symbol_name: { type: "string", description: "Exact symbol name to inspect" },
6157
- ..._tokenProp
6158
- },
6159
- required: ["symbol_name"]
6982
+ }
6983
+ }
6984
+ for (const id of blastRadius) allBlastRadiusNodes.add(id);
6985
+ const blastCount = blastRadius.size;
6986
+ let risk;
6987
+ if (blastCount > 50) {
6988
+ risk = "HIGH";
6989
+ } else if (blastCount >= 10) {
6990
+ risk = "MEDIUM";
6991
+ } else {
6992
+ risk = "LOW";
6993
+ }
6994
+ let callerCount = 0;
6995
+ for (const edge of graph.findEdgesTo(symbolId)) {
6996
+ if (edge.kind === "calls") callerCount++;
6997
+ }
6998
+ let testCoverage = false;
6999
+ for (const edge of graph.findEdgesTo(symbolId)) {
7000
+ if (edge.kind === "imports") {
7001
+ const callerNode = graph.getNode(edge.source);
7002
+ if (callerNode?.filePath && (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec."))) {
7003
+ testCoverage = true;
7004
+ break;
6160
7005
  }
6161
- },
6162
- {
6163
- name: "blast_radius",
6164
- 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).",
6165
- inputSchema: {
6166
- type: "object",
6167
- properties: {
6168
- target: { type: "string", description: "Target symbol name" },
6169
- direction: {
6170
- type: "string",
6171
- enum: ["callers", "callees", "both"],
6172
- description: "Which direction to trace \u2014 callers (who depends on it), callees (what it depends on), or both (default: both)"
6173
- },
7006
+ }
7007
+ }
7008
+ changedSymbols.push({ name: symbolNode.name, risk, callerCount, testCoverage });
7009
+ }
7010
+ const impactedSymbols = [];
7011
+ for (const id of allBlastRadiusNodes) {
7012
+ if (changedSymbolIds.has(id)) continue;
7013
+ const node = graph.getNode(id);
7014
+ if (node) {
7015
+ impactedSymbols.push({ name: node.name, filePath: node.filePath });
7016
+ }
7017
+ }
7018
+ const riskSummary = { HIGH: 0, MEDIUM: 0, LOW: 0 };
7019
+ for (const s of changedSymbols) {
7020
+ riskSummary[s.risk]++;
7021
+ }
7022
+ const coverageGaps = [];
7023
+ for (const s of changedSymbols) {
7024
+ if ((s.risk === "HIGH" || s.risk === "MEDIUM") && !s.testCoverage) {
7025
+ coverageGaps.push(`${s.name} has no test coverage`);
7026
+ }
7027
+ }
7028
+ const fileImpactCount = /* @__PURE__ */ new Map();
7029
+ for (const sym of impactedSymbols) {
7030
+ if (sym.filePath) {
7031
+ fileImpactCount.set(sym.filePath, (fileImpactCount.get(sym.filePath) ?? 0) + 1);
7032
+ }
7033
+ }
7034
+ const filesToReview = [...fileImpactCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([fp]) => fp);
7035
+ return {
7036
+ changedSymbols,
7037
+ impactedSymbols,
7038
+ riskSummary,
7039
+ coverageGaps,
7040
+ filesToReview,
7041
+ crossRepoImpact: null
7042
+ };
7043
+ }
7044
+
7045
+ // src/query/similar-symbols.ts
7046
+ function levenshtein(a, b) {
7047
+ const m = a.length;
7048
+ const n = b.length;
7049
+ const dp = Array.from(
7050
+ { length: m + 1 },
7051
+ (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
7052
+ );
7053
+ for (let i = 1; i <= m; i++) {
7054
+ for (let j = 1; j <= n; j++) {
7055
+ if (a[i - 1] === b[j - 1]) {
7056
+ dp[i][j] = dp[i - 1][j - 1];
7057
+ } else {
7058
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
7059
+ }
7060
+ }
7061
+ }
7062
+ return dp[m][n];
7063
+ }
7064
+ function findSimilarSymbols(graph, symbolName, limit) {
7065
+ const clampedLimit = Math.min(Math.max(1, limit), 50);
7066
+ const allNodes = [...graph.allNodes()];
7067
+ const targetNode = allNodes.find((n) => n.name === symbolName);
7068
+ if (!targetNode) {
7069
+ return { similar: [] };
7070
+ }
7071
+ let targetCluster = null;
7072
+ for (const edge of graph.findEdgesFrom(targetNode.id)) {
7073
+ if (edge.kind === "belongs_to") {
7074
+ const clusterNode = graph.getNode(edge.target);
7075
+ if (clusterNode) {
7076
+ targetCluster = clusterNode.name;
7077
+ break;
7078
+ }
7079
+ }
7080
+ }
7081
+ if (!targetCluster) {
7082
+ for (const edge of graph.findEdgesTo(targetNode.id)) {
7083
+ if (edge.kind === "belongs_to") {
7084
+ const clusterNode = graph.getNode(edge.source);
7085
+ if (clusterNode) {
7086
+ targetCluster = clusterNode.name;
7087
+ break;
7088
+ }
7089
+ }
7090
+ }
7091
+ }
7092
+ const results = [];
7093
+ for (const node of allNodes) {
7094
+ if (node.id === targetNode.id) continue;
7095
+ const maxLen = Math.max(symbolName.length, node.name.length);
7096
+ const nameSim = maxLen === 0 ? 1 : 1 - levenshtein(symbolName, node.name) / maxLen;
7097
+ const structuralSim = node.kind === targetNode.kind ? 0.5 : 0;
7098
+ const combined = 0.5 * nameSim + 0.5 * structuralSim;
7099
+ const reasons = [];
7100
+ if (nameSim >= 0.6) reasons.push("similar name");
7101
+ if (node.kind === targetNode.kind) reasons.push("same kind");
7102
+ if (targetCluster !== null) {
7103
+ let nodeCluster = null;
7104
+ for (const edge of graph.findEdgesFrom(node.id)) {
7105
+ if (edge.kind === "belongs_to") {
7106
+ const clusterNode = graph.getNode(edge.target);
7107
+ if (clusterNode) {
7108
+ nodeCluster = clusterNode.name;
7109
+ break;
7110
+ }
7111
+ }
7112
+ }
7113
+ if (!nodeCluster) {
7114
+ for (const edge of graph.findEdgesTo(node.id)) {
7115
+ if (edge.kind === "belongs_to") {
7116
+ const clusterNode = graph.getNode(edge.source);
7117
+ if (clusterNode) {
7118
+ nodeCluster = clusterNode.name;
7119
+ break;
7120
+ }
7121
+ }
7122
+ }
7123
+ }
7124
+ if (nodeCluster !== null && nodeCluster === targetCluster) {
7125
+ reasons.push("same module");
7126
+ }
7127
+ }
7128
+ if (targetNode.metadata?.["cluster"] !== void 0 && node.metadata?.["cluster"] !== void 0 && node.metadata["cluster"] === targetNode.metadata["cluster"]) {
7129
+ if (!reasons.includes("same module")) reasons.push("same module");
7130
+ }
7131
+ results.push({ name: node.name, similarity: combined, reasons });
7132
+ }
7133
+ results.sort((a, b) => b.similarity - a.similarity);
7134
+ return { similar: results.slice(0, clampedLimit) };
7135
+ }
7136
+
7137
+ // src/query/health-report.ts
7138
+ function computeHealthReport(graph, scope) {
7139
+ const wholeRepo = scope === ".";
7140
+ function inScope(filePath) {
7141
+ if (wholeRepo) return true;
7142
+ return filePath.startsWith(scope) || filePath.includes(scope);
7143
+ }
7144
+ const scopedNodes = [...graph.allNodes()].filter((n) => inScope(n.filePath));
7145
+ const deadCodeKinds = /* @__PURE__ */ new Set(["function", "method", "class"]);
7146
+ const deadCode = [];
7147
+ for (const node of scopedNodes) {
7148
+ if (!deadCodeKinds.has(node.kind)) continue;
7149
+ if (node.exported === true) continue;
7150
+ let hasIncoming = false;
7151
+ for (const _edge of graph.findEdgesTo(node.id)) {
7152
+ hasIncoming = true;
7153
+ break;
7154
+ }
7155
+ if (!hasIncoming) {
7156
+ deadCode.push({ name: node.name, filePath: node.filePath, kind: node.kind });
7157
+ if (deadCode.length >= 20) break;
7158
+ }
7159
+ }
7160
+ const cycles = [];
7161
+ const scopedNodeIds = new Set(scopedNodes.map((n) => n.id));
7162
+ const importAdj = /* @__PURE__ */ new Map();
7163
+ for (const node of scopedNodes) {
7164
+ importAdj.set(node.id, []);
7165
+ }
7166
+ for (const edge of graph.findEdgesByKind("imports")) {
7167
+ if (scopedNodeIds.has(edge.source) && scopedNodeIds.has(edge.target)) {
7168
+ importAdj.get(edge.source).push(edge.target);
7169
+ }
7170
+ }
7171
+ const visited = /* @__PURE__ */ new Set();
7172
+ const inStack = /* @__PURE__ */ new Set();
7173
+ const stackPath = [];
7174
+ function dfs(nodeId) {
7175
+ if (cycles.length >= 5) return;
7176
+ visited.add(nodeId);
7177
+ inStack.add(nodeId);
7178
+ stackPath.push(nodeId);
7179
+ for (const neighborId of importAdj.get(nodeId) ?? []) {
7180
+ if (cycles.length >= 5) break;
7181
+ if (inStack.has(neighborId)) {
7182
+ const cycleStart = stackPath.indexOf(neighborId);
7183
+ const cyclePath = stackPath.slice(cycleStart).map((id) => {
7184
+ const node = graph.getNode(id);
7185
+ return node ? node.name : id;
7186
+ });
7187
+ cycles.push(cyclePath);
7188
+ } else if (!visited.has(neighborId)) {
7189
+ dfs(neighborId);
7190
+ }
7191
+ }
7192
+ stackPath.pop();
7193
+ inStack.delete(nodeId);
7194
+ }
7195
+ for (const node of scopedNodes) {
7196
+ if (cycles.length >= 5) break;
7197
+ if (!visited.has(node.id)) {
7198
+ dfs(node.id);
7199
+ }
7200
+ }
7201
+ const godNodes = [];
7202
+ for (const node of scopedNodes) {
7203
+ let edgeCount = 0;
7204
+ for (const _edge of graph.findEdgesFrom(node.id)) {
7205
+ edgeCount++;
7206
+ }
7207
+ if (edgeCount > 10) {
7208
+ godNodes.push({ name: node.name, edgeCount, filePath: node.filePath });
7209
+ }
7210
+ }
7211
+ godNodes.sort((a, b) => b.edgeCount - a.edgeCount);
7212
+ godNodes.splice(10);
7213
+ const filePathToNodes = /* @__PURE__ */ new Map();
7214
+ for (const node of scopedNodes) {
7215
+ if (!node.filePath) continue;
7216
+ let arr = filePathToNodes.get(node.filePath);
7217
+ if (!arr) {
7218
+ arr = [];
7219
+ filePathToNodes.set(node.filePath, arr);
7220
+ }
7221
+ arr.push(node.id);
7222
+ }
7223
+ const orphanFiles = [];
7224
+ for (const [filePath, nodeIds] of filePathToNodes) {
7225
+ if (orphanFiles.length >= 10) break;
7226
+ let hasAnyEdge = false;
7227
+ for (const nodeId of nodeIds) {
7228
+ let hasOut = false;
7229
+ for (const _edge of graph.findEdgesFrom(nodeId)) {
7230
+ hasOut = true;
7231
+ break;
7232
+ }
7233
+ let hasIn = false;
7234
+ for (const _edge of graph.findEdgesTo(nodeId)) {
7235
+ hasIn = true;
7236
+ break;
7237
+ }
7238
+ if (hasOut || hasIn) {
7239
+ hasAnyEdge = true;
7240
+ break;
7241
+ }
7242
+ }
7243
+ if (!hasAnyEdge) {
7244
+ orphanFiles.push(filePath);
7245
+ }
7246
+ }
7247
+ const hotspotCandidates = [];
7248
+ for (const node of scopedNodes) {
7249
+ const visitedBfs = /* @__PURE__ */ new Set();
7250
+ const queue = [{ id: node.id, depth: 0 }];
7251
+ while (queue.length > 0) {
7252
+ const item = queue.shift();
7253
+ if (item.depth > 5 || visitedBfs.has(item.id)) continue;
7254
+ visitedBfs.add(item.id);
7255
+ for (const edge of graph.findEdgesTo(item.id)) {
7256
+ if (edge.kind === "calls" || edge.kind === "imports") {
7257
+ if (!visitedBfs.has(edge.source)) {
7258
+ queue.push({ id: edge.source, depth: item.depth + 1 });
7259
+ }
7260
+ }
7261
+ }
7262
+ }
7263
+ const blastRadius = visitedBfs.size - 1;
7264
+ hotspotCandidates.push({ name: node.name, blastRadius, filePath: node.filePath });
7265
+ }
7266
+ hotspotCandidates.sort((a, b) => b.blastRadius - a.blastRadius);
7267
+ const complexityHotspots = hotspotCandidates.slice(0, 5);
7268
+ const healthScore = Math.max(
7269
+ 0,
7270
+ Math.min(100, 100 - deadCode.length * 2 - cycles.length * 5 - godNodes.length * 3)
7271
+ );
7272
+ return {
7273
+ healthScore,
7274
+ deadCode,
7275
+ cycles,
7276
+ godNodes,
7277
+ orphanFiles,
7278
+ complexityHotspots
7279
+ };
7280
+ }
7281
+
7282
+ // src/query/suggest-tests.ts
7283
+ function getSuggestedCases(symbolName) {
7284
+ const lower = symbolName.toLowerCase();
7285
+ if (/parse|validate|check|verify/.test(lower)) {
7286
+ return [
7287
+ "Valid input \u2192 success",
7288
+ "Invalid input \u2192 throws error",
7289
+ "Edge case: empty/null input \u2192 handled gracefully"
7290
+ ];
7291
+ }
7292
+ if (/create|add|insert|save/.test(lower)) {
7293
+ return [
7294
+ "Success: valid data \u2192 created",
7295
+ "Duplicate: existing item \u2192 error or no-op",
7296
+ "Missing required fields \u2192 validation error"
7297
+ ];
7298
+ }
7299
+ if (/delete|remove|destroy/.test(lower)) {
7300
+ return [
7301
+ "Existing item \u2192 deleted successfully",
7302
+ "Non-existent item \u2192 no error or 404",
7303
+ "Unauthorized access \u2192 rejected"
7304
+ ];
7305
+ }
7306
+ if (/get|find|fetch|load/.test(lower)) {
7307
+ return [
7308
+ "Found: returns correct data",
7309
+ "Not found: returns null or throws",
7310
+ "Empty collection: returns []"
7311
+ ];
7312
+ }
7313
+ return [
7314
+ "Happy path: valid input \u2192 expected output",
7315
+ "Error case: invalid input \u2192 error handled",
7316
+ "Edge case: boundary values \u2192 correct behavior"
7317
+ ];
7318
+ }
7319
+ function suggestTests(graph, symbolName) {
7320
+ let targetNode = void 0;
7321
+ for (const node of graph.allNodes()) {
7322
+ if (node.name === symbolName) {
7323
+ targetNode = node;
7324
+ break;
7325
+ }
7326
+ }
7327
+ if (!targetNode) {
7328
+ return { error: `Symbol not found: ${symbolName}` };
7329
+ }
7330
+ const targetId = targetNode.id;
7331
+ const callPaths = [];
7332
+ const pathQueue = [{ id: targetId, path: [symbolName], depth: 0 }];
7333
+ while (pathQueue.length > 0 && callPaths.length < 5) {
7334
+ const { id, path: path28, depth } = pathQueue.shift();
7335
+ let hasCallers = false;
7336
+ for (const edge of graph.findEdgesTo(id)) {
7337
+ if (edge.kind !== "calls") continue;
7338
+ const callerNode = graph.getNode(edge.source);
7339
+ if (!callerNode) continue;
7340
+ hasCallers = true;
7341
+ const newPath = [callerNode.name, ...path28];
7342
+ if (depth + 1 >= 3 || callPaths.length >= 5) {
7343
+ if (callPaths.length < 5) callPaths.push(newPath);
7344
+ continue;
7345
+ }
7346
+ pathQueue.push({ id: edge.source, path: newPath, depth: depth + 1 });
7347
+ }
7348
+ if (!hasCallers && path28.length > 1) {
7349
+ callPaths.push(path28);
7350
+ }
7351
+ }
7352
+ if (callPaths.length === 0) {
7353
+ for (const edge of graph.findEdgesTo(targetId)) {
7354
+ if (edge.kind !== "calls") continue;
7355
+ const callerNode = graph.getNode(edge.source);
7356
+ if (!callerNode) continue;
7357
+ callPaths.push([callerNode.name, symbolName]);
7358
+ if (callPaths.length >= 5) break;
7359
+ }
7360
+ }
7361
+ const existingTestFiles = /* @__PURE__ */ new Set();
7362
+ for (const edge of graph.findEdgesTo(targetId)) {
7363
+ if (edge.kind !== "imports") continue;
7364
+ const importerNode = graph.getNode(edge.source);
7365
+ if (!importerNode) continue;
7366
+ if (importerNode.filePath.includes(".test.") || importerNode.filePath.includes(".spec.")) {
7367
+ existingTestFiles.add(importerNode.filePath);
7368
+ }
7369
+ }
7370
+ const existingTests = [...existingTestFiles];
7371
+ const untestedCallers = [];
7372
+ for (const edge of graph.findEdgesTo(targetId)) {
7373
+ if (edge.kind !== "calls") continue;
7374
+ const callerNode = graph.getNode(edge.source);
7375
+ if (!callerNode) continue;
7376
+ if (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec.")) {
7377
+ continue;
7378
+ }
7379
+ let callerHasTest = false;
7380
+ for (const callerImportEdge of graph.findEdgesTo(callerNode.id)) {
7381
+ if (callerImportEdge.kind !== "imports") continue;
7382
+ const importerOfCaller = graph.getNode(callerImportEdge.source);
7383
+ if (!importerOfCaller) continue;
7384
+ if (importerOfCaller.filePath.includes(".test.") || importerOfCaller.filePath.includes(".spec.")) {
7385
+ callerHasTest = true;
7386
+ break;
7387
+ }
7388
+ }
7389
+ if (!callerHasTest) {
7390
+ untestedCallers.push(callerNode.name);
7391
+ }
7392
+ }
7393
+ const suggestedCases = getSuggestedCases(symbolName);
7394
+ return {
7395
+ callPaths,
7396
+ suggestedCases,
7397
+ existingTests,
7398
+ untestedCallers
7399
+ };
7400
+ }
7401
+
7402
+ // src/query/cluster-summary.ts
7403
+ function getPathPrefix(filePath) {
7404
+ const parts = filePath.replace(/\\/g, "/").split("/");
7405
+ return parts.slice(0, 2).join("/");
7406
+ }
7407
+ function summarizeCluster(graph, cluster) {
7408
+ const clusterNodes = [...graph.allNodes()].filter(
7409
+ (n) => n.filePath.startsWith(cluster) || n.metadata?.["cluster"] === cluster
7410
+ );
7411
+ if (clusterNodes.length === 0) {
7412
+ return { error: `Cluster not found: ${cluster}` };
7413
+ }
7414
+ const clusterNodeIds = new Set(clusterNodes.map((n) => n.id));
7415
+ const callerCountMap = /* @__PURE__ */ new Map();
7416
+ for (const node of clusterNodes) {
7417
+ let count = 0;
7418
+ for (const _edge of graph.findEdgesTo(node.id)) {
7419
+ count++;
7420
+ }
7421
+ callerCountMap.set(node.id, count);
7422
+ }
7423
+ const sortedByCallers = [...clusterNodes].sort(
7424
+ (a, b) => (callerCountMap.get(b.id) ?? 0) - (callerCountMap.get(a.id) ?? 0)
7425
+ );
7426
+ const keySymbols = sortedByCallers.slice(0, 5).map((n) => ({
7427
+ name: n.name,
7428
+ callerCount: callerCountMap.get(n.id) ?? 0
7429
+ }));
7430
+ const depsSet = /* @__PURE__ */ new Set();
7431
+ for (const node of clusterNodes) {
7432
+ for (const edge of graph.findEdgesFrom(node.id)) {
7433
+ if (edge.kind !== "imports") continue;
7434
+ const targetNode = graph.getNode(edge.target);
7435
+ if (!targetNode) continue;
7436
+ if (!clusterNodeIds.has(targetNode.id)) {
7437
+ const prefix = getPathPrefix(targetNode.filePath);
7438
+ depsSet.add(prefix);
7439
+ }
7440
+ }
7441
+ }
7442
+ const dependencies = [...depsSet];
7443
+ const dependentsSet = /* @__PURE__ */ new Set();
7444
+ for (const node of clusterNodes) {
7445
+ for (const edge of graph.findEdgesTo(node.id)) {
7446
+ if (edge.kind !== "imports") continue;
7447
+ const sourceNode = graph.getNode(edge.source);
7448
+ if (!sourceNode) continue;
7449
+ if (!clusterNodeIds.has(sourceNode.id)) {
7450
+ const prefix = getPathPrefix(sourceNode.filePath);
7451
+ dependentsSet.add(prefix);
7452
+ }
7453
+ }
7454
+ }
7455
+ const dependents = [...dependentsSet];
7456
+ const healthResult = computeHealthReport(graph, cluster);
7457
+ const health = { score: healthResult.healthScore };
7458
+ const symbolCount = {};
7459
+ for (const node of clusterNodes) {
7460
+ symbolCount[node.kind] = (symbolCount[node.kind] ?? 0) + 1;
7461
+ }
7462
+ let purpose;
7463
+ const topNode = sortedByCallers[0];
7464
+ if (topNode?.metadata?.["summary"] && typeof topNode.metadata["summary"] === "string") {
7465
+ purpose = topNode.metadata["summary"];
7466
+ } else {
7467
+ const clusterName = cluster.split("/").pop() ?? cluster;
7468
+ purpose = `Handles ${clusterName.replace(/[-_/]/g, " ")} functionality`;
7469
+ }
7470
+ return {
7471
+ cluster,
7472
+ purpose,
7473
+ keySymbols,
7474
+ dependencies,
7475
+ dependents,
7476
+ health,
7477
+ symbolCount
7478
+ };
7479
+ }
7480
+
7481
+ // src/mcp-server/server.ts
7482
+ function createMcpServer(graph, repoName, workspaceRoot) {
7483
+ const server = new Server(
7484
+ { name: "code-intel", version: "0.1.0" },
7485
+ { capabilities: { tools: {}, resources: {} } }
7486
+ );
7487
+ const _tokenProp = {
7488
+ _token: { type: "string", description: "Required if CODE_INTEL_TOKEN is configured" }
7489
+ };
7490
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
7491
+ tools: [
7492
+ // ── Core repo tools ──────────────────────────────────────────────────
7493
+ {
7494
+ name: "repos",
7495
+ description: "List all indexed repositories with node and edge counts",
7496
+ inputSchema: { type: "object", properties: { ..._tokenProp } }
7497
+ },
7498
+ {
7499
+ name: "overview",
7500
+ 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.",
7501
+ inputSchema: { type: "object", properties: { ..._tokenProp } }
7502
+ },
7503
+ // ── Search & inspect ─────────────────────────────────────────────────
7504
+ {
7505
+ name: "search",
7506
+ description: "BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc.",
7507
+ inputSchema: {
7508
+ type: "object",
7509
+ properties: {
7510
+ query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
7511
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7512
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
7513
+ ..._tokenProp
7514
+ },
7515
+ required: ["query"]
7516
+ }
7517
+ },
7518
+ {
7519
+ name: "inspect",
7520
+ description: "360\xB0 view of a symbol: definition location, callers, callees, heritage (extends/implements), members, cluster, and source preview (first 500 chars)",
7521
+ inputSchema: {
7522
+ type: "object",
7523
+ properties: {
7524
+ symbol_name: { type: "string", description: "Exact symbol name to inspect" },
7525
+ ..._tokenProp
7526
+ },
7527
+ required: ["symbol_name"]
7528
+ }
7529
+ },
7530
+ {
7531
+ name: "blast_radius",
7532
+ 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).",
7533
+ inputSchema: {
7534
+ type: "object",
7535
+ properties: {
7536
+ target: { type: "string", description: "Target symbol name" },
7537
+ direction: {
7538
+ type: "string",
7539
+ enum: ["callers", "callees", "both"],
7540
+ description: "Which direction to trace \u2014 callers (who depends on it), callees (what it depends on), or both (default: both)"
7541
+ },
6174
7542
  max_hops: { type: "number", description: "Maximum traversal depth (default: 5)" },
6175
7543
  ..._tokenProp
6176
7544
  },
@@ -6184,6 +7552,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6184
7552
  type: "object",
6185
7553
  properties: {
6186
7554
  file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' },
7555
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7556
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
6187
7557
  ..._tokenProp
6188
7558
  },
6189
7559
  required: ["file_path"]
@@ -6213,7 +7583,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6213
7583
  type: "string",
6214
7584
  description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
6215
7585
  },
6216
- limit: { type: "number", description: "Max results (default: 100)" },
7586
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7587
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
6217
7588
  ..._tokenProp
6218
7589
  }
6219
7590
  }
@@ -6230,7 +7601,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6230
7601
  inputSchema: {
6231
7602
  type: "object",
6232
7603
  properties: {
6233
- limit: { type: "number", description: "Max clusters to return (default: 50)" },
7604
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7605
+ limit: { type: "number", description: "Max clusters per page (default: 50, max: 500)" },
6234
7606
  ..._tokenProp
6235
7607
  }
6236
7608
  }
@@ -6241,7 +7613,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6241
7613
  inputSchema: {
6242
7614
  type: "object",
6243
7615
  properties: {
6244
- limit: { type: "number", description: "Max flows to return (default: 50)" },
7616
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
7617
+ limit: { type: "number", description: "Max flows per page (default: 50, max: 500)" },
6245
7618
  ..._tokenProp
6246
7619
  }
6247
7620
  }
@@ -6265,6 +7638,23 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6265
7638
  }
6266
7639
  }
6267
7640
  },
7641
+ // ── query (GQL) ────────────────────────────────────────────────────────
7642
+ {
7643
+ name: "query",
7644
+ description: "Execute a GQL (Graph Query Language) query. Supports FIND, TRAVERSE, PATH, and COUNT. More expressive than raw_query.",
7645
+ inputSchema: {
7646
+ type: "object",
7647
+ properties: {
7648
+ gql: {
7649
+ type: "string",
7650
+ description: 'GQL query string. Examples: "FIND function WHERE name CONTAINS \\"auth\\"", "TRAVERSE CALLS FROM \\"handleLogin\\" DEPTH 3", "PATH FROM \\"createUser\\" TO \\"sendEmail\\"", "COUNT function GROUP BY cluster"'
7651
+ },
7652
+ limit: { type: "number", description: "Override LIMIT in the query (optional)" },
7653
+ ..._tokenProp
7654
+ },
7655
+ required: ["gql"]
7656
+ }
7657
+ },
6268
7658
  // ── Raw query ─────────────────────────────────────────────────────────
6269
7659
  {
6270
7660
  name: "raw_query",
@@ -6346,6 +7736,91 @@ function createMcpServer(graph, repoName, workspaceRoot) {
6346
7736
  },
6347
7737
  required: ["name"]
6348
7738
  }
7739
+ },
7740
+ // ── Reasoning / analysis tools ────────────────────────────────────────
7741
+ {
7742
+ name: "explain_relationship",
7743
+ description: "Explain how two symbols are connected: directed paths, shared imports, and heritage (extends/implements). Returns up to 10 paths with at most 5 hops each.",
7744
+ inputSchema: {
7745
+ type: "object",
7746
+ properties: {
7747
+ from: { type: "string", description: "Source symbol name" },
7748
+ to: { type: "string", description: "Target symbol name" },
7749
+ ..._tokenProp
7750
+ },
7751
+ required: ["from", "to"]
7752
+ }
7753
+ },
7754
+ {
7755
+ name: "pr_impact",
7756
+ description: "Given changed files or a unified diff, compute full blast radius with risk scores (HIGH/MEDIUM/LOW), test coverage gaps, and top files to review.",
7757
+ inputSchema: {
7758
+ type: "object",
7759
+ properties: {
7760
+ changedFiles: {
7761
+ type: "array",
7762
+ items: { type: "string" },
7763
+ description: "List of changed file paths (relative or absolute)"
7764
+ },
7765
+ diff: {
7766
+ type: "string",
7767
+ description: "Raw unified diff text. Changed files are extracted automatically."
7768
+ },
7769
+ maxHops: {
7770
+ type: "number",
7771
+ description: "Maximum BFS depth for blast radius (default: 5)"
7772
+ },
7773
+ ..._tokenProp
7774
+ }
7775
+ }
7776
+ },
7777
+ {
7778
+ name: "similar_symbols",
7779
+ description: "Find symbols with similar names or structure using Levenshtein distance and kind matching. Useful for finding related functions, classes, or interfaces.",
7780
+ inputSchema: {
7781
+ type: "object",
7782
+ properties: {
7783
+ symbol: { type: "string", description: "Symbol name to find similar symbols for" },
7784
+ limit: { type: "number", description: "Maximum number of results (default: 10, max: 50)" },
7785
+ ..._tokenProp
7786
+ },
7787
+ required: ["symbol"]
7788
+ }
7789
+ },
7790
+ {
7791
+ name: "health_report",
7792
+ description: "Code health signals for a scope: dead code, cycles, god nodes, orphan files, complexity hotspots",
7793
+ inputSchema: {
7794
+ type: "object",
7795
+ properties: {
7796
+ scope: { type: "string", description: "Directory scope, e.g. 'src/api/' or '.' for whole repo" },
7797
+ ..._tokenProp
7798
+ }
7799
+ }
7800
+ },
7801
+ {
7802
+ name: "suggest_tests",
7803
+ description: "Suggest test cases for a symbol: call paths, suggested cases, existing tests, untested callers",
7804
+ inputSchema: {
7805
+ type: "object",
7806
+ properties: {
7807
+ symbol: { type: "string", description: "Symbol name to generate test suggestions for" },
7808
+ ..._tokenProp
7809
+ },
7810
+ required: ["symbol"]
7811
+ }
7812
+ },
7813
+ {
7814
+ name: "cluster_summary",
7815
+ description: "Rich summary of a module/cluster: purpose, key symbols, dependencies, health",
7816
+ inputSchema: {
7817
+ type: "object",
7818
+ properties: {
7819
+ cluster: { type: "string", description: "Cluster path e.g. 'src/auth'" },
7820
+ ..._tokenProp
7821
+ },
7822
+ required: ["cluster"]
7823
+ }
6349
7824
  }
6350
7825
  ]
6351
7826
  }));
@@ -6416,8 +7891,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6416
7891
  for (const edge of graph.allEdges()) {
6417
7892
  edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
6418
7893
  }
6419
- const { computeHealthReport: computeHealthReport2 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
6420
- const healthReport = computeHealthReport2(graph);
7894
+ const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
7895
+ const healthReport = computeHealthReport3(graph);
6421
7896
  const health = {
6422
7897
  score: Math.round(healthReport.score),
6423
7898
  grade: healthReport.grade,
@@ -6442,10 +7917,37 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6442
7917
  // ── search ─────────────────────────────────────────────────────────────
6443
7918
  case "search": {
6444
7919
  const query = a.query;
6445
- const limit = a.limit ?? 20;
7920
+ const offset = a.offset ?? 0;
7921
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
6446
7922
  const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
6447
- const { results, searchMode } = await hybridSearch(graph, query, limit, { vectorDbPath: vdbPath });
6448
- return { content: [{ type: "text", text: JSON.stringify({ results, searchMode }, null, 2) }] };
7923
+ const fetchLimit = Math.min(offset + effectiveLimit, 500);
7924
+ const { results: allResults, searchMode } = await hybridSearch(graph, query, fetchLimit, { vectorDbPath: vdbPath });
7925
+ const total = allResults.length;
7926
+ const results = allResults.slice(offset, offset + effectiveLimit);
7927
+ const hasMore = offset + effectiveLimit < total;
7928
+ const suggestNextTools = [];
7929
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
7930
+ if (suggestEnabled && results.length > 0) {
7931
+ const topName = results[0].name;
7932
+ suggestNextTools.push(
7933
+ { tool: "inspect", reason: "Inspect the top result in detail", input: { symbol: topName } },
7934
+ { tool: "similar_symbols", reason: "Find symbols similar to the top result", input: { symbol: topName } }
7935
+ );
7936
+ }
7937
+ return {
7938
+ content: [{
7939
+ type: "text",
7940
+ text: JSON.stringify({
7941
+ results,
7942
+ searchMode,
7943
+ total,
7944
+ offset,
7945
+ limit: effectiveLimit,
7946
+ hasMore,
7947
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
7948
+ }, null, 2)
7949
+ }]
7950
+ };
6449
7951
  }
6450
7952
  // ── inspect ────────────────────────────────────────────────────────────
6451
7953
  case "inspect": {
@@ -6454,6 +7956,26 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6454
7956
  if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
6455
7957
  const incoming = [...graph.findEdgesTo(node.id)];
6456
7958
  const outgoing = [...graph.findEdgesFrom(node.id)];
7959
+ const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
7960
+ id: e.source,
7961
+ name: graph.getNode(e.source)?.name,
7962
+ file: graph.getNode(e.source)?.filePath
7963
+ }));
7964
+ const callees = outgoing.filter((e) => e.kind === "calls").map((e) => ({
7965
+ id: e.target,
7966
+ name: graph.getNode(e.target)?.name,
7967
+ file: graph.getNode(e.target)?.filePath
7968
+ }));
7969
+ const cluster = incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0];
7970
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
7971
+ const suggestNextTools = [];
7972
+ if (suggestEnabled) {
7973
+ const topCallerName = callers[0]?.name;
7974
+ suggestNextTools.push(
7975
+ ...topCallerName ? [{ tool: "explain_relationship", reason: "Explain connection to a related symbol", input: { from: node.name, to: topCallerName } }] : [],
7976
+ ...cluster ? [{ tool: "cluster_summary", reason: "Summarize the module this symbol belongs to", input: { cluster } }] : [{ tool: "cluster_summary", reason: "Summarize the module this symbol belongs to", input: { cluster: node.filePath } }]
7977
+ );
7978
+ }
6457
7979
  return {
6458
7980
  content: [{
6459
7981
  type: "text",
@@ -6467,16 +7989,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6467
7989
  endLine: node.endLine,
6468
7990
  exported: node.exported
6469
7991
  },
6470
- callers: incoming.filter((e) => e.kind === "calls").map((e) => ({
6471
- id: e.source,
6472
- name: graph.getNode(e.source)?.name,
6473
- file: graph.getNode(e.source)?.filePath
6474
- })),
6475
- callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
6476
- id: e.target,
6477
- name: graph.getNode(e.target)?.name,
6478
- file: graph.getNode(e.target)?.filePath
6479
- })),
7992
+ callers,
7993
+ callees,
6480
7994
  imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
6481
7995
  importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
6482
7996
  extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
@@ -6485,8 +7999,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6485
7999
  name: graph.getNode(e.target)?.name,
6486
8000
  kind: graph.getNode(e.target)?.kind
6487
8001
  })),
6488
- cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0],
6489
- content: node.content?.slice(0, 500)
8002
+ cluster,
8003
+ content: node.content?.slice(0, 500),
8004
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
6490
8005
  }, null, 2)
6491
8006
  }]
6492
8007
  };
@@ -6522,6 +8037,16 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6522
8037
  return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
6523
8038
  });
6524
8039
  const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
8040
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
8041
+ const suggestNextTools = [];
8042
+ if (suggestEnabled) {
8043
+ const highestRiskSymbol = node.name;
8044
+ const firstFilePath = affectedDetails[0]?.filePath ?? "";
8045
+ suggestNextTools.push(
8046
+ { tool: "suggest_tests", reason: "Generate tests for the highest-risk symbol", input: { symbol: highestRiskSymbol } },
8047
+ { tool: "pr_impact", reason: "Compute full PR impact for changed files", input: { changedFiles: [firstFilePath] } }
8048
+ );
8049
+ }
6525
8050
  return {
6526
8051
  content: [{
6527
8052
  type: "text",
@@ -6529,7 +8054,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6529
8054
  target: node.name,
6530
8055
  affectedCount: affected.size,
6531
8056
  riskLevel: risk,
6532
- affected: affectedDetails
8057
+ affected: affectedDetails,
8058
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
6533
8059
  }, null, 2)
6534
8060
  }]
6535
8061
  };
@@ -6537,17 +8063,27 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6537
8063
  // ── file_symbols ───────────────────────────────────────────────────────
6538
8064
  case "file_symbols": {
6539
8065
  const filePath = a.file_path;
6540
- const matches = [];
8066
+ const offset = a.offset ?? 0;
8067
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8068
+ const allMatches = [];
6541
8069
  for (const node of graph.allNodes()) {
6542
8070
  if (node.filePath && node.filePath.includes(filePath)) {
6543
- matches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
8071
+ allMatches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
6544
8072
  }
6545
8073
  }
6546
- if (matches.length === 0) {
8074
+ if (allMatches.length === 0) {
6547
8075
  return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
6548
8076
  }
6549
- matches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
6550
- return { content: [{ type: "text", text: JSON.stringify(matches, null, 2) }] };
8077
+ allMatches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
8078
+ const total = allMatches.length;
8079
+ const matches = allMatches.slice(offset, offset + effectiveLimit);
8080
+ const hasMore = offset + effectiveLimit < total;
8081
+ return {
8082
+ content: [{
8083
+ type: "text",
8084
+ text: JSON.stringify({ symbols: matches, total, offset, limit: effectiveLimit, hasMore }, null, 2)
8085
+ }]
8086
+ };
6551
8087
  }
6552
8088
  // ── find_path ──────────────────────────────────────────────────────────
6553
8089
  case "find_path": {
@@ -6593,15 +8129,23 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6593
8129
  // ── list_exports ───────────────────────────────────────────────────────
6594
8130
  case "list_exports": {
6595
8131
  const kindFilter = a.kind;
6596
- const limit = a.limit ?? 100;
6597
- const exports$1 = [];
8132
+ const offset = a.offset ?? 0;
8133
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8134
+ const allExports = [];
6598
8135
  for (const node of graph.allNodes()) {
6599
8136
  if (!node.exported) continue;
6600
8137
  if (kindFilter && node.kind !== kindFilter) continue;
6601
- exports$1.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
6602
- if (exports$1.length >= limit) break;
8138
+ allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
6603
8139
  }
6604
- return { content: [{ type: "text", text: JSON.stringify({ total: exports$1.length, exports: exports$1 }, null, 2) }] };
8140
+ const total = allExports.length;
8141
+ const exports$1 = allExports.slice(offset, offset + effectiveLimit);
8142
+ const hasMore = offset + effectiveLimit < total;
8143
+ return {
8144
+ content: [{
8145
+ type: "text",
8146
+ text: JSON.stringify({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore }, null, 2)
8147
+ }]
8148
+ };
6605
8149
  }
6606
8150
  // ── routes ─────────────────────────────────────────────────────────────
6607
8151
  case "routes": {
@@ -6615,8 +8159,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6615
8159
  }
6616
8160
  // ── clusters ───────────────────────────────────────────────────────────
6617
8161
  case "clusters": {
6618
- const limit = a.limit ?? 50;
6619
- const clusters = [];
8162
+ const offset = a.offset ?? 0;
8163
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8164
+ const allClusters = [];
6620
8165
  for (const node of graph.allNodes()) {
6621
8166
  if (node.kind === "cluster") {
6622
8167
  const members = [];
@@ -6628,35 +8173,50 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6628
8173
  }
6629
8174
  }
6630
8175
  }
6631
- clusters.push({
8176
+ allClusters.push({
6632
8177
  id: node.id,
6633
8178
  name: node.name,
6634
8179
  memberCount: node.metadata?.memberCount ?? members.length,
6635
8180
  topSymbols: members.slice(0, 10)
6636
8181
  });
6637
- if (clusters.length >= limit) break;
6638
8182
  }
6639
8183
  }
6640
- return { content: [{ type: "text", text: JSON.stringify(clusters, null, 2) }] };
8184
+ const total = allClusters.length;
8185
+ const clusters = allClusters.slice(offset, offset + effectiveLimit);
8186
+ const hasMore = offset + effectiveLimit < total;
8187
+ return {
8188
+ content: [{
8189
+ type: "text",
8190
+ text: JSON.stringify({ clusters, total, offset, limit: effectiveLimit, hasMore }, null, 2)
8191
+ }]
8192
+ };
6641
8193
  }
6642
8194
  // ── flows ──────────────────────────────────────────────────────────────
6643
8195
  case "flows": {
6644
- const limit = a.limit ?? 50;
6645
- const flows = [];
8196
+ const offset = a.offset ?? 0;
8197
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8198
+ const allFlows = [];
6646
8199
  for (const node of graph.allNodes()) {
6647
8200
  if (node.kind === "flow") {
6648
8201
  const steps = node.metadata?.steps;
6649
- flows.push({
8202
+ allFlows.push({
6650
8203
  id: node.id,
6651
8204
  name: node.name,
6652
8205
  entryPoint: node.metadata?.entryPoint,
6653
8206
  steps: steps ?? [],
6654
8207
  stepCount: Array.isArray(steps) ? steps.length : 0
6655
8208
  });
6656
- if (flows.length >= limit) break;
6657
8209
  }
6658
8210
  }
6659
- return { content: [{ type: "text", text: JSON.stringify(flows, null, 2) }] };
8211
+ const total = allFlows.length;
8212
+ const flows = allFlows.slice(offset, offset + effectiveLimit);
8213
+ const hasMore = offset + effectiveLimit < total;
8214
+ return {
8215
+ content: [{
8216
+ type: "text",
8217
+ text: JSON.stringify({ flows, total, offset, limit: effectiveLimit, hasMore }, null, 2)
8218
+ }]
8219
+ };
6660
8220
  }
6661
8221
  // ── detect_changes ─────────────────────────────────────────────────────
6662
8222
  case "detect_changes": {
@@ -6684,7 +8244,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6684
8244
  for (const { filePath: changedFile, changedLines } of changedFiles) {
6685
8245
  for (const node of graph.allNodes()) {
6686
8246
  if (!node.filePath) continue;
6687
- const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path26.sep, "");
8247
+ const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path27.sep, "");
6688
8248
  const normChanged = changedFile.replace(/^a\/|^b\//, "");
6689
8249
  if (!normNode.endsWith(normChanged) && !normChanged.endsWith(normNode)) continue;
6690
8250
  if (node.startLine !== void 0 && node.endLine !== void 0) {
@@ -6733,16 +8293,51 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6733
8293
  }]
6734
8294
  };
6735
8295
  }
8296
+ // ── query (GQL) ───────────────────────────────────────────────────────────
8297
+ case "query": {
8298
+ const gqlInput = a.gql;
8299
+ if (!gqlInput) {
8300
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Missing required parameter: gql" }) }], isError: true };
8301
+ }
8302
+ const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
8303
+ const { executeGQL: executeGQL2 } = await Promise.resolve().then(() => (init_gql_executor(), gql_executor_exports));
8304
+ const ast = parseGQL2(gqlInput);
8305
+ if (isGQLParseError2(ast)) {
8306
+ return {
8307
+ content: [{ type: "text", text: JSON.stringify({ error: `GQL parse error: ${ast.message}`, pos: ast.pos, expected: ast.expected, got: ast.got }) }],
8308
+ isError: true
8309
+ };
8310
+ }
8311
+ if (a.limit !== void 0 && ast.type === "FIND") {
8312
+ ast.limit = a.limit;
8313
+ }
8314
+ const result = executeGQL2(ast, graph);
8315
+ return {
8316
+ content: [{
8317
+ type: "text",
8318
+ text: JSON.stringify({
8319
+ nodes: result.nodes,
8320
+ edges: result.edges,
8321
+ groups: result.groups,
8322
+ path: result.path,
8323
+ executionTimeMs: result.executionTimeMs,
8324
+ truncated: result.truncated,
8325
+ totalCount: result.totalCount
8326
+ }, null, 2)
8327
+ }]
8328
+ };
8329
+ }
6736
8330
  // ── raw_query ──────────────────────────────────────────────────────────
6737
8331
  case "raw_query": {
6738
8332
  const q = a.cypher;
8333
+ const deprecationWarning = "raw_query is deprecated, use query instead";
6739
8334
  const nameMatch = q?.match(/name\s*=\s*['"]([^'"]+)['"]/i);
6740
8335
  if (nameMatch) {
6741
8336
  const results = [];
6742
8337
  for (const node of graph.allNodes()) {
6743
8338
  if (node.name === nameMatch[1]) results.push(node);
6744
8339
  }
6745
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
8340
+ return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, results }, null, 2) }] };
6746
8341
  }
6747
8342
  const kindMatch = q?.match(/:\s*(\w+)/);
6748
8343
  if (kindMatch) {
@@ -6751,9 +8346,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6751
8346
  if (node.kind === kindMatch[1]) results.push(node);
6752
8347
  if (results.length >= 50) break;
6753
8348
  }
6754
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
8349
+ return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, results }, null, 2) }] };
6755
8350
  }
6756
- return { content: [{ type: "text", text: "Query not recognized. Use name='X' or :kind syntax." }] };
8351
+ return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, error: "Query not recognized. Use name='X' or :kind syntax. Or use the query tool with GQL instead." }) }] };
6757
8352
  }
6758
8353
  // ── group_list ─────────────────────────────────────────────────────────
6759
8354
  case "group_list": {
@@ -6871,6 +8466,57 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
6871
8466
  }]
6872
8467
  };
6873
8468
  }
8469
+ // ── explain_relationship ───────────────────────────────────────────────
8470
+ case "explain_relationship": {
8471
+ const fromName = a.from;
8472
+ const toName = a.to;
8473
+ const result = explainRelationship(graph, fromName, toName);
8474
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8475
+ }
8476
+ // ── pr_impact ──────────────────────────────────────────────────────────
8477
+ case "pr_impact": {
8478
+ const maxHops = a.maxHops ?? 5;
8479
+ let changedFiles = a.changedFiles ?? [];
8480
+ if (a.diff && typeof a.diff === "string") {
8481
+ const diffFiles = parseDiffFiles(a.diff);
8482
+ changedFiles = [.../* @__PURE__ */ new Set([...changedFiles, ...diffFiles])];
8483
+ }
8484
+ if (changedFiles.length === 0) {
8485
+ return {
8486
+ content: [{
8487
+ type: "text",
8488
+ text: JSON.stringify({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
8489
+ }]
8490
+ };
8491
+ }
8492
+ const result = computePRImpact(graph, changedFiles, maxHops);
8493
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8494
+ }
8495
+ // ── similar_symbols ────────────────────────────────────────────────────
8496
+ case "similar_symbols": {
8497
+ const symbolName = a.symbol;
8498
+ const limit = a.limit ?? 10;
8499
+ const result = findSimilarSymbols(graph, symbolName, limit);
8500
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8501
+ }
8502
+ // ── health_report ──────────────────────────────────────────────────────
8503
+ case "health_report": {
8504
+ const scope = a.scope ?? ".";
8505
+ const result = computeHealthReport(graph, scope);
8506
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8507
+ }
8508
+ // ── suggest_tests ──────────────────────────────────────────────────────
8509
+ case "suggest_tests": {
8510
+ const sym = a.symbol;
8511
+ const result = suggestTests(graph, sym);
8512
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8513
+ }
8514
+ // ── cluster_summary ────────────────────────────────────────────────────
8515
+ case "cluster_summary": {
8516
+ const cluster = a.cluster;
8517
+ const result = summarizeCluster(graph, cluster);
8518
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
8519
+ }
6874
8520
  default:
6875
8521
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
6876
8522
  }
@@ -6964,7 +8610,7 @@ var STUCK_THRESHOLD_MINUTES = 30;
6964
8610
  var JobsDB = class {
6965
8611
  db;
6966
8612
  constructor(dbPath) {
6967
- fs19.mkdirSync(path26.dirname(dbPath), { recursive: true });
8613
+ fs19.mkdirSync(path27.dirname(dbPath), { recursive: true });
6968
8614
  this.db = new Database3(dbPath);
6969
8615
  this.db.pragma("journal_mode = WAL");
6970
8616
  this.db.pragma("foreign_keys = ON");
@@ -7106,7 +8752,7 @@ var JobsDB = class {
7106
8752
  }
7107
8753
  };
7108
8754
  function getJobsDBPath() {
7109
- return path26.join(os12.homedir(), ".code-intel", "jobs.db");
8755
+ return path27.join(os12.homedir(), ".code-intel", "jobs.db");
7110
8756
  }
7111
8757
  var _jobsDB = null;
7112
8758
  function getOrCreateJobsDB() {
@@ -7198,7 +8844,7 @@ var BACKUP_VERSION = "1.0";
7198
8844
  var ALGORITHM = "aes-256-gcm";
7199
8845
  var IV_LENGTH = 16;
7200
8846
  function getBackupDir() {
7201
- return path26.join(os12.homedir(), ".code-intel", "backups");
8847
+ return path27.join(os12.homedir(), ".code-intel", "backups");
7202
8848
  }
7203
8849
  function getBackupKey() {
7204
8850
  const keyHex = process.env["CODE_INTEL_BACKUP_KEY"];
@@ -7236,22 +8882,22 @@ var BackupService = class {
7236
8882
  * Returns the backup entry.
7237
8883
  */
7238
8884
  createBackup(repoPath) {
7239
- const codeIntelDir = path26.join(repoPath, ".code-intel");
8885
+ const codeIntelDir = path27.join(repoPath, ".code-intel");
7240
8886
  const id = v4();
7241
8887
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7242
8888
  const filesToBackup = [];
7243
8889
  const candidates = ["graph.db", "vector.db", "meta.json"];
7244
8890
  for (const f of candidates) {
7245
- const fp = path26.join(codeIntelDir, f);
8891
+ const fp = path27.join(codeIntelDir, f);
7246
8892
  if (fs19.existsSync(fp)) {
7247
8893
  filesToBackup.push({ name: f, localPath: fp });
7248
8894
  }
7249
8895
  }
7250
- const registryPath = path26.join(os12.homedir(), ".code-intel", "registry.json");
8896
+ const registryPath = path27.join(os12.homedir(), ".code-intel", "registry.json");
7251
8897
  if (fs19.existsSync(registryPath)) {
7252
8898
  filesToBackup.push({ name: "registry.json", localPath: registryPath });
7253
8899
  }
7254
- const usersDbPath = path26.join(os12.homedir(), ".code-intel", "users.db");
8900
+ const usersDbPath = path27.join(os12.homedir(), ".code-intel", "users.db");
7255
8901
  if (fs19.existsSync(usersDbPath)) {
7256
8902
  filesToBackup.push({ name: "users.db", localPath: usersDbPath });
7257
8903
  }
@@ -7288,7 +8934,7 @@ var BackupService = class {
7288
8934
  const plaintext = Buffer.concat(parts);
7289
8935
  const encrypted = encryptBuffer(plaintext, this.key);
7290
8936
  const backupFileName = `backup-${id}.cib`;
7291
- const backupPath = path26.join(this.backupDir, backupFileName);
8937
+ const backupPath = path27.join(this.backupDir, backupFileName);
7292
8938
  fs19.writeFileSync(backupPath, encrypted);
7293
8939
  const entry = {
7294
8940
  id,
@@ -7316,7 +8962,7 @@ var BackupService = class {
7316
8962
  async uploadToS3(entry) {
7317
8963
  const cfg = getS3Config();
7318
8964
  if (!cfg) throw new Error("S3 not configured. Set CODE_INTEL_BACKUP_S3_BUCKET, CODE_INTEL_BACKUP_S3_ACCESS_KEY_ID, CODE_INTEL_BACKUP_S3_SECRET_ACCESS_KEY.");
7319
- const fileName = path26.basename(entry.path);
8965
+ const fileName = path27.basename(entry.path);
7320
8966
  const s3Key = `${cfg.prefix}${fileName}`;
7321
8967
  const body = fs19.readFileSync(entry.path);
7322
8968
  const result = await s3Request({ method: "PUT", cfg, key: s3Key, body });
@@ -7335,7 +8981,7 @@ var BackupService = class {
7335
8981
  if (result.statusCode < 200 || result.statusCode >= 300) {
7336
8982
  throw new Error(`S3 download failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
7337
8983
  }
7338
- fs19.mkdirSync(path26.dirname(destPath), { recursive: true });
8984
+ fs19.mkdirSync(path27.dirname(destPath), { recursive: true });
7339
8985
  fs19.writeFileSync(destPath, Buffer.from(result.body, "binary"));
7340
8986
  }
7341
8987
  /**
@@ -7399,7 +9045,7 @@ var BackupService = class {
7399
9045
  offset += manifestLen;
7400
9046
  const manifest = JSON.parse(manifestStr);
7401
9047
  const restoreBase = targetRepoPath ?? entry.repoPath;
7402
- const codeIntelDir = path26.join(restoreBase, ".code-intel");
9048
+ const codeIntelDir = path27.join(restoreBase, ".code-intel");
7403
9049
  fs19.mkdirSync(codeIntelDir, { recursive: true });
7404
9050
  for (const fileEntry of manifest.files) {
7405
9051
  const nameLen = plaintext.readUInt16BE(offset);
@@ -7417,9 +9063,9 @@ var BackupService = class {
7417
9063
  }
7418
9064
  let destPath;
7419
9065
  if (name === "registry.json" || name === "users.db") {
7420
- destPath = path26.join(os12.homedir(), ".code-intel", name);
9066
+ destPath = path27.join(os12.homedir(), ".code-intel", name);
7421
9067
  } else {
7422
- destPath = path26.join(codeIntelDir, name);
9068
+ destPath = path27.join(codeIntelDir, name);
7423
9069
  }
7424
9070
  fs19.writeFileSync(destPath, data);
7425
9071
  }
@@ -7470,7 +9116,7 @@ var BackupService = class {
7470
9116
  }
7471
9117
  // ── Index helpers ──────────────────────────────────────────────────────────
7472
9118
  _indexPath() {
7473
- return path26.join(this.backupDir, "index.json");
9119
+ return path27.join(this.backupDir, "index.json");
7474
9120
  }
7475
9121
  _loadIndex() {
7476
9122
  try {
@@ -7971,6 +9617,60 @@ var openApiSpec = {
7971
9617
  }
7972
9618
  }
7973
9619
  },
9620
+ "/source": {
9621
+ get: {
9622
+ tags: ["Files"],
9623
+ summary: "Get source code preview with context around specified lines",
9624
+ description: "Returns the file content around the specified line range (\xB120 lines context), with language detection. Requires viewer role.",
9625
+ security: [{ BearerAuth: [] }, { SessionCookie: [] }],
9626
+ parameters: [
9627
+ {
9628
+ name: "file",
9629
+ in: "query",
9630
+ required: true,
9631
+ description: "Absolute path to the file",
9632
+ schema: { type: "string" }
9633
+ },
9634
+ {
9635
+ name: "startLine",
9636
+ in: "query",
9637
+ required: false,
9638
+ description: "Start line number (1-indexed)",
9639
+ schema: { type: "integer", minimum: 1 }
9640
+ },
9641
+ {
9642
+ name: "endLine",
9643
+ in: "query",
9644
+ required: false,
9645
+ description: "End line number (1-indexed)",
9646
+ schema: { type: "integer", minimum: 1 }
9647
+ }
9648
+ ],
9649
+ responses: {
9650
+ "200": {
9651
+ description: "Source code preview",
9652
+ content: {
9653
+ "application/json": {
9654
+ schema: {
9655
+ type: "object",
9656
+ properties: {
9657
+ content: { type: "string", description: "File content (with context lines)" },
9658
+ language: { type: "string", description: "Detected programming language", example: "typescript" },
9659
+ startLine: { type: "integer", description: "Actual start line returned (with context)" },
9660
+ endLine: { type: "integer", description: "Actual end line returned (with context)" }
9661
+ },
9662
+ required: ["content", "language", "startLine", "endLine"]
9663
+ }
9664
+ }
9665
+ }
9666
+ },
9667
+ "400": { description: "Bad request (missing file param or path traversal detected)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
9668
+ "401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
9669
+ "403": { description: "Forbidden (file outside indexed repos)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
9670
+ "404": { description: "File not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
9671
+ }
9672
+ }
9673
+ },
7974
9674
  "/grep": {
7975
9675
  post: {
7976
9676
  tags: ["Files"],
@@ -8084,16 +9784,122 @@ var openApiSpec = {
8084
9784
  "404": { description: "Group not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
8085
9785
  }
8086
9786
  }
9787
+ },
9788
+ "/query": {
9789
+ post: {
9790
+ tags: ["GQL"],
9791
+ summary: "Execute a GQL (Graph Query Language) query against the knowledge graph",
9792
+ description: "Supports FIND, TRAVERSE, PATH, and COUNT statements. Requires viewer role minimum.",
9793
+ security: [{ BearerAuth: [] }, { SessionCookie: [] }],
9794
+ requestBody: {
9795
+ required: true,
9796
+ content: {
9797
+ "application/json": {
9798
+ schema: {
9799
+ type: "object",
9800
+ properties: {
9801
+ gql: {
9802
+ type: "string",
9803
+ description: "GQL query string",
9804
+ example: 'FIND function WHERE name CONTAINS "auth"'
9805
+ },
9806
+ format: {
9807
+ type: "string",
9808
+ enum: ["json", "table", "csv"],
9809
+ default: "json",
9810
+ description: "Output format"
9811
+ }
9812
+ },
9813
+ required: ["gql"]
9814
+ }
9815
+ }
9816
+ }
9817
+ },
9818
+ responses: {
9819
+ "200": {
9820
+ description: "GQL execution result",
9821
+ content: {
9822
+ "application/json": {
9823
+ schema: {
9824
+ type: "object",
9825
+ properties: {
9826
+ nodes: { type: "array", items: { "$ref": "#/components/schemas/CodeNode" } },
9827
+ edges: { type: "array", items: { type: "object" } },
9828
+ groups: { type: "array", items: { type: "object", properties: { key: { type: "string" }, count: { type: "integer" } } } },
9829
+ path: { type: "array", items: { "$ref": "#/components/schemas/CodeNode" }, nullable: true },
9830
+ executionTimeMs: { type: "number" },
9831
+ truncated: { type: "boolean" },
9832
+ totalCount: { type: "integer" }
9833
+ }
9834
+ }
9835
+ }
9836
+ }
9837
+ },
9838
+ "400": { description: "Missing gql field", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
9839
+ "401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
9840
+ "403": { description: "Forbidden (insufficient role)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
9841
+ "422": { description: "GQL parse error", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
9842
+ }
9843
+ }
9844
+ },
9845
+ "/query/explain": {
9846
+ post: {
9847
+ tags: ["GQL"],
9848
+ summary: "Explain a GQL query \u2014 returns the execution plan without running it",
9849
+ description: "Returns a query plan object describing the steps that would be executed. Requires viewer role minimum.",
9850
+ security: [{ BearerAuth: [] }, { SessionCookie: [] }],
9851
+ requestBody: {
9852
+ required: true,
9853
+ content: {
9854
+ "application/json": {
9855
+ schema: {
9856
+ type: "object",
9857
+ properties: {
9858
+ gql: { type: "string", description: "GQL query string", example: 'FIND function WHERE name CONTAINS "auth"' }
9859
+ },
9860
+ required: ["gql"]
9861
+ }
9862
+ }
9863
+ }
9864
+ },
9865
+ responses: {
9866
+ "200": {
9867
+ description: "Query plan",
9868
+ content: {
9869
+ "application/json": {
9870
+ schema: {
9871
+ type: "object",
9872
+ properties: {
9873
+ plan: {
9874
+ type: "object",
9875
+ properties: {
9876
+ type: { type: "string", enum: ["FIND", "TRAVERSE", "PATH", "COUNT"] },
9877
+ gql: { type: "string" },
9878
+ steps: { type: "array", items: { type: "object" } },
9879
+ estimatedCost: { type: "number" }
9880
+ }
9881
+ },
9882
+ graphSize: { type: "object", properties: { nodes: { type: "integer" }, edges: { type: "integer" } } }
9883
+ }
9884
+ }
9885
+ }
9886
+ }
9887
+ },
9888
+ "400": { description: "Missing gql field", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
9889
+ "401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
9890
+ "422": { description: "GQL parse error", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
9891
+ }
9892
+ }
8087
9893
  }
8088
9894
  }
8089
9895
  };
8090
9896
 
8091
9897
  // src/http/app.ts
8092
- var __dirname$1 = path26.dirname(fileURLToPath(import.meta.url));
9898
+ var __dirname$1 = path27.dirname(fileURLToPath(import.meta.url));
8093
9899
  var WEB_DIST = (() => {
8094
- const bundled = path26.resolve(__dirname$1, "..", "web");
9900
+ const bundled = path27.resolve(__dirname$1, "..", "web");
8095
9901
  if (fs19.existsSync(bundled)) return bundled;
8096
- return path26.resolve(__dirname$1, "..", "..", "..", "web", "dist");
9902
+ return path27.resolve(__dirname$1, "..", "..", "..", "web", "dist");
8097
9903
  })();
8098
9904
  function getAllowedOrigins() {
8099
9905
  const env = process.env["CODE_INTEL_CORS_ORIGINS"];
@@ -8121,6 +9927,7 @@ function createDefaultLimiter() {
8121
9927
  function createApp(graph, repoName, workspaceRoot, watcherState) {
8122
9928
  const app = express();
8123
9929
  app.set("trust proxy", 1);
9930
+ app.use(compression());
8124
9931
  app.use(
8125
9932
  helmet({
8126
9933
  contentSecurityPolicy: false
@@ -8623,7 +10430,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
8623
10430
  const registry = loadRegistry();
8624
10431
  const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
8625
10432
  if (!entry) return null;
8626
- const dbPath = path26.join(entry.path, ".code-intel", "graph.db");
10433
+ const dbPath = path27.join(entry.path, ".code-intel", "graph.db");
8627
10434
  if (!fs19.existsSync(dbPath)) return null;
8628
10435
  const repoGraph = createKnowledgeGraph();
8629
10436
  const db = new DbManager(dbPath);
@@ -8969,7 +10776,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
8969
10776
  for (const member of group.members) {
8970
10777
  const regEntry = registry.find((r) => r.name === member.registryName);
8971
10778
  if (!regEntry) continue;
8972
- const dbPath = path26.join(regEntry.path, ".code-intel", "graph.db");
10779
+ const dbPath = path27.join(regEntry.path, ".code-intel", "graph.db");
8973
10780
  if (!fs19.existsSync(dbPath)) continue;
8974
10781
  const db = new DbManager(dbPath);
8975
10782
  try {
@@ -8982,10 +10789,245 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
8982
10789
  }
8983
10790
  res.json({ nodes: [...mergedGraph.allNodes()], edges: [...mergedGraph.allEdges()] });
8984
10791
  });
10792
+ app.get("/api/v1/source", requireAuth, requireRole("viewer"), (req, res) => {
10793
+ const { file, startLine: startLineStr, endLine: endLineStr } = req.query;
10794
+ if (!file) {
10795
+ res.status(400).json({
10796
+ error: {
10797
+ code: ErrorCodes.INVALID_REQUEST,
10798
+ message: "Missing required query parameter: file",
10799
+ requestId: req.requestId,
10800
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10801
+ }
10802
+ });
10803
+ return;
10804
+ }
10805
+ if (file.includes("../")) {
10806
+ res.status(400).json({
10807
+ error: {
10808
+ code: ErrorCodes.INVALID_REQUEST,
10809
+ message: "Path traversal detected",
10810
+ hint: 'File paths must not contain "../"',
10811
+ requestId: req.requestId,
10812
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10813
+ }
10814
+ });
10815
+ return;
10816
+ }
10817
+ let rawResolved = path27.normalize(file);
10818
+ if (!path27.isAbsolute(rawResolved) && workspaceRoot) {
10819
+ rawResolved = path27.join(workspaceRoot, rawResolved);
10820
+ }
10821
+ const resolvedFile = path27.resolve(rawResolved);
10822
+ function isInsideDir(fileAbs, dir) {
10823
+ const rel = path27.relative(path27.resolve(dir), fileAbs);
10824
+ return !rel.startsWith("..") && !path27.isAbsolute(rel);
10825
+ }
10826
+ if (workspaceRoot) {
10827
+ if (!isInsideDir(resolvedFile, workspaceRoot)) {
10828
+ const registry = loadRegistry();
10829
+ const inKnownRepo = registry.some((r) => isInsideDir(resolvedFile, r.path));
10830
+ if (!inKnownRepo) {
10831
+ res.status(403).json({
10832
+ error: {
10833
+ code: ErrorCodes.FORBIDDEN,
10834
+ message: "Access denied",
10835
+ hint: "File path must be within an indexed repository",
10836
+ requestId: req.requestId,
10837
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10838
+ }
10839
+ });
10840
+ return;
10841
+ }
10842
+ }
10843
+ } else {
10844
+ const registry = loadRegistry();
10845
+ const inKnownRepo = registry.some((r) => isInsideDir(resolvedFile, r.path));
10846
+ if (!inKnownRepo) {
10847
+ res.status(403).json({
10848
+ error: {
10849
+ code: ErrorCodes.FORBIDDEN,
10850
+ message: "Access denied",
10851
+ hint: "File path must be within an indexed repository",
10852
+ requestId: req.requestId,
10853
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10854
+ }
10855
+ });
10856
+ return;
10857
+ }
10858
+ }
10859
+ let fileContent;
10860
+ try {
10861
+ fileContent = fs19.readFileSync(resolvedFile, "utf-8");
10862
+ } catch {
10863
+ res.status(404).json({
10864
+ error: {
10865
+ code: ErrorCodes.NOT_FOUND,
10866
+ message: "File not found",
10867
+ requestId: req.requestId,
10868
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10869
+ }
10870
+ });
10871
+ return;
10872
+ }
10873
+ const lines = fileContent.split("\n");
10874
+ const parsedStart = startLineStr ? Number.parseInt(startLineStr, 10) : 1;
10875
+ const parsedEnd = endLineStr ? Number.parseInt(endLineStr, 10) : parsedStart;
10876
+ if (!Number.isFinite(parsedStart) || parsedStart < 1 || !Number.isFinite(parsedEnd) || parsedEnd < 1) {
10877
+ res.status(400).json({
10878
+ error: {
10879
+ code: ErrorCodes.INVALID_REQUEST,
10880
+ message: "Invalid startLine or endLine: must be positive integers",
10881
+ requestId: req.requestId,
10882
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10883
+ }
10884
+ });
10885
+ return;
10886
+ }
10887
+ const startLine = Math.max(1, parsedStart);
10888
+ const endLine = Math.min(lines.length, parsedEnd);
10889
+ const contextStart = Math.max(1, startLine - 20);
10890
+ const contextEnd = Math.min(lines.length, endLine + 20);
10891
+ const content = lines.slice(contextStart - 1, contextEnd).join("\n");
10892
+ const ext = path27.extname(resolvedFile).toLowerCase();
10893
+ const languageMap = {
10894
+ ".ts": "typescript",
10895
+ ".tsx": "typescript",
10896
+ ".js": "javascript",
10897
+ ".jsx": "javascript",
10898
+ ".mjs": "javascript",
10899
+ ".cjs": "javascript",
10900
+ ".py": "python",
10901
+ ".go": "go",
10902
+ ".rs": "rust",
10903
+ ".java": "java",
10904
+ ".cs": "csharp",
10905
+ ".cpp": "cpp",
10906
+ ".cc": "cpp",
10907
+ ".cxx": "cpp",
10908
+ ".c": "c",
10909
+ ".h": "c",
10910
+ ".hpp": "cpp",
10911
+ ".rb": "ruby",
10912
+ ".php": "php",
10913
+ ".swift": "swift",
10914
+ ".kt": "kotlin",
10915
+ ".kts": "kotlin",
10916
+ ".json": "json",
10917
+ ".yaml": "yaml",
10918
+ ".yml": "yaml",
10919
+ ".md": "markdown",
10920
+ ".sh": "bash",
10921
+ ".bash": "bash",
10922
+ ".zsh": "bash",
10923
+ ".sql": "sql",
10924
+ ".html": "html",
10925
+ ".htm": "html",
10926
+ ".css": "css",
10927
+ ".scss": "scss",
10928
+ ".less": "less",
10929
+ ".xml": "xml",
10930
+ ".toml": "toml"
10931
+ };
10932
+ const language = languageMap[ext] ?? "plaintext";
10933
+ res.json({
10934
+ content,
10935
+ language,
10936
+ startLine: contextStart,
10937
+ endLine: contextEnd
10938
+ });
10939
+ });
10940
+ app.post("/api/v1/query", requireRole("viewer"), async (req, res) => {
10941
+ const { gql, format } = req.body;
10942
+ if (!gql || typeof gql !== "string") {
10943
+ res.status(400).json({
10944
+ error: { code: ErrorCodes.INVALID_REQUEST, message: "Missing required field: gql", requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
10945
+ });
10946
+ return;
10947
+ }
10948
+ try {
10949
+ const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
10950
+ const { executeGQL: executeGQL2 } = await Promise.resolve().then(() => (init_gql_executor(), gql_executor_exports));
10951
+ const ast = parseGQL2(gql);
10952
+ if (isGQLParseError2(ast)) {
10953
+ res.status(422).json({
10954
+ error: {
10955
+ code: ErrorCodes.INVALID_REQUEST,
10956
+ message: `GQL parse error: ${ast.message}`,
10957
+ hint: `Position: ${ast.pos}${ast.expected ? `, expected: ${ast.expected}` : ""}${ast.got ? `, got: ${ast.got}` : ""}`,
10958
+ requestId: req.requestId,
10959
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10960
+ }
10961
+ });
10962
+ return;
10963
+ }
10964
+ const result = executeGQL2(ast, graph);
10965
+ const statusCode = result.truncated ? 408 : 200;
10966
+ res.status(statusCode).json({ ...result, format: format ?? "json" });
10967
+ } catch (err) {
10968
+ res.status(500).json({ error: { code: ErrorCodes.INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err), requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
10969
+ }
10970
+ });
10971
+ app.post("/api/v1/query/explain", requireRole("viewer"), async (req, res) => {
10972
+ const { gql } = req.body;
10973
+ if (!gql || typeof gql !== "string") {
10974
+ res.status(400).json({
10975
+ error: { code: ErrorCodes.INVALID_REQUEST, message: "Missing required field: gql", requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
10976
+ });
10977
+ return;
10978
+ }
10979
+ try {
10980
+ const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
10981
+ const ast = parseGQL2(gql);
10982
+ if (isGQLParseError2(ast)) {
10983
+ res.status(422).json({
10984
+ error: {
10985
+ code: ErrorCodes.INVALID_REQUEST,
10986
+ message: `GQL parse error: ${ast.message}`,
10987
+ hint: `Position: ${ast.pos}`,
10988
+ requestId: req.requestId,
10989
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10990
+ }
10991
+ });
10992
+ return;
10993
+ }
10994
+ const plan = { type: ast.type, gql };
10995
+ if (ast.type === "FIND") {
10996
+ plan.steps = [
10997
+ { step: 1, op: "SCAN_NODES", filter: ast.target === "*" ? "all" : `kind=${ast.target}` },
10998
+ ...ast.where ? [{ step: 2, op: "WHERE", conditions: ast.where.exprs.length }] : [],
10999
+ ...ast.limit !== void 0 ? [{ step: 3, op: "LIMIT", value: ast.limit }] : []
11000
+ ];
11001
+ plan.estimatedCost = graph.size.nodes;
11002
+ } else if (ast.type === "TRAVERSE") {
11003
+ plan.steps = [
11004
+ { step: 1, op: "FIND_START_NODE", name: ast.from },
11005
+ { step: 2, op: "BFS", edgeKind: ast.edgeKind, maxDepth: ast.depth ?? 5 }
11006
+ ];
11007
+ plan.estimatedCost = Math.min(graph.size.nodes, Math.pow(4, ast.depth ?? 5));
11008
+ } else if (ast.type === "PATH") {
11009
+ plan.steps = [
11010
+ { step: 1, op: "FIND_NODES", from: ast.from, to: ast.to },
11011
+ { step: 2, op: "BFS_SHORTEST_PATH" }
11012
+ ];
11013
+ plan.estimatedCost = graph.size.nodes + graph.size.edges;
11014
+ } else if (ast.type === "COUNT") {
11015
+ plan.steps = [
11016
+ { step: 1, op: "SCAN_NODES", filter: ast.target === "*" ? "all" : `kind=${ast.target}` },
11017
+ ...ast.where ? [{ step: 2, op: "WHERE", conditions: ast.where.exprs.length }] : [],
11018
+ ...ast.groupBy ? [{ step: 3, op: "GROUP_BY", property: ast.groupBy }] : [{ step: 3, op: "COUNT" }]
11019
+ ];
11020
+ plan.estimatedCost = graph.size.nodes;
11021
+ }
11022
+ res.json({ plan, graphSize: graph.size });
11023
+ } catch (err) {
11024
+ res.status(500).json({ error: { code: ErrorCodes.INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err), requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
11025
+ }
11026
+ });
8985
11027
  if (fs19.existsSync(WEB_DIST)) {
8986
11028
  app.use(express.static(WEB_DIST));
8987
11029
  app.get("/{*path}", (_req, res) => {
8988
- res.sendFile(path26.join(WEB_DIST, "index.html"));
11030
+ res.sendFile(path27.join(WEB_DIST, "index.html"));
8989
11031
  });
8990
11032
  }
8991
11033
  app.use("/admin", requireRole("admin"));