@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/cli/main.js CHANGED
@@ -7,8 +7,8 @@ import { SEMRESATTRS_DEPLOYMENT_ENVIRONMENT, SEMRESATTRS_SERVICE_NAME } from '@o
7
7
  import { SpanStatusCode, trace, context } from '@opentelemetry/api';
8
8
  import winston from 'winston';
9
9
  import DailyRotateFile from 'winston-daily-rotate-file';
10
- import fs27, { readFileSync, existsSync } from 'fs';
11
- import path29, { dirname, join } from 'path';
10
+ import fs28, { readFileSync, existsSync } from 'fs';
11
+ import path30, { dirname, join } from 'path';
12
12
  import os12 from 'os';
13
13
  import { Registry, collectDefaultMetrics, Counter, Histogram, Gauge } from 'prom-client';
14
14
  import { createRequire } from 'module';
@@ -26,6 +26,7 @@ import { Command } from 'commander';
26
26
  import { Worker } from 'worker_threads';
27
27
  import { EventEmitter } from 'events';
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';
@@ -325,7 +326,7 @@ var init_logger = __esm({
325
326
  };
326
327
  }
327
328
  /** Global log directory: ~/.code-intel/logs */
328
- static LOG_DIR = path29.join(os12.homedir(), ".code-intel", "logs");
329
+ static LOG_DIR = path30.join(os12.homedir(), ".code-intel", "logs");
329
330
  static getLogger() {
330
331
  if (!_Logger.instance) {
331
332
  const isProduction = process.env.NODE_ENV === "production";
@@ -334,12 +335,12 @@ var init_logger = __esm({
334
335
  transports.push(new winston.transports.Console());
335
336
  if (!isProduction) {
336
337
  try {
337
- if (!fs27.existsSync(_Logger.LOG_DIR)) {
338
- fs27.mkdirSync(_Logger.LOG_DIR, { recursive: true });
338
+ if (!fs28.existsSync(_Logger.LOG_DIR)) {
339
+ fs28.mkdirSync(_Logger.LOG_DIR, { recursive: true });
339
340
  }
340
341
  transports.push(
341
342
  new DailyRotateFile({
342
- filename: path29.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
343
+ filename: path30.join(_Logger.LOG_DIR, "%DATE%-code-intel.log"),
343
344
  datePattern: "YYYY-MM-DD",
344
345
  maxSize: "20m",
345
346
  maxFiles: "14d"
@@ -414,25 +415,25 @@ function validateDAG(phases) {
414
415
  const visiting = /* @__PURE__ */ new Set();
415
416
  const visited = /* @__PURE__ */ new Set();
416
417
  const phaseMap = new Map(phases.map((p) => [p.name, p]));
417
- function dfs(name, path30) {
418
+ function dfs(name, path31) {
418
419
  if (visiting.has(name)) {
419
- const cycleStart = path30.indexOf(name);
420
- const cycle = path30.slice(cycleStart).concat(name);
420
+ const cycleStart = path31.indexOf(name);
421
+ const cycle = path31.slice(cycleStart).concat(name);
421
422
  errors.push({ type: "cycle", message: `Cycle detected: ${cycle.join(" \u2192 ")}` });
422
423
  return true;
423
424
  }
424
425
  if (visited.has(name)) return false;
425
426
  visiting.add(name);
426
- path30.push(name);
427
+ path31.push(name);
427
428
  const phase = phaseMap.get(name);
428
429
  if (phase) {
429
430
  for (const dep of phase.dependencies) {
430
- if (dfs(dep, path30)) return true;
431
+ if (dfs(dep, path31)) return true;
431
432
  }
432
433
  }
433
434
  visiting.delete(name);
434
435
  visited.add(name);
435
- path30.pop();
436
+ path31.pop();
436
437
  return false;
437
438
  }
438
439
  for (const phase of phases) {
@@ -692,11 +693,11 @@ var init_id_generator = __esm({
692
693
  }
693
694
  });
694
695
  function findBundledWasmDir() {
695
- const fileDir = path29.dirname(fileURLToPath(import.meta.url));
696
+ const fileDir = path30.dirname(fileURLToPath(import.meta.url));
696
697
  const candidates = [
697
- path29.join(fileDir, "wasm"),
698
+ path30.join(fileDir, "wasm"),
698
699
  // dist/index.js → dist/wasm/
699
- path29.join(fileDir, "../wasm")
700
+ path30.join(fileDir, "../wasm")
700
701
  // dist/cli/main.js → dist/wasm/
701
702
  ];
702
703
  for (const candidate of candidates) {
@@ -737,7 +738,7 @@ function wasmPath(lang) {
737
738
  }
738
739
  const bundled = BUNDLED_WASM_MAP[lang];
739
740
  if (bundled) {
740
- const bundledPath = path29.join(_bundledWasmDir, bundled);
741
+ const bundledPath = path30.join(_bundledWasmDir, bundled);
741
742
  if (existsSync(bundledPath)) return bundledPath;
742
743
  }
743
744
  return null;
@@ -750,14 +751,14 @@ async function initParser() {
750
751
  }
751
752
  async function getLanguage(lang) {
752
753
  if (languageCache.has(lang)) return languageCache.get(lang);
753
- const path30 = wasmPath(lang);
754
- if (!path30) {
754
+ const path31 = wasmPath(lang);
755
+ if (!path31) {
755
756
  languageCache.set(lang, null);
756
757
  return null;
757
758
  }
758
759
  try {
759
760
  await initParser();
760
- const language = await Language.load(path30);
761
+ const language = await Language.load(path31);
761
762
  languageCache.set(lang, language);
762
763
  return language;
763
764
  } catch {
@@ -2182,7 +2183,7 @@ var init_parse_phase = __esm({
2182
2183
  const batch = filePaths.slice(i, i + CONCURRENCY);
2183
2184
  await Promise.all(batch.map(async (filePath) => {
2184
2185
  try {
2185
- const source = await fs27.promises.readFile(filePath, "utf-8");
2186
+ const source = await fs28.promises.readFile(filePath, "utf-8");
2186
2187
  context2.fileCache.set(filePath, source);
2187
2188
  } catch {
2188
2189
  }
@@ -2195,14 +2196,14 @@ var init_parse_phase = __esm({
2195
2196
  const lang = detectLanguage(filePath);
2196
2197
  if (!lang) {
2197
2198
  if (context2.verbose) {
2198
- const relativePath2 = path29.relative(context2.workspaceRoot, filePath);
2199
+ const relativePath2 = path30.relative(context2.workspaceRoot, filePath);
2199
2200
  logger_default.info(` [parse] skipped (no parser): ${relativePath2}`);
2200
2201
  }
2201
2202
  continue;
2202
2203
  }
2203
2204
  const source = context2.fileCache.get(filePath);
2204
2205
  if (!source) continue;
2205
- const relativePath = path29.relative(context2.workspaceRoot, filePath);
2206
+ const relativePath = path30.relative(context2.workspaceRoot, filePath);
2206
2207
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
2207
2208
  const fileNode = context2.graph.getNode(fileNodeId);
2208
2209
  if (fileNode) {
@@ -2448,11 +2449,11 @@ var init_resolve_phase = __esm({
2448
2449
  let heritageEdges = 0;
2449
2450
  const fileIndex = /* @__PURE__ */ new Map();
2450
2451
  for (const fp of filePaths) {
2451
- const rel = path29.relative(workspaceRoot, fp);
2452
+ const rel = path30.relative(workspaceRoot, fp);
2452
2453
  fileIndex.set(rel, fp);
2453
2454
  const noExt = rel.replace(/\.\w+$/, "");
2454
2455
  if (!fileIndex.has(noExt)) fileIndex.set(noExt, fp);
2455
- const base = path29.basename(rel, path29.extname(rel));
2456
+ const base = path30.basename(rel, path30.extname(rel));
2456
2457
  if (!fileIndex.has(base)) fileIndex.set(base, fp);
2457
2458
  }
2458
2459
  const symbolIndex = /* @__PURE__ */ new Map();
@@ -2483,7 +2484,7 @@ var init_resolve_phase = __esm({
2483
2484
  for (const filePath of filePaths) {
2484
2485
  const lang = detectLanguage(filePath);
2485
2486
  if (!lang) continue;
2486
- const relativePath = path29.relative(workspaceRoot, filePath);
2487
+ const relativePath = path30.relative(workspaceRoot, filePath);
2487
2488
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
2488
2489
  const source = fileCache.get(filePath);
2489
2490
  if (!source) continue;
@@ -2496,13 +2497,13 @@ var init_resolve_phase = __esm({
2496
2497
  let resolvedRelPath = null;
2497
2498
  if (cleaned.startsWith(".")) {
2498
2499
  const cleanedNoJs = cleaned.replace(/\.(js|jsx)$/, "");
2499
- const fromDir = path29.dirname(relativePath);
2500
+ const fromDir = path30.dirname(relativePath);
2500
2501
  for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", "/index.ts", "/index.js"]) {
2501
- const candidate = path29.join(fromDir, cleanedNoJs + ext);
2502
- const normalized = path29.normalize(candidate);
2502
+ const candidate = path30.join(fromDir, cleanedNoJs + ext);
2503
+ const normalized = path30.normalize(candidate);
2503
2504
  if (fileIndex.has(normalized)) {
2504
2505
  const absPath = fileIndex.get(normalized);
2505
- resolvedRelPath = path29.relative(workspaceRoot, absPath);
2506
+ resolvedRelPath = path30.relative(workspaceRoot, absPath);
2506
2507
  break;
2507
2508
  }
2508
2509
  }
@@ -2967,7 +2968,7 @@ var init_db_manager = __esm({
2967
2968
  this.dbPath = dbPath;
2968
2969
  }
2969
2970
  async init() {
2970
- fs27.mkdirSync(path29.dirname(this.dbPath), { recursive: true });
2971
+ fs28.mkdirSync(path30.dirname(this.dbPath), { recursive: true });
2971
2972
  this.db = new Database$1(this.dbPath);
2972
2973
  await this.db.init();
2973
2974
  this.conn = new Connection(this.db);
@@ -3057,13 +3058,14 @@ var init_schema = __esm({
3057
3058
  constant: "const_nodes",
3058
3059
  route: "route_nodes",
3059
3060
  cluster: "cluster_nodes",
3060
- flow: "flow_nodes"
3061
+ flow: "flow_nodes",
3062
+ vulnerability: "vuln_nodes"
3061
3063
  };
3062
3064
  ALL_NODE_TABLES = [...new Set(Object.values(NODE_TABLE_MAP))];
3063
3065
  }
3064
3066
  });
3065
3067
  function writeNodeCSVs(graph, outputDir) {
3066
- fs27.mkdirSync(outputDir, { recursive: true });
3068
+ fs28.mkdirSync(outputDir, { recursive: true });
3067
3069
  const header = "id,name,file_path,start_line,end_line,exported,content,metadata\n";
3068
3070
  const tableBuffers = /* @__PURE__ */ new Map();
3069
3071
  const tableFilePaths = /* @__PURE__ */ new Map();
@@ -3071,7 +3073,7 @@ function writeNodeCSVs(graph, outputDir) {
3071
3073
  const table = NODE_TABLE_MAP[node.kind];
3072
3074
  if (!tableBuffers.has(table)) {
3073
3075
  tableBuffers.set(table, [header]);
3074
- tableFilePaths.set(table, path29.join(outputDir, `${table}.csv`));
3076
+ tableFilePaths.set(table, path30.join(outputDir, `${table}.csv`));
3075
3077
  }
3076
3078
  tableBuffers.get(table).push(
3077
3079
  csvRow([
@@ -3091,12 +3093,12 @@ function writeNodeCSVs(graph, outputDir) {
3091
3093
  );
3092
3094
  }
3093
3095
  for (const [table, lines] of tableBuffers) {
3094
- fs27.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
3096
+ fs28.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
3095
3097
  }
3096
3098
  return tableFilePaths;
3097
3099
  }
3098
3100
  function writeEdgeCSV(graph, outputDir) {
3099
- fs27.mkdirSync(outputDir, { recursive: true });
3101
+ fs28.mkdirSync(outputDir, { recursive: true });
3100
3102
  const header = "from_id,to_id,kind,weight,label\n";
3101
3103
  const groups = /* @__PURE__ */ new Map();
3102
3104
  for (const edge of graph.allEdges()) {
@@ -3107,7 +3109,7 @@ function writeEdgeCSV(graph, outputDir) {
3107
3109
  const toTable = NODE_TABLE_MAP[targetNode.kind];
3108
3110
  const key = `${fromTable}->${toTable}`;
3109
3111
  if (!groups.has(key)) {
3110
- const filePath = path29.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
3112
+ const filePath = path30.join(outputDir, `edges_${fromTable}_${toTable}.csv`);
3111
3113
  groups.set(key, { lines: [header], from: fromTable, to: toTable, filePath });
3112
3114
  }
3113
3115
  groups.get(key).lines.push(
@@ -3122,7 +3124,7 @@ function writeEdgeCSV(graph, outputDir) {
3122
3124
  }
3123
3125
  const result = [];
3124
3126
  for (const group of groups.values()) {
3125
- fs27.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
3127
+ fs28.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
3126
3128
  result.push({ fromTable: group.from, toTable: group.to, filePath: group.filePath });
3127
3129
  }
3128
3130
  return result;
@@ -3165,7 +3167,7 @@ async function loadGraphToDB(graph, dbManager) {
3165
3167
  } catch {
3166
3168
  }
3167
3169
  }
3168
- const tmpDir = fs27.mkdtempSync(path29.join(os12.tmpdir(), "code-intel-csv-"));
3170
+ const tmpDir = fs28.mkdtempSync(path30.join(os12.tmpdir(), "code-intel-csv-"));
3169
3171
  try {
3170
3172
  const nodeTableFiles = writeNodeCSVs(graph, tmpDir);
3171
3173
  const edgeGroups = writeEdgeCSV(graph, tmpDir);
@@ -3184,8 +3186,8 @@ async function loadGraphToDB(graph, dbManager) {
3184
3186
  }
3185
3187
  let nodeCount = 0;
3186
3188
  for (const [table, csvPath] of nodeTableFiles) {
3187
- if (!fs27.existsSync(csvPath)) continue;
3188
- const stat = fs27.statSync(csvPath);
3189
+ if (!fs28.existsSync(csvPath)) continue;
3190
+ const stat = fs28.statSync(csvPath);
3189
3191
  if (stat.size < 50) continue;
3190
3192
  try {
3191
3193
  await dbManager.execute(
@@ -3198,8 +3200,8 @@ async function loadGraphToDB(graph, dbManager) {
3198
3200
  }
3199
3201
  let edgeCount = 0;
3200
3202
  for (const group of edgeGroups) {
3201
- if (!fs27.existsSync(group.filePath)) continue;
3202
- const stat = fs27.statSync(group.filePath);
3203
+ if (!fs28.existsSync(group.filePath)) continue;
3204
+ const stat = fs28.statSync(group.filePath);
3203
3205
  if (stat.size < 50) continue;
3204
3206
  try {
3205
3207
  await dbManager.execute(
@@ -3213,7 +3215,7 @@ async function loadGraphToDB(graph, dbManager) {
3213
3215
  return { nodeCount, edgeCount };
3214
3216
  } finally {
3215
3217
  try {
3216
- fs27.rmSync(tmpDir, { recursive: true, force: true });
3218
+ fs28.rmSync(tmpDir, { recursive: true, force: true });
3217
3219
  } catch {
3218
3220
  }
3219
3221
  }
@@ -3315,15 +3317,15 @@ var init_graph_loader = __esm({
3315
3317
  });
3316
3318
  function loadRegistry() {
3317
3319
  try {
3318
- const data = fs27.readFileSync(REPOS_FILE, "utf-8");
3320
+ const data = fs28.readFileSync(REPOS_FILE, "utf-8");
3319
3321
  return JSON.parse(data);
3320
3322
  } catch {
3321
3323
  return [];
3322
3324
  }
3323
3325
  }
3324
3326
  function saveRegistry(entries) {
3325
- fs27.mkdirSync(GLOBAL_DIR, { recursive: true });
3326
- fs27.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
3327
+ fs28.mkdirSync(GLOBAL_DIR, { recursive: true });
3328
+ fs28.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
3327
3329
  }
3328
3330
  function upsertRepo(entry) {
3329
3331
  const entries = loadRegistry();
@@ -3342,28 +3344,28 @@ function removeRepo(repoPath) {
3342
3344
  var GLOBAL_DIR, REPOS_FILE;
3343
3345
  var init_repo_registry = __esm({
3344
3346
  "src/storage/repo-registry.ts"() {
3345
- GLOBAL_DIR = path29.join(os12.homedir(), ".code-intel");
3346
- REPOS_FILE = path29.join(GLOBAL_DIR, "repos.json");
3347
+ GLOBAL_DIR = path30.join(os12.homedir(), ".code-intel");
3348
+ REPOS_FILE = path30.join(GLOBAL_DIR, "repos.json");
3347
3349
  }
3348
3350
  });
3349
3351
  function saveMetadata(repoDir, metadata) {
3350
- const metaDir = path29.join(repoDir, ".code-intel");
3351
- fs27.mkdirSync(metaDir, { recursive: true });
3352
- fs27.writeFileSync(path29.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
3352
+ const metaDir = path30.join(repoDir, ".code-intel");
3353
+ fs28.mkdirSync(metaDir, { recursive: true });
3354
+ fs28.writeFileSync(path30.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
3353
3355
  }
3354
3356
  function loadMetadata(repoDir) {
3355
3357
  try {
3356
- const data = fs27.readFileSync(path29.join(repoDir, ".code-intel", "meta.json"), "utf-8");
3358
+ const data = fs28.readFileSync(path30.join(repoDir, ".code-intel", "meta.json"), "utf-8");
3357
3359
  return JSON.parse(data);
3358
3360
  } catch {
3359
3361
  return null;
3360
3362
  }
3361
3363
  }
3362
3364
  function getDbPath(repoDir) {
3363
- return path29.join(repoDir, ".code-intel", "graph.db");
3365
+ return path30.join(repoDir, ".code-intel", "graph.db");
3364
3366
  }
3365
3367
  function getVectorDbPath(repoDir) {
3366
- return path29.join(repoDir, ".code-intel", "vector.db");
3368
+ return path30.join(repoDir, ".code-intel", "vector.db");
3367
3369
  }
3368
3370
  var init_metadata = __esm({
3369
3371
  "src/storage/metadata.ts"() {
@@ -3419,27 +3421,27 @@ __export(group_registry_exports, {
3419
3421
  saveSyncResult: () => saveSyncResult
3420
3422
  });
3421
3423
  function groupFile(name) {
3422
- return path29.join(GROUPS_DIR, `${name}.json`);
3424
+ return path30.join(GROUPS_DIR, `${name}.json`);
3423
3425
  }
3424
3426
  function loadGroup(name) {
3425
3427
  try {
3426
- return JSON.parse(fs27.readFileSync(groupFile(name), "utf-8"));
3428
+ return JSON.parse(fs28.readFileSync(groupFile(name), "utf-8"));
3427
3429
  } catch {
3428
3430
  return null;
3429
3431
  }
3430
3432
  }
3431
3433
  function saveGroup(group) {
3432
- fs27.mkdirSync(GROUPS_DIR, { recursive: true });
3433
- fs27.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
3434
+ fs28.mkdirSync(GROUPS_DIR, { recursive: true });
3435
+ fs28.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
3434
3436
  }
3435
3437
  function listGroups() {
3436
3438
  const groups = [];
3437
3439
  try {
3438
- for (const file of fs27.readdirSync(GROUPS_DIR)) {
3440
+ for (const file of fs28.readdirSync(GROUPS_DIR)) {
3439
3441
  if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
3440
3442
  try {
3441
3443
  const g = JSON.parse(
3442
- fs27.readFileSync(path29.join(GROUPS_DIR, file), "utf-8")
3444
+ fs28.readFileSync(path30.join(GROUPS_DIR, file), "utf-8")
3443
3445
  );
3444
3446
  groups.push(g);
3445
3447
  } catch {
@@ -3451,16 +3453,16 @@ function listGroups() {
3451
3453
  }
3452
3454
  function deleteGroup(name) {
3453
3455
  try {
3454
- fs27.unlinkSync(groupFile(name));
3456
+ fs28.unlinkSync(groupFile(name));
3455
3457
  } catch {
3456
3458
  }
3457
3459
  try {
3458
- fs27.unlinkSync(path29.join(GROUPS_DIR, `${name}.sync.json`));
3460
+ fs28.unlinkSync(path30.join(GROUPS_DIR, `${name}.sync.json`));
3459
3461
  } catch {
3460
3462
  }
3461
3463
  }
3462
3464
  function groupExists(name) {
3463
- return fs27.existsSync(groupFile(name));
3465
+ return fs28.existsSync(groupFile(name));
3464
3466
  }
3465
3467
  function addMember(groupName, member) {
3466
3468
  const group = loadGroup(groupName);
@@ -3486,16 +3488,16 @@ function removeMember(groupName, groupPath) {
3486
3488
  return group;
3487
3489
  }
3488
3490
  function saveSyncResult(result) {
3489
- fs27.mkdirSync(GROUPS_DIR, { recursive: true });
3490
- fs27.writeFileSync(
3491
- path29.join(GROUPS_DIR, `${result.groupName}.sync.json`),
3491
+ fs28.mkdirSync(GROUPS_DIR, { recursive: true });
3492
+ fs28.writeFileSync(
3493
+ path30.join(GROUPS_DIR, `${result.groupName}.sync.json`),
3492
3494
  JSON.stringify(result, null, 2) + "\n"
3493
3495
  );
3494
3496
  }
3495
3497
  function loadSyncResult(groupName) {
3496
3498
  try {
3497
3499
  return JSON.parse(
3498
- fs27.readFileSync(path29.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
3500
+ fs28.readFileSync(path30.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
3499
3501
  );
3500
3502
  } catch {
3501
3503
  return null;
@@ -3504,7 +3506,7 @@ function loadSyncResult(groupName) {
3504
3506
  var GROUPS_DIR;
3505
3507
  var init_group_registry = __esm({
3506
3508
  "src/multi-repo/group-registry.ts"() {
3507
- GROUPS_DIR = path29.join(os12.homedir(), ".code-intel", "groups");
3509
+ GROUPS_DIR = path30.join(os12.homedir(), ".code-intel", "groups");
3508
3510
  }
3509
3511
  });
3510
3512
 
@@ -3541,10 +3543,10 @@ var init_codes = __esm({
3541
3543
  }
3542
3544
  });
3543
3545
  function secureMkdir(dir) {
3544
- fs27.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
3546
+ fs28.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
3545
3547
  if (process.platform !== "win32") {
3546
3548
  try {
3547
- fs27.chmodSync(dir, SECURE_DIR_MODE);
3549
+ fs28.chmodSync(dir, SECURE_DIR_MODE);
3548
3550
  } catch {
3549
3551
  }
3550
3552
  }
@@ -3552,22 +3554,22 @@ function secureMkdir(dir) {
3552
3554
  function secureChmodFile(file) {
3553
3555
  if (process.platform === "win32") return;
3554
3556
  try {
3555
- fs27.chmodSync(file, SECURE_FILE_MODE);
3557
+ fs28.chmodSync(file, SECURE_FILE_MODE);
3556
3558
  } catch {
3557
3559
  }
3558
3560
  }
3559
3561
  function secureWriteFile(file, data) {
3560
- secureMkdir(path29.dirname(file));
3561
- fs27.writeFileSync(file, data, { mode: SECURE_FILE_MODE });
3562
+ secureMkdir(path30.dirname(file));
3563
+ fs28.writeFileSync(file, data, { mode: SECURE_FILE_MODE });
3562
3564
  secureChmodFile(file);
3563
3565
  }
3564
3566
  function tightenDbFiles(dir) {
3565
3567
  if (process.platform === "win32") return;
3566
- if (!fs27.existsSync(dir)) return;
3567
- for (const name of fs27.readdirSync(dir)) {
3568
+ if (!fs28.existsSync(dir)) return;
3569
+ for (const name of fs28.readdirSync(dir)) {
3568
3570
  if (name.endsWith(".db") || name.endsWith(".db-wal") || name.endsWith(".db-shm")) {
3569
3571
  try {
3570
- fs27.chmodSync(path29.join(dir, name), SECURE_FILE_MODE);
3572
+ fs28.chmodSync(path30.join(dir, name), SECURE_FILE_MODE);
3571
3573
  } catch {
3572
3574
  }
3573
3575
  }
@@ -3581,7 +3583,7 @@ var init_fs_secure = __esm({
3581
3583
  }
3582
3584
  });
3583
3585
  function getUsersDBPath() {
3584
- return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path29.join(os12.homedir(), ".code-intel", "users.db");
3586
+ return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path30.join(os12.homedir(), ".code-intel", "users.db");
3585
3587
  }
3586
3588
  function getOrCreateUsersDB() {
3587
3589
  if (!_usersDB) {
@@ -3597,7 +3599,7 @@ var init_users_db = __esm({
3597
3599
  UsersDB = class {
3598
3600
  db;
3599
3601
  constructor(dbPath) {
3600
- const dir = path29.dirname(dbPath);
3602
+ const dir = path30.dirname(dbPath);
3601
3603
  secureMkdir(dir);
3602
3604
  this.db = new Database(dbPath);
3603
3605
  this.db.pragma("journal_mode = WAL");
@@ -3874,7 +3876,7 @@ function getScryptN() {
3874
3876
  return Number.isInteger(v) && v >= 1024 ? v : 1 << 14;
3875
3877
  }
3876
3878
  function getSecretsPath() {
3877
- return process.env["CODE_INTEL_SECRETS_PATH"] ?? path29.join(os12.homedir(), ".code-intel", ".secrets");
3879
+ return process.env["CODE_INTEL_SECRETS_PATH"] ?? path30.join(os12.homedir(), ".code-intel", ".secrets");
3878
3880
  }
3879
3881
  function getMasterPassword() {
3880
3882
  const fromEnv = process.env["CODE_INTEL_SECRET_KEY"];
@@ -3916,12 +3918,12 @@ function decryptSecrets(encrypted) {
3916
3918
  return JSON.parse(plaintext.toString("utf8"));
3917
3919
  }
3918
3920
  function loadSecrets(secretsPath = getSecretsPath()) {
3919
- if (!fs27.existsSync(secretsPath)) return {};
3920
- const blob = fs27.readFileSync(secretsPath);
3921
+ if (!fs28.existsSync(secretsPath)) return {};
3922
+ const blob = fs28.readFileSync(secretsPath);
3921
3923
  return decryptSecrets(blob);
3922
3924
  }
3923
3925
  function saveSecrets(blob, secretsPath = getSecretsPath()) {
3924
- secureMkdir(path29.dirname(secretsPath));
3926
+ secureMkdir(path30.dirname(secretsPath));
3925
3927
  const encrypted = encryptSecrets(blob);
3926
3928
  secureWriteFile(secretsPath, encrypted);
3927
3929
  secureChmodFile(secretsPath);
@@ -4395,6 +4397,753 @@ var init_oidc = __esm({
4395
4397
  _cachedIssuer = "";
4396
4398
  }
4397
4399
  });
4400
+
4401
+ // src/query/gql-parser.ts
4402
+ var gql_parser_exports = {};
4403
+ __export(gql_parser_exports, {
4404
+ isGQLParseError: () => isGQLParseError,
4405
+ parseGQL: () => parseGQL
4406
+ });
4407
+ function isGQLParseError(v) {
4408
+ return v.type === "GQLParseError";
4409
+ }
4410
+ function tokenize(input) {
4411
+ const tokens = [];
4412
+ let i = 0;
4413
+ const len = input.length;
4414
+ while (i < len) {
4415
+ if (/\s/.test(input[i])) {
4416
+ i++;
4417
+ continue;
4418
+ }
4419
+ if (input[i] === "#") {
4420
+ while (i < len && input[i] !== "\n") i++;
4421
+ continue;
4422
+ }
4423
+ const pos = i;
4424
+ if (input[i] === '"' || input[i] === "'") {
4425
+ const quote = input[i];
4426
+ i++;
4427
+ let str = "";
4428
+ while (i < len && input[i] !== quote) {
4429
+ if (input[i] === "\\") {
4430
+ i++;
4431
+ if (i < len) {
4432
+ const esc = input[i];
4433
+ str += esc === "n" ? "\n" : esc === "t" ? " " : esc;
4434
+ i++;
4435
+ }
4436
+ } else {
4437
+ str += input[i++];
4438
+ }
4439
+ }
4440
+ if (i >= len) {
4441
+ return { type: "GQLParseError", message: `Unterminated string at position ${pos}`, pos };
4442
+ }
4443
+ i++;
4444
+ tokens.push({ kind: "STRING", value: str, pos });
4445
+ continue;
4446
+ }
4447
+ if (/[0-9]/.test(input[i])) {
4448
+ let num = "";
4449
+ while (i < len && /[0-9]/.test(input[i])) num += input[i++];
4450
+ tokens.push({ kind: "NUMBER", value: num, pos });
4451
+ continue;
4452
+ }
4453
+ if (input[i] === "[") {
4454
+ tokens.push({ kind: "LBRACKET", value: "[", pos });
4455
+ i++;
4456
+ continue;
4457
+ }
4458
+ if (input[i] === "]") {
4459
+ tokens.push({ kind: "RBRACKET", value: "]", pos });
4460
+ i++;
4461
+ continue;
4462
+ }
4463
+ if (input[i] === "*") {
4464
+ tokens.push({ kind: "STAR", value: "*", pos });
4465
+ i++;
4466
+ continue;
4467
+ }
4468
+ if (input[i] === "!" && input[i + 1] === "=") {
4469
+ tokens.push({ kind: "OPERATOR", value: "!=", pos });
4470
+ i += 2;
4471
+ continue;
4472
+ }
4473
+ if (input[i] === "=") {
4474
+ tokens.push({ kind: "OPERATOR", value: "=", pos });
4475
+ i++;
4476
+ continue;
4477
+ }
4478
+ if (/[a-zA-Z_]/.test(input[i])) {
4479
+ let ident = "";
4480
+ while (i < len && /[a-zA-Z0-9_]/.test(input[i])) ident += input[i++];
4481
+ const upper = ident.toUpperCase();
4482
+ if (upper === "CONTAINS" || upper === "STARTS_WITH" || upper === "IN") {
4483
+ tokens.push({ kind: "OPERATOR", value: upper, pos });
4484
+ } else if (KEYWORDS.has(upper)) {
4485
+ tokens.push({ kind: "KEYWORD", value: upper, pos });
4486
+ } else {
4487
+ tokens.push({ kind: "IDENT", value: ident, pos });
4488
+ }
4489
+ continue;
4490
+ }
4491
+ if (input[i] === ",") {
4492
+ i++;
4493
+ continue;
4494
+ }
4495
+ return {
4496
+ type: "GQLParseError",
4497
+ message: `Unexpected character '${input[i]}' at position ${i}`,
4498
+ pos: i
4499
+ };
4500
+ }
4501
+ tokens.push({ kind: "EOF", value: "", pos: len });
4502
+ return tokens;
4503
+ }
4504
+ function parseGQL(input) {
4505
+ const tokens = tokenize(input.trim());
4506
+ if (!Array.isArray(tokens)) return tokens;
4507
+ const parser = new Parser2(tokens);
4508
+ return parser.parse();
4509
+ }
4510
+ var KEYWORDS, Parser2;
4511
+ var init_gql_parser = __esm({
4512
+ "src/query/gql-parser.ts"() {
4513
+ KEYWORDS = /* @__PURE__ */ new Set([
4514
+ "FIND",
4515
+ "TRAVERSE",
4516
+ "PATH",
4517
+ "COUNT",
4518
+ "WHERE",
4519
+ "FROM",
4520
+ "TO",
4521
+ "IN",
4522
+ "BY",
4523
+ "AND",
4524
+ "NOT",
4525
+ "LIMIT",
4526
+ "OFFSET",
4527
+ "DEPTH",
4528
+ "GROUP",
4529
+ "CONTAINS",
4530
+ "STARTS_WITH",
4531
+ "CALLS",
4532
+ "IMPORTS",
4533
+ "EXTENDS",
4534
+ "IMPLEMENTS",
4535
+ "HAS_MEMBER",
4536
+ "ACCESSES",
4537
+ "OVERRIDES",
4538
+ "BELONGS_TO",
4539
+ "STEP_OF",
4540
+ "HANDLES",
4541
+ "CONTAINS_EDGE",
4542
+ "OUTGOING",
4543
+ "INCOMING",
4544
+ "BOTH"
4545
+ ]);
4546
+ Parser2 = class {
4547
+ tokens;
4548
+ pos = 0;
4549
+ constructor(tokens) {
4550
+ this.tokens = tokens;
4551
+ }
4552
+ peek() {
4553
+ return this.tokens[this.pos];
4554
+ }
4555
+ consume() {
4556
+ return this.tokens[this.pos++];
4557
+ }
4558
+ expect(kind, value) {
4559
+ const tok = this.peek();
4560
+ if (tok.kind !== kind) {
4561
+ return {
4562
+ type: "GQLParseError",
4563
+ message: `Expected ${value ?? kind} but got '${tok.value}' at position ${tok.pos}`,
4564
+ pos: tok.pos,
4565
+ expected: value ?? kind,
4566
+ got: tok.value
4567
+ };
4568
+ }
4569
+ if (value !== void 0 && tok.value !== value) {
4570
+ return {
4571
+ type: "GQLParseError",
4572
+ message: `Expected '${value}' but got '${tok.value}' at position ${tok.pos}`,
4573
+ pos: tok.pos,
4574
+ expected: value,
4575
+ got: tok.value
4576
+ };
4577
+ }
4578
+ return this.consume();
4579
+ }
4580
+ matchKeyword(...values) {
4581
+ const tok = this.peek();
4582
+ return tok.kind === "KEYWORD" && values.includes(tok.value);
4583
+ }
4584
+ optionalKeyword(...values) {
4585
+ if (this.matchKeyword(...values)) {
4586
+ return this.consume();
4587
+ }
4588
+ return null;
4589
+ }
4590
+ /** Parse the node kind filter (IDENT, KEYWORD that's a kind, or STAR) */
4591
+ parseNodeKind() {
4592
+ const tok = this.peek();
4593
+ if (tok.kind === "STAR") {
4594
+ this.consume();
4595
+ return "*";
4596
+ }
4597
+ if (tok.kind === "IDENT" || tok.kind === "KEYWORD") {
4598
+ this.consume();
4599
+ return tok.value.toLowerCase();
4600
+ }
4601
+ return {
4602
+ type: "GQLParseError",
4603
+ message: `Expected node kind or '*' at position ${tok.pos}`,
4604
+ pos: tok.pos
4605
+ };
4606
+ }
4607
+ /** Parse a string value (STRING or IDENT) */
4608
+ parseStringValue() {
4609
+ const tok = this.peek();
4610
+ if (tok.kind === "STRING") {
4611
+ this.consume();
4612
+ return tok.value;
4613
+ }
4614
+ if (tok.kind === "IDENT" || tok.kind === "KEYWORD") {
4615
+ this.consume();
4616
+ return tok.value;
4617
+ }
4618
+ return {
4619
+ type: "GQLParseError",
4620
+ message: `Expected string value at position ${tok.pos}`,
4621
+ pos: tok.pos
4622
+ };
4623
+ }
4624
+ /** Parse an IN list: [ value, value, ... ] */
4625
+ parseInList() {
4626
+ const lb = this.expect("LBRACKET");
4627
+ if (isGQLParseError(lb)) return lb;
4628
+ const values = [];
4629
+ while (!this.matchKeyword() && this.peek().kind !== "RBRACKET" && this.peek().kind !== "EOF") {
4630
+ const v = this.parseStringValue();
4631
+ if (typeof v !== "string") return v;
4632
+ values.push(v);
4633
+ }
4634
+ const rb = this.expect("RBRACKET");
4635
+ if (isGQLParseError(rb)) return rb;
4636
+ return values;
4637
+ }
4638
+ /** Parse a single WHERE expression */
4639
+ parseWhereExpr() {
4640
+ const propTok = this.peek();
4641
+ if (propTok.kind !== "IDENT" && propTok.kind !== "KEYWORD") {
4642
+ return {
4643
+ type: "GQLParseError",
4644
+ message: `Expected property name at position ${propTok.pos}`,
4645
+ pos: propTok.pos
4646
+ };
4647
+ }
4648
+ this.consume();
4649
+ const property = propTok.value.toLowerCase();
4650
+ const opTok = this.peek();
4651
+ if (opTok.kind !== "OPERATOR") {
4652
+ return {
4653
+ type: "GQLParseError",
4654
+ message: `Expected operator (=, !=, CONTAINS, STARTS_WITH, IN) at position ${opTok.pos}`,
4655
+ pos: opTok.pos,
4656
+ expected: "operator",
4657
+ got: opTok.value
4658
+ };
4659
+ }
4660
+ this.consume();
4661
+ const operator = opTok.value;
4662
+ if (operator === "IN") {
4663
+ const list = this.parseInList();
4664
+ if (!Array.isArray(list)) return list;
4665
+ return { property, operator, value: list };
4666
+ }
4667
+ const val = this.parseStringValue();
4668
+ if (typeof val !== "string") return val;
4669
+ return { property, operator, value: val };
4670
+ }
4671
+ /** Parse WHERE clause: WHERE expr (AND expr)* */
4672
+ parseWhereClause() {
4673
+ const kw = this.expect("KEYWORD", "WHERE");
4674
+ if (isGQLParseError(kw)) return kw;
4675
+ const exprs = [];
4676
+ const first = this.parseWhereExpr();
4677
+ if ("type" in first && first.type === "GQLParseError") return first;
4678
+ exprs.push(first);
4679
+ while (this.matchKeyword("AND")) {
4680
+ this.consume();
4681
+ const expr = this.parseWhereExpr();
4682
+ if ("type" in expr && expr.type === "GQLParseError") return expr;
4683
+ exprs.push(expr);
4684
+ }
4685
+ return { exprs };
4686
+ }
4687
+ /** Parse FIND statement */
4688
+ parseFindStatement() {
4689
+ this.consume();
4690
+ const kind = this.parseNodeKind();
4691
+ if (typeof kind !== "string") return kind;
4692
+ let where;
4693
+ if (this.matchKeyword("WHERE")) {
4694
+ const w = this.parseWhereClause();
4695
+ if ("type" in w && w.type === "GQLParseError") return w;
4696
+ where = w;
4697
+ }
4698
+ let limit;
4699
+ let offset;
4700
+ while (this.matchKeyword("LIMIT", "OFFSET")) {
4701
+ const kw = this.consume();
4702
+ const numTok = this.peek();
4703
+ if (numTok.kind !== "NUMBER") {
4704
+ return {
4705
+ type: "GQLParseError",
4706
+ message: `Expected number after ${kw.value} at position ${numTok.pos}`,
4707
+ pos: numTok.pos
4708
+ };
4709
+ }
4710
+ this.consume();
4711
+ const n = parseInt(numTok.value, 10);
4712
+ if (kw.value === "LIMIT") limit = n;
4713
+ else offset = n;
4714
+ }
4715
+ return { type: "FIND", target: kind, where, limit, offset };
4716
+ }
4717
+ /** Parse TRAVERSE statement */
4718
+ parseTraverseStatement() {
4719
+ this.consume();
4720
+ const edgeTok = this.peek();
4721
+ if (edgeTok.kind !== "KEYWORD" && edgeTok.kind !== "IDENT") {
4722
+ return {
4723
+ type: "GQLParseError",
4724
+ message: `Expected edge kind after TRAVERSE at position ${edgeTok.pos}`,
4725
+ pos: edgeTok.pos
4726
+ };
4727
+ }
4728
+ this.consume();
4729
+ const edgeKind = edgeTok.value.toLowerCase();
4730
+ const fromKw = this.expect("KEYWORD", "FROM");
4731
+ if (isGQLParseError(fromKw)) return fromKw;
4732
+ const fromVal = this.parseStringValue();
4733
+ if (typeof fromVal !== "string") return fromVal;
4734
+ let depth;
4735
+ let direction;
4736
+ if (this.matchKeyword("DEPTH")) {
4737
+ this.consume();
4738
+ const numTok = this.peek();
4739
+ if (numTok.kind !== "NUMBER") {
4740
+ return {
4741
+ type: "GQLParseError",
4742
+ message: `Expected number after DEPTH at position ${numTok.pos}`,
4743
+ pos: numTok.pos
4744
+ };
4745
+ }
4746
+ this.consume();
4747
+ depth = parseInt(numTok.value, 10);
4748
+ }
4749
+ if (this.matchKeyword("OUTGOING", "INCOMING", "BOTH")) {
4750
+ direction = this.consume().value;
4751
+ }
4752
+ return { type: "TRAVERSE", edgeKind, from: fromVal, depth, direction };
4753
+ }
4754
+ /** Parse PATH statement */
4755
+ parsePathStatement() {
4756
+ this.consume();
4757
+ const fromKw = this.expect("KEYWORD", "FROM");
4758
+ if (isGQLParseError(fromKw)) return fromKw;
4759
+ const fromVal = this.parseStringValue();
4760
+ if (typeof fromVal !== "string") return fromVal;
4761
+ const toKw = this.expect("KEYWORD", "TO");
4762
+ if (isGQLParseError(toKw)) return toKw;
4763
+ const toVal = this.parseStringValue();
4764
+ if (typeof toVal !== "string") return toVal;
4765
+ return { type: "PATH", from: fromVal, to: toVal };
4766
+ }
4767
+ /** Parse COUNT statement */
4768
+ parseCountStatement() {
4769
+ this.consume();
4770
+ const kind = this.parseNodeKind();
4771
+ if (typeof kind !== "string") return kind;
4772
+ let where;
4773
+ if (this.matchKeyword("WHERE")) {
4774
+ const w = this.parseWhereClause();
4775
+ if ("type" in w && w.type === "GQLParseError") return w;
4776
+ where = w;
4777
+ }
4778
+ let groupBy;
4779
+ if (this.matchKeyword("GROUP")) {
4780
+ this.consume();
4781
+ const byKw = this.expect("KEYWORD", "BY");
4782
+ if (isGQLParseError(byKw)) return byKw;
4783
+ const propTok = this.peek();
4784
+ if (propTok.kind !== "IDENT" && propTok.kind !== "KEYWORD") {
4785
+ return {
4786
+ type: "GQLParseError",
4787
+ message: `Expected property name after GROUP BY at position ${propTok.pos}`,
4788
+ pos: propTok.pos
4789
+ };
4790
+ }
4791
+ this.consume();
4792
+ groupBy = propTok.value.toLowerCase();
4793
+ }
4794
+ return { type: "COUNT", target: kind, where, groupBy };
4795
+ }
4796
+ parse() {
4797
+ const tok = this.peek();
4798
+ if (tok.kind !== "KEYWORD") {
4799
+ return {
4800
+ type: "GQLParseError",
4801
+ message: `Expected FIND, TRAVERSE, PATH, or COUNT at position ${tok.pos}`,
4802
+ pos: tok.pos,
4803
+ expected: "FIND | TRAVERSE | PATH | COUNT",
4804
+ got: tok.value
4805
+ };
4806
+ }
4807
+ let result;
4808
+ switch (tok.value) {
4809
+ case "FIND":
4810
+ result = this.parseFindStatement();
4811
+ break;
4812
+ case "TRAVERSE":
4813
+ result = this.parseTraverseStatement();
4814
+ break;
4815
+ case "PATH":
4816
+ result = this.parsePathStatement();
4817
+ break;
4818
+ case "COUNT":
4819
+ result = this.parseCountStatement();
4820
+ break;
4821
+ default:
4822
+ return {
4823
+ type: "GQLParseError",
4824
+ message: `Unknown statement type '${tok.value}' at position ${tok.pos}`,
4825
+ pos: tok.pos,
4826
+ expected: "FIND | TRAVERSE | PATH | COUNT",
4827
+ got: tok.value
4828
+ };
4829
+ }
4830
+ if (isGQLParseError(result)) return result;
4831
+ const remaining = this.peek();
4832
+ if (remaining.kind !== "EOF") {
4833
+ return {
4834
+ type: "GQLParseError",
4835
+ message: `Unexpected token '${remaining.value}' at position ${remaining.pos}`,
4836
+ pos: remaining.pos,
4837
+ got: remaining.value
4838
+ };
4839
+ }
4840
+ return result;
4841
+ }
4842
+ };
4843
+ }
4844
+ });
4845
+
4846
+ // src/query/gql-executor.ts
4847
+ var gql_executor_exports = {};
4848
+ __export(gql_executor_exports, {
4849
+ executeGQL: () => executeGQL
4850
+ });
4851
+ function getNodeProperty(node, property) {
4852
+ switch (property) {
4853
+ case "name":
4854
+ return node.name;
4855
+ case "kind":
4856
+ return node.kind;
4857
+ case "filepath":
4858
+ case "filePath":
4859
+ return node.filePath;
4860
+ case "exported":
4861
+ return node.exported;
4862
+ case "language":
4863
+ return node.metadata?.language ?? void 0;
4864
+ case "cluster":
4865
+ return node.metadata?.cluster ?? void 0;
4866
+ default:
4867
+ return node.metadata?.[property] ?? void 0;
4868
+ }
4869
+ }
4870
+ function evaluateExpr(node, expr) {
4871
+ const val = getNodeProperty(node, expr.property);
4872
+ if (val === void 0) return false;
4873
+ const strVal = String(val).toLowerCase();
4874
+ switch (expr.operator) {
4875
+ case "=":
4876
+ if (typeof expr.value === "string") {
4877
+ return strVal === expr.value.toLowerCase();
4878
+ }
4879
+ return false;
4880
+ case "!=":
4881
+ if (typeof expr.value === "string") {
4882
+ return strVal !== expr.value.toLowerCase();
4883
+ }
4884
+ return true;
4885
+ case "CONTAINS":
4886
+ if (typeof expr.value === "string") {
4887
+ return strVal.includes(expr.value.toLowerCase());
4888
+ }
4889
+ return false;
4890
+ case "STARTS_WITH":
4891
+ if (typeof expr.value === "string") {
4892
+ return strVal.startsWith(expr.value.toLowerCase());
4893
+ }
4894
+ return false;
4895
+ case "IN":
4896
+ if (Array.isArray(expr.value)) {
4897
+ return expr.value.some((v) => strVal === v.toLowerCase());
4898
+ }
4899
+ return false;
4900
+ default:
4901
+ return false;
4902
+ }
4903
+ }
4904
+ function evaluateWhere(node, where) {
4905
+ return where.exprs.every((expr) => evaluateExpr(node, expr));
4906
+ }
4907
+ function executeFIND(stmt, graph) {
4908
+ const start = Date.now();
4909
+ const limit = stmt.limit ?? 1e3;
4910
+ const offset = stmt.offset ?? 0;
4911
+ let totalCount = 0;
4912
+ let truncated = false;
4913
+ const allMatching = [];
4914
+ const deadline = start + EXECUTION_TIMEOUT_MS;
4915
+ for (const node of graph.allNodes()) {
4916
+ if (Date.now() > deadline) {
4917
+ truncated = true;
4918
+ break;
4919
+ }
4920
+ if (stmt.target !== "*" && node.kind !== stmt.target) continue;
4921
+ if (stmt.where && !evaluateWhere(node, stmt.where)) continue;
4922
+ allMatching.push(node);
4923
+ }
4924
+ totalCount = allMatching.length;
4925
+ const paginated = allMatching.slice(offset, offset + limit);
4926
+ return {
4927
+ nodes: paginated,
4928
+ executionTimeMs: Date.now() - start,
4929
+ truncated,
4930
+ totalCount
4931
+ };
4932
+ }
4933
+ function executeTRAVERSE(stmt, graph) {
4934
+ const start = Date.now();
4935
+ const maxDepth = stmt.depth ?? 5;
4936
+ const edgeKind = stmt.edgeKind;
4937
+ const direction = stmt.direction ?? "OUTGOING";
4938
+ const deadline = start + EXECUTION_TIMEOUT_MS;
4939
+ let startNode;
4940
+ for (const node of graph.allNodes()) {
4941
+ if (node.name === stmt.from) {
4942
+ startNode = node;
4943
+ break;
4944
+ }
4945
+ }
4946
+ if (!startNode) {
4947
+ return {
4948
+ nodes: [],
4949
+ edges: [],
4950
+ executionTimeMs: Date.now() - start,
4951
+ truncated: false,
4952
+ totalCount: 0
4953
+ };
4954
+ }
4955
+ const visitedNodes = /* @__PURE__ */ new Set();
4956
+ const visitedEdges = /* @__PURE__ */ new Set();
4957
+ const resultNodes = [];
4958
+ const resultEdges = [];
4959
+ const queue = [{ id: startNode.id, depth: 0 }];
4960
+ visitedNodes.add(startNode.id);
4961
+ resultNodes.push(startNode);
4962
+ let truncated = false;
4963
+ while (queue.length > 0) {
4964
+ if (Date.now() > deadline) {
4965
+ truncated = true;
4966
+ break;
4967
+ }
4968
+ const { id, depth } = queue.shift();
4969
+ if (depth >= maxDepth) continue;
4970
+ const nextEdges = [];
4971
+ if (direction === "OUTGOING" || direction === "BOTH") {
4972
+ for (const edge of graph.findEdgesFrom(id)) {
4973
+ if (!edgeKind || edge.kind === edgeKind) nextEdges.push(edge);
4974
+ }
4975
+ }
4976
+ if (direction === "INCOMING" || direction === "BOTH") {
4977
+ for (const edge of graph.findEdgesTo(id)) {
4978
+ if (!edgeKind || edge.kind === edgeKind) nextEdges.push(edge);
4979
+ }
4980
+ }
4981
+ for (const edge of nextEdges) {
4982
+ if (!visitedEdges.has(edge.id)) {
4983
+ visitedEdges.add(edge.id);
4984
+ resultEdges.push(edge);
4985
+ }
4986
+ const neighborId = direction === "INCOMING" ? edge.source : edge.target;
4987
+ const effectiveNeighborId = direction === "BOTH" ? edge.source === id ? edge.target : edge.source : neighborId;
4988
+ if (!visitedNodes.has(effectiveNeighborId)) {
4989
+ visitedNodes.add(effectiveNeighborId);
4990
+ const neighborNode = graph.getNode(effectiveNeighborId);
4991
+ if (neighborNode) {
4992
+ resultNodes.push(neighborNode);
4993
+ queue.push({ id: effectiveNeighborId, depth: depth + 1 });
4994
+ }
4995
+ }
4996
+ }
4997
+ }
4998
+ return {
4999
+ nodes: resultNodes,
5000
+ edges: resultEdges,
5001
+ executionTimeMs: Date.now() - start,
5002
+ truncated,
5003
+ totalCount: resultNodes.length
5004
+ };
5005
+ }
5006
+ function executePATH(stmt, graph) {
5007
+ const start = Date.now();
5008
+ const deadline = start + EXECUTION_TIMEOUT_MS;
5009
+ let startNode;
5010
+ let endNode;
5011
+ for (const node of graph.allNodes()) {
5012
+ if (node.name === stmt.from) startNode = node;
5013
+ if (node.name === stmt.to) endNode = node;
5014
+ if (startNode && endNode) break;
5015
+ }
5016
+ if (!startNode || !endNode) {
5017
+ return {
5018
+ path: null,
5019
+ nodes: [],
5020
+ executionTimeMs: Date.now() - start,
5021
+ truncated: false,
5022
+ totalCount: 0
5023
+ };
5024
+ }
5025
+ const visited = /* @__PURE__ */ new Set();
5026
+ const parent = /* @__PURE__ */ new Map();
5027
+ const queue = [startNode.id];
5028
+ visited.add(startNode.id);
5029
+ let found = false;
5030
+ let truncated = false;
5031
+ outer: while (queue.length > 0) {
5032
+ if (Date.now() > deadline) {
5033
+ truncated = true;
5034
+ break;
5035
+ }
5036
+ const current2 = queue.shift();
5037
+ for (const edge of graph.findEdgesFrom(current2)) {
5038
+ const next = edge.target;
5039
+ if (!visited.has(next)) {
5040
+ visited.add(next);
5041
+ parent.set(next, { nodeId: current2, edgeId: edge.id });
5042
+ if (next === endNode.id) {
5043
+ found = true;
5044
+ break outer;
5045
+ }
5046
+ queue.push(next);
5047
+ }
5048
+ }
5049
+ for (const edge of graph.findEdgesTo(current2)) {
5050
+ const next = edge.source;
5051
+ if (!visited.has(next)) {
5052
+ visited.add(next);
5053
+ parent.set(next, { nodeId: current2, edgeId: edge.id });
5054
+ if (next === endNode.id) {
5055
+ found = true;
5056
+ break outer;
5057
+ }
5058
+ queue.push(next);
5059
+ }
5060
+ }
5061
+ }
5062
+ if (!found) {
5063
+ return {
5064
+ path: null,
5065
+ nodes: [],
5066
+ executionTimeMs: Date.now() - start,
5067
+ truncated,
5068
+ totalCount: 0
5069
+ };
5070
+ }
5071
+ const pathNodeIds = [];
5072
+ const pathEdgeIds = [];
5073
+ let current = endNode.id;
5074
+ while (current !== startNode.id) {
5075
+ pathNodeIds.unshift(current);
5076
+ const p = parent.get(current);
5077
+ pathEdgeIds.unshift(p.edgeId);
5078
+ current = p.nodeId;
5079
+ }
5080
+ pathNodeIds.unshift(startNode.id);
5081
+ const pathNodes = pathNodeIds.map((id) => graph.getNode(id)).filter(Boolean);
5082
+ const pathEdges = pathEdgeIds.map((id) => graph.getEdge(id)).filter(Boolean);
5083
+ return {
5084
+ path: pathNodes,
5085
+ nodes: pathNodes,
5086
+ edges: pathEdges,
5087
+ executionTimeMs: Date.now() - start,
5088
+ truncated,
5089
+ totalCount: pathNodes.length
5090
+ };
5091
+ }
5092
+ function executeCOUNT(stmt, graph) {
5093
+ const start = Date.now();
5094
+ const deadline = start + EXECUTION_TIMEOUT_MS;
5095
+ let truncated = false;
5096
+ const groups = /* @__PURE__ */ new Map();
5097
+ let total = 0;
5098
+ for (const node of graph.allNodes()) {
5099
+ if (Date.now() > deadline) {
5100
+ truncated = true;
5101
+ break;
5102
+ }
5103
+ if (stmt.target !== "*" && node.kind !== stmt.target) continue;
5104
+ if (stmt.where && !evaluateWhere(node, stmt.where)) continue;
5105
+ total++;
5106
+ if (stmt.groupBy) {
5107
+ const key = String(getNodeProperty(node, stmt.groupBy) ?? "(none)");
5108
+ groups.set(key, (groups.get(key) ?? 0) + 1);
5109
+ } else {
5110
+ groups.set("total", (groups.get("total") ?? 0) + 1);
5111
+ }
5112
+ }
5113
+ const groupList = [...groups.entries()].map(([key, count]) => ({ key, count }));
5114
+ groupList.sort((a, b) => b.count - a.count);
5115
+ return {
5116
+ groups: groupList,
5117
+ executionTimeMs: Date.now() - start,
5118
+ truncated,
5119
+ totalCount: total
5120
+ };
5121
+ }
5122
+ function executeGQL(ast, graph) {
5123
+ switch (ast.type) {
5124
+ case "FIND":
5125
+ return executeFIND(ast, graph);
5126
+ case "TRAVERSE":
5127
+ return executeTRAVERSE(ast, graph);
5128
+ case "PATH":
5129
+ return executePATH(ast, graph);
5130
+ case "COUNT":
5131
+ return executeCOUNT(ast, graph);
5132
+ default:
5133
+ return {
5134
+ nodes: [],
5135
+ executionTimeMs: 0,
5136
+ truncated: false,
5137
+ totalCount: 0
5138
+ };
5139
+ }
5140
+ }
5141
+ var EXECUTION_TIMEOUT_MS;
5142
+ var init_gql_executor = __esm({
5143
+ "src/query/gql-executor.ts"() {
5144
+ EXECUTION_TIMEOUT_MS = 1e4;
5145
+ }
5146
+ });
4398
5147
  function verifyWebSocketHandshake(req) {
4399
5148
  const cookieHeader = req.headers["cookie"] ?? "";
4400
5149
  const cookies = parseCookies(cookieHeader);
@@ -4768,9 +5517,9 @@ var init_orphan_files = __esm({
4768
5517
  // src/health/health-score.ts
4769
5518
  var health_score_exports = {};
4770
5519
  __export(health_score_exports, {
4771
- computeHealthReport: () => computeHealthReport
5520
+ computeHealthReport: () => computeHealthReport2
4772
5521
  });
4773
- function computeHealthReport(graph, godNodeConfig) {
5522
+ function computeHealthReport2(graph, godNodeConfig) {
4774
5523
  const deadCode = detectDeadCode(graph);
4775
5524
  const cycles = detectCircularDeps(graph);
4776
5525
  const godNodes = detectGodNodes(graph, godNodeConfig);
@@ -4874,10 +5623,10 @@ var init_file_watcher = __esm({
4874
5623
  }
4875
5624
  // ── private ─────────────────────────────────────────────────────────────────
4876
5625
  readCodeIntelIgnore() {
4877
- const ignoreFile = path29.join(this.workspaceRoot, ".codeintelignore");
5626
+ const ignoreFile = path30.join(this.workspaceRoot, ".codeintelignore");
4878
5627
  try {
4879
- if (!fs27.existsSync(ignoreFile)) return [];
4880
- return fs27.readFileSync(ignoreFile, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
5628
+ if (!fs28.existsSync(ignoreFile)) return [];
5629
+ return fs28.readFileSync(ignoreFile, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
4881
5630
  } catch {
4882
5631
  return [];
4883
5632
  }
@@ -4920,25 +5669,29 @@ var init_incremental_indexer = __esm({
4920
5669
  return { filesProcessed: 0, nodesRemoved: 0, nodesAdded: 0, duration: 0 };
4921
5670
  }
4922
5671
  let nodesRemoved = 0;
5672
+ const nodesByFilePath = /* @__PURE__ */ new Map();
5673
+ for (const node of graph.allNodes()) {
5674
+ if (!node.filePath) continue;
5675
+ const ids = nodesByFilePath.get(node.filePath);
5676
+ if (ids) ids.push(node.id);
5677
+ else nodesByFilePath.set(node.filePath, [node.id]);
5678
+ }
5679
+ const nodeIdsToRemove = /* @__PURE__ */ new Set();
4923
5680
  for (const absPath of changedFiles) {
4924
- const relPath2 = path29.relative(workspaceRoot, absPath);
4925
- const toRemove = [];
4926
- for (const node of graph.allNodes()) {
4927
- if (node.filePath === relPath2 || node.filePath === absPath) {
4928
- toRemove.push(node.id);
4929
- }
4930
- }
4931
- for (const id of toRemove) {
4932
- graph.removeNodeCascade(id);
4933
- nodesRemoved++;
4934
- }
5681
+ const relPath2 = path30.relative(workspaceRoot, absPath);
5682
+ for (const id of nodesByFilePath.get(relPath2) ?? []) nodeIdsToRemove.add(id);
5683
+ for (const id of nodesByFilePath.get(absPath) ?? []) nodeIdsToRemove.add(id);
5684
+ }
5685
+ for (const id of nodeIdsToRemove) {
5686
+ graph.removeNodeCascade(id);
5687
+ nodesRemoved++;
4935
5688
  }
4936
- if (fs27.existsSync(dbPath)) {
5689
+ if (fs28.existsSync(dbPath)) {
4937
5690
  try {
4938
5691
  const db = new DbManager(dbPath);
4939
5692
  await db.init();
4940
5693
  for (const absPath of changedFiles) {
4941
- const relPath2 = path29.relative(workspaceRoot, absPath);
5694
+ const relPath2 = path30.relative(workspaceRoot, absPath);
4942
5695
  await removeNodesForFile(relPath2, db);
4943
5696
  }
4944
5697
  db.close();
@@ -4948,7 +5701,7 @@ var init_incremental_indexer = __esm({
4948
5701
  }
4949
5702
  const existingFiles = changedFiles.filter((f) => {
4950
5703
  try {
4951
- return fs27.statSync(f).isFile();
5704
+ return fs28.statSync(f).isFile();
4952
5705
  } catch {
4953
5706
  return false;
4954
5707
  }
@@ -4970,13 +5723,13 @@ var init_incremental_indexer = __esm({
4970
5723
  await runPipeline([noopScan, parsePhase, resolvePhase], context2);
4971
5724
  }
4972
5725
  const nodesAdded = Math.max(0, graph.size.nodes - (nodesBeforeParse - nodesRemoved));
4973
- if (fs27.existsSync(dbPath) && existingFiles.length > 0) {
5726
+ if (fs28.existsSync(dbPath) && existingFiles.length > 0) {
4974
5727
  try {
4975
5728
  const db = new DbManager(dbPath);
4976
5729
  await db.init();
4977
- const changedRelPaths = new Set(changedFiles.map((f) => path29.relative(workspaceRoot, f)));
5730
+ const changedRelPaths = new Set(changedFiles.map((f) => path30.relative(workspaceRoot, f)));
4978
5731
  const nodesToUpsert = [...graph.allNodes()].filter(
4979
- (n) => changedRelPaths.has(n.filePath) || changedRelPaths.has(path29.relative(workspaceRoot, n.filePath))
5732
+ (n) => changedRelPaths.has(n.filePath) || changedRelPaths.has(path30.relative(workspaceRoot, n.filePath))
4980
5733
  );
4981
5734
  await upsertNodes(nodesToUpsert, db);
4982
5735
  db.close();
@@ -4997,6 +5750,70 @@ var init_incremental_indexer = __esm({
4997
5750
  }
4998
5751
  });
4999
5752
 
5753
+ // src/query/saved-queries.ts
5754
+ var saved_queries_exports = {};
5755
+ __export(saved_queries_exports, {
5756
+ deleteQuery: () => deleteQuery,
5757
+ listQueries: () => listQueries,
5758
+ loadQuery: () => loadQuery,
5759
+ queryExists: () => queryExists,
5760
+ saveQuery: () => saveQuery
5761
+ });
5762
+ function getQueriesDir(workspaceRoot) {
5763
+ return path30.join(workspaceRoot, ".code-intel", "queries");
5764
+ }
5765
+ function ensureQueriesDir(workspaceRoot) {
5766
+ const dir = getQueriesDir(workspaceRoot);
5767
+ if (!fs28.existsSync(dir)) {
5768
+ fs28.mkdirSync(dir, { recursive: true });
5769
+ }
5770
+ return dir;
5771
+ }
5772
+ function saveQuery(workspaceRoot, name, gql) {
5773
+ const dir = ensureQueriesDir(workspaceRoot);
5774
+ const filePath = path30.join(dir, `${name}.gql`);
5775
+ fs28.writeFileSync(filePath, gql, "utf-8");
5776
+ }
5777
+ function loadQuery(workspaceRoot, name) {
5778
+ const dir = getQueriesDir(workspaceRoot);
5779
+ const filePath = path30.join(dir, `${name}.gql`);
5780
+ if (!fs28.existsSync(filePath)) return null;
5781
+ return fs28.readFileSync(filePath, "utf-8");
5782
+ }
5783
+ function listQueries(workspaceRoot) {
5784
+ const dir = getQueriesDir(workspaceRoot);
5785
+ if (!fs28.existsSync(dir)) return [];
5786
+ const files = fs28.readdirSync(dir).filter((f) => f.endsWith(".gql"));
5787
+ return files.map((f) => {
5788
+ const filePath = path30.join(dir, f);
5789
+ const name = f.replace(/\.gql$/, "");
5790
+ const content = fs28.readFileSync(filePath, "utf-8");
5791
+ const stat = fs28.statSync(filePath);
5792
+ return {
5793
+ name,
5794
+ content,
5795
+ filePath,
5796
+ savedAt: stat.mtime.toISOString()
5797
+ };
5798
+ }).sort((a, b) => a.name.localeCompare(b.name));
5799
+ }
5800
+ function deleteQuery(workspaceRoot, name) {
5801
+ const dir = getQueriesDir(workspaceRoot);
5802
+ const filePath = path30.join(dir, `${name}.gql`);
5803
+ if (!fs28.existsSync(filePath)) return false;
5804
+ fs28.unlinkSync(filePath);
5805
+ return true;
5806
+ }
5807
+ function queryExists(workspaceRoot, name) {
5808
+ const dir = getQueriesDir(workspaceRoot);
5809
+ const filePath = path30.join(dir, `${name}.gql`);
5810
+ return fs28.existsSync(filePath);
5811
+ }
5812
+ var init_saved_queries = __esm({
5813
+ "src/query/saved-queries.ts"() {
5814
+ }
5815
+ });
5816
+
5000
5817
  // src/cli/main.ts
5001
5818
  init_logger();
5002
5819
 
@@ -5160,7 +5977,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
5160
5977
  ]);
5161
5978
  function loadIgnorePatterns(workspaceRoot) {
5162
5979
  try {
5163
- const raw = fs27.readFileSync(path29.join(workspaceRoot, ".codeintelignore"), "utf-8");
5980
+ const raw = fs28.readFileSync(path30.join(workspaceRoot, ".codeintelignore"), "utf-8");
5164
5981
  const extras = /* @__PURE__ */ new Set();
5165
5982
  for (const line of raw.split("\n")) {
5166
5983
  const trimmed = line.trim();
@@ -5184,7 +6001,7 @@ var scanPhase = {
5184
6001
  function walk2(dir) {
5185
6002
  let entries;
5186
6003
  try {
5187
- entries = fs27.readdirSync(dir, { withFileTypes: true });
6004
+ entries = fs28.readdirSync(dir, { withFileTypes: true });
5188
6005
  } catch {
5189
6006
  return;
5190
6007
  }
@@ -5193,15 +6010,15 @@ var scanPhase = {
5193
6010
  if (entry.name.startsWith(".")) continue;
5194
6011
  if (IGNORED_DIRS.has(entry.name)) continue;
5195
6012
  if (extraIgnore.has(entry.name)) continue;
5196
- walk2(path29.join(dir, entry.name));
6013
+ walk2(path30.join(dir, entry.name));
5197
6014
  } else if (entry.isFile()) {
5198
6015
  const name = entry.name;
5199
6016
  if (IGNORED_FILE_SUFFIXES.some((s) => name.endsWith(s))) continue;
5200
- const ext = path29.extname(name);
6017
+ const ext = path30.extname(name);
5201
6018
  if (!extensions.has(ext)) continue;
5202
- const fullPath = path29.join(dir, name);
6019
+ const fullPath = path30.join(dir, name);
5203
6020
  try {
5204
- const stat = fs27.statSync(fullPath);
6021
+ const stat = fs28.statSync(fullPath);
5205
6022
  if (stat.size > MAX_FILE_SIZE_BYTES) continue;
5206
6023
  } catch {
5207
6024
  continue;
@@ -5228,20 +6045,20 @@ var structurePhase = {
5228
6045
  const dirs = /* @__PURE__ */ new Set();
5229
6046
  let structDone = 0;
5230
6047
  for (const filePath of context2.filePaths) {
5231
- const relativePath = path29.relative(context2.workspaceRoot, filePath);
6048
+ const relativePath = path30.relative(context2.workspaceRoot, filePath);
5232
6049
  const lang = detectLanguage(filePath);
5233
6050
  context2.graph.addNode({
5234
6051
  id: generateNodeId("file", relativePath, relativePath),
5235
6052
  kind: "file",
5236
- name: path29.basename(filePath),
6053
+ name: path30.basename(filePath),
5237
6054
  filePath: relativePath,
5238
6055
  metadata: lang ? { language: lang } : void 0
5239
6056
  });
5240
- let dir = path29.dirname(relativePath);
6057
+ let dir = path30.dirname(relativePath);
5241
6058
  while (dir && dir !== "." && dir !== "") {
5242
6059
  if (dirs.has(dir)) break;
5243
6060
  dirs.add(dir);
5244
- dir = path29.dirname(dir);
6061
+ dir = path30.dirname(dir);
5245
6062
  }
5246
6063
  structDone++;
5247
6064
  context2.onPhaseProgress?.("structure", structDone, context2.filePaths.length);
@@ -5250,7 +6067,7 @@ var structurePhase = {
5250
6067
  context2.graph.addNode({
5251
6068
  id: generateNodeId("directory", dir, dir),
5252
6069
  kind: "directory",
5253
- name: path29.basename(dir),
6070
+ name: path30.basename(dir),
5254
6071
  filePath: dir
5255
6072
  });
5256
6073
  }
@@ -5361,22 +6178,22 @@ var flowPhase = {
5361
6178
  const queue = [{ nodeId: ep.id, path: [ep.id] }];
5362
6179
  const visited = /* @__PURE__ */ new Set();
5363
6180
  while (queue.length > 0 && flowCount < maxFlows) {
5364
- const { nodeId, path: path30 } = queue.shift();
5365
- if (path30.length > maxDepth) continue;
6181
+ const { nodeId, path: path31 } = queue.shift();
6182
+ if (path31.length > maxDepth) continue;
5366
6183
  const callEdges = [...graph.findEdgesFrom(nodeId)].filter((e) => e.kind === "calls").slice(0, maxBranching);
5367
- if (callEdges.length === 0 && path30.length >= 3) {
6184
+ if (callEdges.length === 0 && path31.length >= 3) {
5368
6185
  const flowId = generateNodeId("flow", ep.filePath, `flow-${flowCount}`);
5369
6186
  graph.addNode({
5370
6187
  id: flowId,
5371
6188
  kind: "flow",
5372
6189
  name: `${ep.name} flow ${flowCount}`,
5373
6190
  filePath: ep.filePath,
5374
- metadata: { steps: path30, entryPoint: ep.name }
6191
+ metadata: { steps: path31, entryPoint: ep.name }
5375
6192
  });
5376
- for (let i = 0; i < path30.length; i++) {
6193
+ for (let i = 0; i < path31.length; i++) {
5377
6194
  graph.addEdge({
5378
- id: generateEdgeId(path30[i], flowId, `step_of_${i}`),
5379
- source: path30[i],
6195
+ id: generateEdgeId(path31[i], flowId, `step_of_${i}`),
6196
+ source: path31[i],
5380
6197
  target: flowId,
5381
6198
  kind: "step_of",
5382
6199
  weight: 1,
@@ -5389,7 +6206,7 @@ var flowPhase = {
5389
6206
  for (const edge of callEdges) {
5390
6207
  if (visited.has(edge.target)) continue;
5391
6208
  visited.add(edge.target);
5392
- queue.push({ nodeId: edge.target, path: [...path30, edge.target] });
6209
+ queue.push({ nodeId: edge.target, path: [...path31, edge.target] });
5393
6210
  }
5394
6211
  }
5395
6212
  }
@@ -5407,7 +6224,7 @@ var LLMGovernanceLogger = class {
5407
6224
  }
5408
6225
  /** Path to the JSONL log file. */
5409
6226
  getLogPath() {
5410
- return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path29.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
6227
+ return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path30.join(os12.homedir(), ".code-intel", "llm-governance.jsonl");
5411
6228
  }
5412
6229
  /**
5413
6230
  * Append an entry to the governance log.
@@ -5423,8 +6240,8 @@ var LLMGovernanceLogger = class {
5423
6240
  ...entry
5424
6241
  };
5425
6242
  const logPath = this.getLogPath();
5426
- fs27.mkdirSync(path29.dirname(logPath), { recursive: true });
5427
- fs27.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
6243
+ fs28.mkdirSync(path30.dirname(logPath), { recursive: true });
6244
+ fs28.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
5428
6245
  } catch {
5429
6246
  }
5430
6247
  }
@@ -5434,7 +6251,7 @@ var LLMGovernanceLogger = class {
5434
6251
  */
5435
6252
  readLog(limit = 100) {
5436
6253
  try {
5437
- const raw = fs27.readFileSync(this.getLogPath(), "utf-8");
6254
+ const raw = fs28.readFileSync(this.getLogPath(), "utf-8");
5438
6255
  const lines = raw.split("\n").filter((l) => l.trim().length > 0).slice(-limit);
5439
6256
  return lines.map((l) => JSON.parse(l));
5440
6257
  } catch {
@@ -5748,7 +6565,7 @@ var LANG_QUERIES2 = {
5748
6565
  };
5749
6566
  function workerScriptPath() {
5750
6567
  const thisFile = fileURLToPath(import.meta.url);
5751
- return path29.join(path29.dirname(thisFile), "parse-worker.js");
6568
+ return path30.join(path30.dirname(thisFile), "parse-worker.js");
5752
6569
  }
5753
6570
  var parsePhaseParallel = {
5754
6571
  name: "parse",
@@ -5764,14 +6581,14 @@ var parsePhaseParallel = {
5764
6581
  const batch = filePaths.slice(i, i + CONCURRENCY);
5765
6582
  await Promise.all(batch.map(async (filePath) => {
5766
6583
  try {
5767
- const source = await fs27.promises.readFile(filePath, "utf-8");
6584
+ const source = await fs28.promises.readFile(filePath, "utf-8");
5768
6585
  context2.fileCache.set(filePath, source);
5769
6586
  } catch {
5770
6587
  }
5771
6588
  }));
5772
6589
  }
5773
6590
  const workerScript = workerScriptPath();
5774
- const workerScriptExists = fs27.existsSync(workerScript);
6591
+ const workerScriptExists = fs28.existsSync(workerScript);
5775
6592
  if (!workerScriptExists || workerCount === 1) {
5776
6593
  logger_default.info(`[parse-parallel] falling back to sequential (workerCount=${workerCount}, scriptExists=${workerScriptExists})`);
5777
6594
  const { parsePhase: parsePhase2 } = await Promise.resolve().then(() => (init_parse_phase(), parse_phase_exports));
@@ -5783,7 +6600,7 @@ var parsePhaseParallel = {
5783
6600
  if (!lang) continue;
5784
6601
  const source = context2.fileCache.get(filePath);
5785
6602
  if (!source) continue;
5786
- const relativePath = path29.relative(context2.workspaceRoot, filePath);
6603
+ const relativePath = path30.relative(context2.workspaceRoot, filePath);
5787
6604
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
5788
6605
  const fileNode = context2.graph.getNode(fileNodeId);
5789
6606
  if (fileNode) fileNode.content = source.slice(0, 2e3);
@@ -5826,7 +6643,7 @@ var parsePhaseParallel = {
5826
6643
  symbolCount += res.nodes.length;
5827
6644
  if (res.usedTreeSitter) treeSitterCount++;
5828
6645
  else regexCount++;
5829
- const relativePath = path29.relative(context2.workspaceRoot, res.taskId);
6646
+ const relativePath = path30.relative(context2.workspaceRoot, res.taskId);
5830
6647
  const funcs = res.nodes.filter((n) => n.kind === "function" || n.kind === "method").map((n) => ({ id: n.id, startLine: n.startLine ?? 0, endLine: n.endLine })).sort((a, b) => a.startLine - b.startLine);
5831
6648
  if (funcs.length > 0) context2.fileFunctionIndex.set(relativePath, funcs);
5832
6649
  parseDone++;
@@ -5853,7 +6670,7 @@ init_id_generator();
5853
6670
  init_logger();
5854
6671
  function workerScriptPath2() {
5855
6672
  const thisFile = fileURLToPath(import.meta.url);
5856
- return path29.join(path29.dirname(thisFile), "resolve-worker.js");
6673
+ return path30.join(path30.dirname(thisFile), "resolve-worker.js");
5857
6674
  }
5858
6675
  var resolvePhaseParallel = {
5859
6676
  name: "resolve",
@@ -5865,11 +6682,11 @@ var resolvePhaseParallel = {
5865
6682
  const fileFunctionIndex = context2.fileFunctionIndex ?? /* @__PURE__ */ new Map();
5866
6683
  const fileIndex = {};
5867
6684
  for (const fp of filePaths) {
5868
- const rel = path29.relative(workspaceRoot, fp);
6685
+ const rel = path30.relative(workspaceRoot, fp);
5869
6686
  fileIndex[rel] = fp;
5870
6687
  const noExt = rel.replace(/\.\w+$/, "");
5871
6688
  if (!fileIndex[noExt]) fileIndex[noExt] = fp;
5872
- const base = path29.basename(rel, path29.extname(rel));
6689
+ const base = path30.basename(rel, path30.extname(rel));
5873
6690
  if (!fileIndex[base]) fileIndex[base] = fp;
5874
6691
  }
5875
6692
  const symbolIndex = {};
@@ -5883,7 +6700,7 @@ var resolvePhaseParallel = {
5883
6700
  }
5884
6701
  const snapshot = { symbolIndex, fileSymbolIndex, fileIndex, workspaceRoot };
5885
6702
  const workerScript = workerScriptPath2();
5886
- const workerScriptExists = fs27.existsSync(workerScript);
6703
+ const workerScriptExists = fs28.existsSync(workerScript);
5887
6704
  const workerCount = parseInt(process.env["PARSE_WORKERS"] ?? "", 10) || Math.max(1, os12.cpus().length - 1);
5888
6705
  if (!workerScriptExists || workerCount === 1) {
5889
6706
  logger_default.info(`[resolve-parallel] falling back to sequential`);
@@ -5896,7 +6713,7 @@ var resolvePhaseParallel = {
5896
6713
  if (!lang) continue;
5897
6714
  const source = fileCache.get(filePath);
5898
6715
  if (!source) continue;
5899
- const relativePath = path29.relative(workspaceRoot, filePath);
6716
+ const relativePath = path30.relative(workspaceRoot, filePath);
5900
6717
  const fileNodeId = generateNodeId("file", relativePath, relativePath);
5901
6718
  const funcList = fileFunctionIndex.get(relativePath) ?? [];
5902
6719
  tasks.push({ taskId: filePath, filePath, relativePath, fileNodeId, source, funcList });
@@ -6019,7 +6836,7 @@ init_embedder();
6019
6836
  async function hybridSearch(graph, query, limit, options = {}) {
6020
6837
  const { vectorDbPath, bm25Limit = 50, vectorLimit = 50 } = options;
6021
6838
  const bm25Promise = Promise.resolve(textSearch(graph, query, bm25Limit));
6022
- const hasVectorDb = Boolean(vectorDbPath && fs27.existsSync(vectorDbPath));
6839
+ const hasVectorDb = Boolean(vectorDbPath && fs28.existsSync(vectorDbPath));
6023
6840
  if (!hasVectorDb) {
6024
6841
  const bm25Results2 = await bm25Promise;
6025
6842
  return {
@@ -6273,8 +7090,8 @@ async function syncGroup(group) {
6273
7090
  logger_default.warn(` \u26A0 Registry entry "${member.registryName}" not found \u2014 skipping ${member.groupPath}`);
6274
7091
  continue;
6275
7092
  }
6276
- const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
6277
- if (!fs27.existsSync(dbPath)) {
7093
+ const dbPath = path30.join(regEntry.path, ".code-intel", "graph.db");
7094
+ if (!fs28.existsSync(dbPath)) {
6278
7095
  logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
6279
7096
  continue;
6280
7097
  }
@@ -6311,8 +7128,8 @@ async function queryGroup(group, query, limit = 20) {
6311
7128
  for (const member of group.members) {
6312
7129
  const regEntry = registry.find((r) => r.name === member.registryName);
6313
7130
  if (!regEntry) continue;
6314
- const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
6315
- if (!fs27.existsSync(dbPath)) continue;
7131
+ const dbPath = path30.join(regEntry.path, ".code-intel", "graph.db");
7132
+ if (!fs28.existsSync(dbPath)) continue;
6316
7133
  const graph = createKnowledgeGraph();
6317
7134
  const db = new DbManager(dbPath);
6318
7135
  try {
@@ -6352,7 +7169,7 @@ var STUCK_THRESHOLD_MINUTES = 30;
6352
7169
  var JobsDB = class {
6353
7170
  db;
6354
7171
  constructor(dbPath) {
6355
- fs27.mkdirSync(path29.dirname(dbPath), { recursive: true });
7172
+ fs28.mkdirSync(path30.dirname(dbPath), { recursive: true });
6356
7173
  this.db = new Database(dbPath);
6357
7174
  this.db.pragma("journal_mode = WAL");
6358
7175
  this.db.pragma("foreign_keys = ON");
@@ -6494,7 +7311,7 @@ var JobsDB = class {
6494
7311
  }
6495
7312
  };
6496
7313
  function getJobsDBPath() {
6497
- return path29.join(os12.homedir(), ".code-intel", "jobs.db");
7314
+ return path30.join(os12.homedir(), ".code-intel", "jobs.db");
6498
7315
  }
6499
7316
  var _jobsDB = null;
6500
7317
  function getOrCreateJobsDB() {
@@ -6586,7 +7403,7 @@ var BACKUP_VERSION = "1.0";
6586
7403
  var ALGORITHM = "aes-256-gcm";
6587
7404
  var IV_LENGTH = 16;
6588
7405
  function getBackupDir() {
6589
- return path29.join(os12.homedir(), ".code-intel", "backups");
7406
+ return path30.join(os12.homedir(), ".code-intel", "backups");
6590
7407
  }
6591
7408
  function getBackupKey() {
6592
7409
  const keyHex = process.env["CODE_INTEL_BACKUP_KEY"];
@@ -6617,30 +7434,30 @@ var BackupService = class {
6617
7434
  constructor(backupDir) {
6618
7435
  this.backupDir = backupDir ?? getBackupDir();
6619
7436
  this.key = getBackupKey();
6620
- fs27.mkdirSync(this.backupDir, { recursive: true });
7437
+ fs28.mkdirSync(this.backupDir, { recursive: true });
6621
7438
  }
6622
7439
  /**
6623
7440
  * Create a backup for a repository.
6624
7441
  * Returns the backup entry.
6625
7442
  */
6626
7443
  createBackup(repoPath) {
6627
- const codeIntelDir = path29.join(repoPath, ".code-intel");
7444
+ const codeIntelDir = path30.join(repoPath, ".code-intel");
6628
7445
  const id = v4();
6629
7446
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6630
7447
  const filesToBackup = [];
6631
7448
  const candidates = ["graph.db", "vector.db", "meta.json"];
6632
7449
  for (const f of candidates) {
6633
- const fp = path29.join(codeIntelDir, f);
6634
- if (fs27.existsSync(fp)) {
7450
+ const fp = path30.join(codeIntelDir, f);
7451
+ if (fs28.existsSync(fp)) {
6635
7452
  filesToBackup.push({ name: f, localPath: fp });
6636
7453
  }
6637
7454
  }
6638
- const registryPath = path29.join(os12.homedir(), ".code-intel", "registry.json");
6639
- if (fs27.existsSync(registryPath)) {
7455
+ const registryPath = path30.join(os12.homedir(), ".code-intel", "registry.json");
7456
+ if (fs28.existsSync(registryPath)) {
6640
7457
  filesToBackup.push({ name: "registry.json", localPath: registryPath });
6641
7458
  }
6642
- const usersDbPath = path29.join(os12.homedir(), ".code-intel", "users.db");
6643
- if (fs27.existsSync(usersDbPath)) {
7459
+ const usersDbPath = path30.join(os12.homedir(), ".code-intel", "users.db");
7460
+ if (fs28.existsSync(usersDbPath)) {
6644
7461
  filesToBackup.push({ name: "users.db", localPath: usersDbPath });
6645
7462
  }
6646
7463
  if (filesToBackup.length === 0) {
@@ -6651,7 +7468,7 @@ var BackupService = class {
6651
7468
  createdAt,
6652
7469
  version: BACKUP_VERSION,
6653
7470
  files: filesToBackup.map((f) => {
6654
- const data = fs27.readFileSync(f.localPath);
7471
+ const data = fs28.readFileSync(f.localPath);
6655
7472
  return {
6656
7473
  name: f.name,
6657
7474
  sha256: crypto5.createHash("sha256").update(data).digest("hex"),
@@ -6665,7 +7482,7 @@ var BackupService = class {
6665
7482
  manifestLenBuf.writeUInt32BE(manifestBuf.length, 0);
6666
7483
  parts.push(manifestLenBuf, manifestBuf);
6667
7484
  for (const f of filesToBackup) {
6668
- const data = fs27.readFileSync(f.localPath);
7485
+ const data = fs28.readFileSync(f.localPath);
6669
7486
  const nameBuf = Buffer.from(f.name, "utf-8");
6670
7487
  const nameLenBuf = Buffer.alloc(2);
6671
7488
  nameLenBuf.writeUInt16BE(nameBuf.length, 0);
@@ -6676,8 +7493,8 @@ var BackupService = class {
6676
7493
  const plaintext = Buffer.concat(parts);
6677
7494
  const encrypted = encryptBuffer(plaintext, this.key);
6678
7495
  const backupFileName = `backup-${id}.cib`;
6679
- const backupPath = path29.join(this.backupDir, backupFileName);
6680
- fs27.writeFileSync(backupPath, encrypted);
7496
+ const backupPath = path30.join(this.backupDir, backupFileName);
7497
+ fs28.writeFileSync(backupPath, encrypted);
6681
7498
  const entry = {
6682
7499
  id,
6683
7500
  createdAt,
@@ -6704,9 +7521,9 @@ var BackupService = class {
6704
7521
  async uploadToS3(entry) {
6705
7522
  const cfg = getS3Config();
6706
7523
  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.");
6707
- const fileName = path29.basename(entry.path);
7524
+ const fileName = path30.basename(entry.path);
6708
7525
  const s3Key = `${cfg.prefix}${fileName}`;
6709
- const body = fs27.readFileSync(entry.path);
7526
+ const body = fs28.readFileSync(entry.path);
6710
7527
  const result = await s3Request({ method: "PUT", cfg, key: s3Key, body });
6711
7528
  if (result.statusCode < 200 || result.statusCode >= 300) {
6712
7529
  throw new Error(`S3 upload failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
@@ -6723,8 +7540,8 @@ var BackupService = class {
6723
7540
  if (result.statusCode < 200 || result.statusCode >= 300) {
6724
7541
  throw new Error(`S3 download failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
6725
7542
  }
6726
- fs27.mkdirSync(path29.dirname(destPath), { recursive: true });
6727
- fs27.writeFileSync(destPath, Buffer.from(result.body, "binary"));
7543
+ fs28.mkdirSync(path30.dirname(destPath), { recursive: true });
7544
+ fs28.writeFileSync(destPath, Buffer.from(result.body, "binary"));
6728
7545
  }
6729
7546
  /**
6730
7547
  * List backup objects in S3 with the configured prefix.
@@ -6770,10 +7587,10 @@ var BackupService = class {
6770
7587
  if (!entry) {
6771
7588
  throw new Error(`Backup "${backupId}" not found.`);
6772
7589
  }
6773
- if (!fs27.existsSync(entry.path)) {
7590
+ if (!fs28.existsSync(entry.path)) {
6774
7591
  throw new Error(`Backup file not found at: ${entry.path}`);
6775
7592
  }
6776
- const encrypted = fs27.readFileSync(entry.path);
7593
+ const encrypted = fs28.readFileSync(entry.path);
6777
7594
  let plaintext;
6778
7595
  try {
6779
7596
  plaintext = decryptBuffer(encrypted, this.key);
@@ -6787,8 +7604,8 @@ var BackupService = class {
6787
7604
  offset += manifestLen;
6788
7605
  const manifest = JSON.parse(manifestStr);
6789
7606
  const restoreBase = targetRepoPath ?? entry.repoPath;
6790
- const codeIntelDir = path29.join(restoreBase, ".code-intel");
6791
- fs27.mkdirSync(codeIntelDir, { recursive: true });
7607
+ const codeIntelDir = path30.join(restoreBase, ".code-intel");
7608
+ fs28.mkdirSync(codeIntelDir, { recursive: true });
6792
7609
  for (const fileEntry of manifest.files) {
6793
7610
  const nameLen = plaintext.readUInt16BE(offset);
6794
7611
  offset += 2;
@@ -6805,18 +7622,18 @@ var BackupService = class {
6805
7622
  }
6806
7623
  let destPath;
6807
7624
  if (name === "registry.json" || name === "users.db") {
6808
- destPath = path29.join(os12.homedir(), ".code-intel", name);
7625
+ destPath = path30.join(os12.homedir(), ".code-intel", name);
6809
7626
  } else {
6810
- destPath = path29.join(codeIntelDir, name);
7627
+ destPath = path30.join(codeIntelDir, name);
6811
7628
  }
6812
- fs27.writeFileSync(destPath, data);
7629
+ fs28.writeFileSync(destPath, data);
6813
7630
  }
6814
7631
  }
6815
7632
  /**
6816
7633
  * Apply retention policy: keep N daily, M weekly, L monthly backups.
6817
7634
  */
6818
7635
  applyRetention(options = { daily: 7, weekly: 4, monthly: 12 }) {
6819
- const entries = this._loadIndex().filter((e) => fs27.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
7636
+ const entries = this._loadIndex().filter((e) => fs28.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
6820
7637
  const keep = /* @__PURE__ */ new Set();
6821
7638
  const now = /* @__PURE__ */ new Date();
6822
7639
  const dailyCutoff = new Date(now);
@@ -6846,7 +7663,7 @@ var BackupService = class {
6846
7663
  for (const e of entries) {
6847
7664
  if (!keep.has(e.id)) {
6848
7665
  try {
6849
- fs27.unlinkSync(e.path);
7666
+ fs28.unlinkSync(e.path);
6850
7667
  deleted++;
6851
7668
  } catch {
6852
7669
  }
@@ -6858,17 +7675,17 @@ var BackupService = class {
6858
7675
  }
6859
7676
  // ── Index helpers ──────────────────────────────────────────────────────────
6860
7677
  _indexPath() {
6861
- return path29.join(this.backupDir, "index.json");
7678
+ return path30.join(this.backupDir, "index.json");
6862
7679
  }
6863
7680
  _loadIndex() {
6864
7681
  try {
6865
- return JSON.parse(fs27.readFileSync(this._indexPath(), "utf-8"));
7682
+ return JSON.parse(fs28.readFileSync(this._indexPath(), "utf-8"));
6866
7683
  } catch {
6867
7684
  return [];
6868
7685
  }
6869
7686
  }
6870
7687
  _saveIndex(entries) {
6871
- fs27.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
7688
+ fs28.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
6872
7689
  }
6873
7690
  _appendIndex(entry) {
6874
7691
  const entries = this._loadIndex();
@@ -7207,6 +8024,60 @@ var openApiSpec = {
7207
8024
  }
7208
8025
  }
7209
8026
  },
8027
+ "/source": {
8028
+ get: {
8029
+ tags: ["Files"],
8030
+ summary: "Get source code preview with context around specified lines",
8031
+ description: "Returns the file content around the specified line range (\xB120 lines context), with language detection. Requires viewer role.",
8032
+ security: [{ BearerAuth: [] }, { SessionCookie: [] }],
8033
+ parameters: [
8034
+ {
8035
+ name: "file",
8036
+ in: "query",
8037
+ required: true,
8038
+ description: "Absolute path to the file",
8039
+ schema: { type: "string" }
8040
+ },
8041
+ {
8042
+ name: "startLine",
8043
+ in: "query",
8044
+ required: false,
8045
+ description: "Start line number (1-indexed)",
8046
+ schema: { type: "integer", minimum: 1 }
8047
+ },
8048
+ {
8049
+ name: "endLine",
8050
+ in: "query",
8051
+ required: false,
8052
+ description: "End line number (1-indexed)",
8053
+ schema: { type: "integer", minimum: 1 }
8054
+ }
8055
+ ],
8056
+ responses: {
8057
+ "200": {
8058
+ description: "Source code preview",
8059
+ content: {
8060
+ "application/json": {
8061
+ schema: {
8062
+ type: "object",
8063
+ properties: {
8064
+ content: { type: "string", description: "File content (with context lines)" },
8065
+ language: { type: "string", description: "Detected programming language", example: "typescript" },
8066
+ startLine: { type: "integer", description: "Actual start line returned (with context)" },
8067
+ endLine: { type: "integer", description: "Actual end line returned (with context)" }
8068
+ },
8069
+ required: ["content", "language", "startLine", "endLine"]
8070
+ }
8071
+ }
8072
+ }
8073
+ },
8074
+ "400": { description: "Bad request (missing file param or path traversal detected)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
8075
+ "401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
8076
+ "403": { description: "Forbidden (file outside indexed repos)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
8077
+ "404": { description: "File not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
8078
+ }
8079
+ }
8080
+ },
7210
8081
  "/grep": {
7211
8082
  post: {
7212
8083
  tags: ["Files"],
@@ -7320,16 +8191,122 @@ var openApiSpec = {
7320
8191
  "404": { description: "Group not found", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
7321
8192
  }
7322
8193
  }
8194
+ },
8195
+ "/query": {
8196
+ post: {
8197
+ tags: ["GQL"],
8198
+ summary: "Execute a GQL (Graph Query Language) query against the knowledge graph",
8199
+ description: "Supports FIND, TRAVERSE, PATH, and COUNT statements. Requires viewer role minimum.",
8200
+ security: [{ BearerAuth: [] }, { SessionCookie: [] }],
8201
+ requestBody: {
8202
+ required: true,
8203
+ content: {
8204
+ "application/json": {
8205
+ schema: {
8206
+ type: "object",
8207
+ properties: {
8208
+ gql: {
8209
+ type: "string",
8210
+ description: "GQL query string",
8211
+ example: 'FIND function WHERE name CONTAINS "auth"'
8212
+ },
8213
+ format: {
8214
+ type: "string",
8215
+ enum: ["json", "table", "csv"],
8216
+ default: "json",
8217
+ description: "Output format"
8218
+ }
8219
+ },
8220
+ required: ["gql"]
8221
+ }
8222
+ }
8223
+ }
8224
+ },
8225
+ responses: {
8226
+ "200": {
8227
+ description: "GQL execution result",
8228
+ content: {
8229
+ "application/json": {
8230
+ schema: {
8231
+ type: "object",
8232
+ properties: {
8233
+ nodes: { type: "array", items: { "$ref": "#/components/schemas/CodeNode" } },
8234
+ edges: { type: "array", items: { type: "object" } },
8235
+ groups: { type: "array", items: { type: "object", properties: { key: { type: "string" }, count: { type: "integer" } } } },
8236
+ path: { type: "array", items: { "$ref": "#/components/schemas/CodeNode" }, nullable: true },
8237
+ executionTimeMs: { type: "number" },
8238
+ truncated: { type: "boolean" },
8239
+ totalCount: { type: "integer" }
8240
+ }
8241
+ }
8242
+ }
8243
+ }
8244
+ },
8245
+ "400": { description: "Missing gql field", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
8246
+ "401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
8247
+ "403": { description: "Forbidden (insufficient role)", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
8248
+ "422": { description: "GQL parse error", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
8249
+ }
8250
+ }
8251
+ },
8252
+ "/query/explain": {
8253
+ post: {
8254
+ tags: ["GQL"],
8255
+ summary: "Explain a GQL query \u2014 returns the execution plan without running it",
8256
+ description: "Returns a query plan object describing the steps that would be executed. Requires viewer role minimum.",
8257
+ security: [{ BearerAuth: [] }, { SessionCookie: [] }],
8258
+ requestBody: {
8259
+ required: true,
8260
+ content: {
8261
+ "application/json": {
8262
+ schema: {
8263
+ type: "object",
8264
+ properties: {
8265
+ gql: { type: "string", description: "GQL query string", example: 'FIND function WHERE name CONTAINS "auth"' }
8266
+ },
8267
+ required: ["gql"]
8268
+ }
8269
+ }
8270
+ }
8271
+ },
8272
+ responses: {
8273
+ "200": {
8274
+ description: "Query plan",
8275
+ content: {
8276
+ "application/json": {
8277
+ schema: {
8278
+ type: "object",
8279
+ properties: {
8280
+ plan: {
8281
+ type: "object",
8282
+ properties: {
8283
+ type: { type: "string", enum: ["FIND", "TRAVERSE", "PATH", "COUNT"] },
8284
+ gql: { type: "string" },
8285
+ steps: { type: "array", items: { type: "object" } },
8286
+ estimatedCost: { type: "number" }
8287
+ }
8288
+ },
8289
+ graphSize: { type: "object", properties: { nodes: { type: "integer" }, edges: { type: "integer" } } }
8290
+ }
8291
+ }
8292
+ }
8293
+ }
8294
+ },
8295
+ "400": { description: "Missing gql field", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
8296
+ "401": { description: "Unauthorized", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } },
8297
+ "422": { description: "GQL parse error", content: { "application/json": { schema: { "$ref": "#/components/schemas/ErrorResponse" } } } }
8298
+ }
8299
+ }
7323
8300
  }
7324
8301
  }
7325
8302
  };
7326
8303
 
7327
8304
  // src/http/app.ts
7328
- var __dirname$1 = path29.dirname(fileURLToPath(import.meta.url));
8305
+ var __dirname$1 = path30.dirname(fileURLToPath(import.meta.url));
7329
8306
  var WEB_DIST = (() => {
7330
- const bundled = path29.resolve(__dirname$1, "..", "web");
7331
- if (fs27.existsSync(bundled)) return bundled;
7332
- return path29.resolve(__dirname$1, "..", "..", "..", "web", "dist");
8307
+ const bundled = path30.resolve(__dirname$1, "..", "web");
8308
+ if (fs28.existsSync(bundled)) return bundled;
8309
+ return path30.resolve(__dirname$1, "..", "..", "..", "web", "dist");
7333
8310
  })();
7334
8311
  function getAllowedOrigins() {
7335
8312
  const env = process.env["CODE_INTEL_CORS_ORIGINS"];
@@ -7357,6 +8334,7 @@ function createDefaultLimiter() {
7357
8334
  function createApp(graph, repoName, workspaceRoot, watcherState) {
7358
8335
  const app = express();
7359
8336
  app.set("trust proxy", 1);
8337
+ app.use(compression());
7360
8338
  app.use(
7361
8339
  helmet({
7362
8340
  contentSecurityPolicy: false
@@ -7859,8 +8837,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
7859
8837
  const registry = loadRegistry();
7860
8838
  const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
7861
8839
  if (!entry) return null;
7862
- const dbPath = path29.join(entry.path, ".code-intel", "graph.db");
7863
- if (!fs27.existsSync(dbPath)) return null;
8840
+ const dbPath = path30.join(entry.path, ".code-intel", "graph.db");
8841
+ if (!fs28.existsSync(dbPath)) return null;
7864
8842
  const repoGraph = createKnowledgeGraph();
7865
8843
  const db = new DbManager(dbPath);
7866
8844
  try {
@@ -7947,7 +8925,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
7947
8925
  return;
7948
8926
  }
7949
8927
  try {
7950
- const content = fs27.readFileSync(file_path, "utf-8");
8928
+ const content = fs28.readFileSync(file_path, "utf-8");
7951
8929
  res.json({ content });
7952
8930
  } catch {
7953
8931
  res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "File not found" } });
@@ -8205,8 +9183,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
8205
9183
  for (const member of group.members) {
8206
9184
  const regEntry = registry.find((r) => r.name === member.registryName);
8207
9185
  if (!regEntry) continue;
8208
- const dbPath = path29.join(regEntry.path, ".code-intel", "graph.db");
8209
- if (!fs27.existsSync(dbPath)) continue;
9186
+ const dbPath = path30.join(regEntry.path, ".code-intel", "graph.db");
9187
+ if (!fs28.existsSync(dbPath)) continue;
8210
9188
  const db = new DbManager(dbPath);
8211
9189
  try {
8212
9190
  await db.init();
@@ -8218,25 +9196,260 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
8218
9196
  }
8219
9197
  res.json({ nodes: [...mergedGraph.allNodes()], edges: [...mergedGraph.allEdges()] });
8220
9198
  });
8221
- if (fs27.existsSync(WEB_DIST)) {
8222
- app.use(express.static(WEB_DIST));
8223
- app.get("/{*path}", (_req, res) => {
8224
- res.sendFile(path29.join(WEB_DIST, "index.html"));
8225
- });
8226
- }
8227
- app.use("/admin", requireRole("admin"));
8228
- app.get("/admin/users", (_req, res) => {
8229
- const db = getOrCreateUsersDB();
8230
- res.json({ users: db.listUsers() });
8231
- });
8232
- app.post("/admin/users", async (req, res) => {
8233
- const { username, password, role } = req.body;
8234
- if (!username || !password || !role) {
8235
- res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "username, password, role required", requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
8236
- return;
8237
- }
8238
- const validRoles = ["admin", "analyst", "viewer", "repo-owner"];
8239
- if (!validRoles.includes(role)) {
9199
+ app.get("/api/v1/source", requireAuth, requireRole("viewer"), (req, res) => {
9200
+ const { file, startLine: startLineStr, endLine: endLineStr } = req.query;
9201
+ if (!file) {
9202
+ res.status(400).json({
9203
+ error: {
9204
+ code: ErrorCodes.INVALID_REQUEST,
9205
+ message: "Missing required query parameter: file",
9206
+ requestId: req.requestId,
9207
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9208
+ }
9209
+ });
9210
+ return;
9211
+ }
9212
+ if (file.includes("../")) {
9213
+ res.status(400).json({
9214
+ error: {
9215
+ code: ErrorCodes.INVALID_REQUEST,
9216
+ message: "Path traversal detected",
9217
+ hint: 'File paths must not contain "../"',
9218
+ requestId: req.requestId,
9219
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9220
+ }
9221
+ });
9222
+ return;
9223
+ }
9224
+ let rawResolved = path30.normalize(file);
9225
+ if (!path30.isAbsolute(rawResolved) && workspaceRoot) {
9226
+ rawResolved = path30.join(workspaceRoot, rawResolved);
9227
+ }
9228
+ const resolvedFile = path30.resolve(rawResolved);
9229
+ function isInsideDir(fileAbs, dir) {
9230
+ const rel = path30.relative(path30.resolve(dir), fileAbs);
9231
+ return !rel.startsWith("..") && !path30.isAbsolute(rel);
9232
+ }
9233
+ if (workspaceRoot) {
9234
+ if (!isInsideDir(resolvedFile, workspaceRoot)) {
9235
+ const registry = loadRegistry();
9236
+ const inKnownRepo = registry.some((r) => isInsideDir(resolvedFile, r.path));
9237
+ if (!inKnownRepo) {
9238
+ res.status(403).json({
9239
+ error: {
9240
+ code: ErrorCodes.FORBIDDEN,
9241
+ message: "Access denied",
9242
+ hint: "File path must be within an indexed repository",
9243
+ requestId: req.requestId,
9244
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9245
+ }
9246
+ });
9247
+ return;
9248
+ }
9249
+ }
9250
+ } else {
9251
+ const registry = loadRegistry();
9252
+ const inKnownRepo = registry.some((r) => isInsideDir(resolvedFile, r.path));
9253
+ if (!inKnownRepo) {
9254
+ res.status(403).json({
9255
+ error: {
9256
+ code: ErrorCodes.FORBIDDEN,
9257
+ message: "Access denied",
9258
+ hint: "File path must be within an indexed repository",
9259
+ requestId: req.requestId,
9260
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9261
+ }
9262
+ });
9263
+ return;
9264
+ }
9265
+ }
9266
+ let fileContent;
9267
+ try {
9268
+ fileContent = fs28.readFileSync(resolvedFile, "utf-8");
9269
+ } catch {
9270
+ res.status(404).json({
9271
+ error: {
9272
+ code: ErrorCodes.NOT_FOUND,
9273
+ message: "File not found",
9274
+ requestId: req.requestId,
9275
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9276
+ }
9277
+ });
9278
+ return;
9279
+ }
9280
+ const lines = fileContent.split("\n");
9281
+ const parsedStart = startLineStr ? Number.parseInt(startLineStr, 10) : 1;
9282
+ const parsedEnd = endLineStr ? Number.parseInt(endLineStr, 10) : parsedStart;
9283
+ if (!Number.isFinite(parsedStart) || parsedStart < 1 || !Number.isFinite(parsedEnd) || parsedEnd < 1) {
9284
+ res.status(400).json({
9285
+ error: {
9286
+ code: ErrorCodes.INVALID_REQUEST,
9287
+ message: "Invalid startLine or endLine: must be positive integers",
9288
+ requestId: req.requestId,
9289
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9290
+ }
9291
+ });
9292
+ return;
9293
+ }
9294
+ const startLine = Math.max(1, parsedStart);
9295
+ const endLine = Math.min(lines.length, parsedEnd);
9296
+ const contextStart = Math.max(1, startLine - 20);
9297
+ const contextEnd = Math.min(lines.length, endLine + 20);
9298
+ const content = lines.slice(contextStart - 1, contextEnd).join("\n");
9299
+ const ext = path30.extname(resolvedFile).toLowerCase();
9300
+ const languageMap = {
9301
+ ".ts": "typescript",
9302
+ ".tsx": "typescript",
9303
+ ".js": "javascript",
9304
+ ".jsx": "javascript",
9305
+ ".mjs": "javascript",
9306
+ ".cjs": "javascript",
9307
+ ".py": "python",
9308
+ ".go": "go",
9309
+ ".rs": "rust",
9310
+ ".java": "java",
9311
+ ".cs": "csharp",
9312
+ ".cpp": "cpp",
9313
+ ".cc": "cpp",
9314
+ ".cxx": "cpp",
9315
+ ".c": "c",
9316
+ ".h": "c",
9317
+ ".hpp": "cpp",
9318
+ ".rb": "ruby",
9319
+ ".php": "php",
9320
+ ".swift": "swift",
9321
+ ".kt": "kotlin",
9322
+ ".kts": "kotlin",
9323
+ ".json": "json",
9324
+ ".yaml": "yaml",
9325
+ ".yml": "yaml",
9326
+ ".md": "markdown",
9327
+ ".sh": "bash",
9328
+ ".bash": "bash",
9329
+ ".zsh": "bash",
9330
+ ".sql": "sql",
9331
+ ".html": "html",
9332
+ ".htm": "html",
9333
+ ".css": "css",
9334
+ ".scss": "scss",
9335
+ ".less": "less",
9336
+ ".xml": "xml",
9337
+ ".toml": "toml"
9338
+ };
9339
+ const language = languageMap[ext] ?? "plaintext";
9340
+ res.json({
9341
+ content,
9342
+ language,
9343
+ startLine: contextStart,
9344
+ endLine: contextEnd
9345
+ });
9346
+ });
9347
+ app.post("/api/v1/query", requireRole("viewer"), async (req, res) => {
9348
+ const { gql, format } = req.body;
9349
+ if (!gql || typeof gql !== "string") {
9350
+ res.status(400).json({
9351
+ error: { code: ErrorCodes.INVALID_REQUEST, message: "Missing required field: gql", requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
9352
+ });
9353
+ return;
9354
+ }
9355
+ try {
9356
+ const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
9357
+ const { executeGQL: executeGQL2 } = await Promise.resolve().then(() => (init_gql_executor(), gql_executor_exports));
9358
+ const ast = parseGQL2(gql);
9359
+ if (isGQLParseError2(ast)) {
9360
+ res.status(422).json({
9361
+ error: {
9362
+ code: ErrorCodes.INVALID_REQUEST,
9363
+ message: `GQL parse error: ${ast.message}`,
9364
+ hint: `Position: ${ast.pos}${ast.expected ? `, expected: ${ast.expected}` : ""}${ast.got ? `, got: ${ast.got}` : ""}`,
9365
+ requestId: req.requestId,
9366
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9367
+ }
9368
+ });
9369
+ return;
9370
+ }
9371
+ const result = executeGQL2(ast, graph);
9372
+ const statusCode = result.truncated ? 408 : 200;
9373
+ res.status(statusCode).json({ ...result, format: format ?? "json" });
9374
+ } catch (err) {
9375
+ 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() } });
9376
+ }
9377
+ });
9378
+ app.post("/api/v1/query/explain", requireRole("viewer"), async (req, res) => {
9379
+ const { gql } = req.body;
9380
+ if (!gql || typeof gql !== "string") {
9381
+ res.status(400).json({
9382
+ error: { code: ErrorCodes.INVALID_REQUEST, message: "Missing required field: gql", requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
9383
+ });
9384
+ return;
9385
+ }
9386
+ try {
9387
+ const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
9388
+ const ast = parseGQL2(gql);
9389
+ if (isGQLParseError2(ast)) {
9390
+ res.status(422).json({
9391
+ error: {
9392
+ code: ErrorCodes.INVALID_REQUEST,
9393
+ message: `GQL parse error: ${ast.message}`,
9394
+ hint: `Position: ${ast.pos}`,
9395
+ requestId: req.requestId,
9396
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9397
+ }
9398
+ });
9399
+ return;
9400
+ }
9401
+ const plan = { type: ast.type, gql };
9402
+ if (ast.type === "FIND") {
9403
+ plan.steps = [
9404
+ { step: 1, op: "SCAN_NODES", filter: ast.target === "*" ? "all" : `kind=${ast.target}` },
9405
+ ...ast.where ? [{ step: 2, op: "WHERE", conditions: ast.where.exprs.length }] : [],
9406
+ ...ast.limit !== void 0 ? [{ step: 3, op: "LIMIT", value: ast.limit }] : []
9407
+ ];
9408
+ plan.estimatedCost = graph.size.nodes;
9409
+ } else if (ast.type === "TRAVERSE") {
9410
+ plan.steps = [
9411
+ { step: 1, op: "FIND_START_NODE", name: ast.from },
9412
+ { step: 2, op: "BFS", edgeKind: ast.edgeKind, maxDepth: ast.depth ?? 5 }
9413
+ ];
9414
+ plan.estimatedCost = Math.min(graph.size.nodes, Math.pow(4, ast.depth ?? 5));
9415
+ } else if (ast.type === "PATH") {
9416
+ plan.steps = [
9417
+ { step: 1, op: "FIND_NODES", from: ast.from, to: ast.to },
9418
+ { step: 2, op: "BFS_SHORTEST_PATH" }
9419
+ ];
9420
+ plan.estimatedCost = graph.size.nodes + graph.size.edges;
9421
+ } else if (ast.type === "COUNT") {
9422
+ plan.steps = [
9423
+ { step: 1, op: "SCAN_NODES", filter: ast.target === "*" ? "all" : `kind=${ast.target}` },
9424
+ ...ast.where ? [{ step: 2, op: "WHERE", conditions: ast.where.exprs.length }] : [],
9425
+ ...ast.groupBy ? [{ step: 3, op: "GROUP_BY", property: ast.groupBy }] : [{ step: 3, op: "COUNT" }]
9426
+ ];
9427
+ plan.estimatedCost = graph.size.nodes;
9428
+ }
9429
+ res.json({ plan, graphSize: graph.size });
9430
+ } catch (err) {
9431
+ 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() } });
9432
+ }
9433
+ });
9434
+ if (fs28.existsSync(WEB_DIST)) {
9435
+ app.use(express.static(WEB_DIST));
9436
+ app.get("/{*path}", (_req, res) => {
9437
+ res.sendFile(path30.join(WEB_DIST, "index.html"));
9438
+ });
9439
+ }
9440
+ app.use("/admin", requireRole("admin"));
9441
+ app.get("/admin/users", (_req, res) => {
9442
+ const db = getOrCreateUsersDB();
9443
+ res.json({ users: db.listUsers() });
9444
+ });
9445
+ app.post("/admin/users", async (req, res) => {
9446
+ const { username, password, role } = req.body;
9447
+ if (!username || !password || !role) {
9448
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "username, password, role required", requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
9449
+ return;
9450
+ }
9451
+ const validRoles = ["admin", "analyst", "viewer", "repo-owner"];
9452
+ if (!validRoles.includes(role)) {
8240
9453
  res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: `role must be one of: ${validRoles.join(", ")}`, requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
8241
9454
  return;
8242
9455
  }
@@ -8378,6 +9591,624 @@ init_metadata();
8378
9591
  init_group_registry();
8379
9592
  init_tracing();
8380
9593
  init_metrics();
9594
+
9595
+ // src/query/explain-relationship.ts
9596
+ function explainRelationship(graph, from, to) {
9597
+ const allNodes = [...graph.allNodes()];
9598
+ const fromNode = allNodes.find((n) => n.name === from);
9599
+ if (!fromNode) {
9600
+ const firstChar = from[0]?.toLowerCase() ?? "";
9601
+ const fromLower = from.toLowerCase();
9602
+ const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(fromLower)).slice(0, 5).map((n) => n.name);
9603
+ return { error: `Symbol not found: ${from}`, suggestions };
9604
+ }
9605
+ const toNode = allNodes.find((n) => n.name === to);
9606
+ if (!toNode) {
9607
+ const firstChar = to[0]?.toLowerCase() ?? "";
9608
+ const toLower = to.toLowerCase();
9609
+ const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(toLower)).slice(0, 5).map((n) => n.name);
9610
+ return { error: `Symbol not found: ${to}`, suggestions };
9611
+ }
9612
+ const paths = [];
9613
+ const queue = [{
9614
+ id: fromNode.id,
9615
+ nodeNames: [fromNode.name],
9616
+ lastEdgeKind: "",
9617
+ visited: /* @__PURE__ */ new Set([fromNode.id])
9618
+ }];
9619
+ while (queue.length > 0 && paths.length < 10) {
9620
+ const entry = queue.shift();
9621
+ const { id, nodeNames, visited } = entry;
9622
+ if (nodeNames.length > 6) continue;
9623
+ for (const edge of graph.findEdgesFrom(id)) {
9624
+ const targetNode = graph.getNode(edge.target);
9625
+ if (!targetNode) continue;
9626
+ if (visited.has(edge.target)) continue;
9627
+ const newNames = [...nodeNames, targetNode.name];
9628
+ if (edge.target === toNode.id) {
9629
+ paths.push({ hops: newNames.length - 1, nodes: newNames, edgeKind: edge.kind });
9630
+ if (paths.length >= 10) break;
9631
+ continue;
9632
+ }
9633
+ if (newNames.length < 6) {
9634
+ const newVisited = new Set(visited);
9635
+ newVisited.add(edge.target);
9636
+ queue.push({ id: edge.target, nodeNames: newNames, lastEdgeKind: edge.kind, visited: newVisited });
9637
+ }
9638
+ }
9639
+ }
9640
+ const fromImports = /* @__PURE__ */ new Set();
9641
+ for (const edge of graph.findEdgesFrom(fromNode.id)) {
9642
+ if (edge.kind === "imports") fromImports.add(edge.target);
9643
+ }
9644
+ const sharedImportIds = [];
9645
+ for (const edge of graph.findEdgesFrom(toNode.id)) {
9646
+ if (edge.kind === "imports" && fromImports.has(edge.target)) {
9647
+ sharedImportIds.push(edge.target);
9648
+ }
9649
+ }
9650
+ const sharedImports = sharedImportIds.map((id) => graph.getNode(id)?.name ?? id);
9651
+ let heritage = null;
9652
+ for (const edge of graph.findEdgesFrom(fromNode.id)) {
9653
+ if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === toNode.id) {
9654
+ heritage = `${from} ${edge.kind} ${to}`;
9655
+ break;
9656
+ }
9657
+ }
9658
+ if (!heritage) {
9659
+ for (const edge of graph.findEdgesFrom(toNode.id)) {
9660
+ if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === fromNode.id) {
9661
+ heritage = `${to} ${edge.kind} ${from}`;
9662
+ break;
9663
+ }
9664
+ }
9665
+ }
9666
+ const sharedStr = sharedImports.length > 0 ? sharedImports.join(", ") : "none";
9667
+ const heritageStr = heritage ?? "none";
9668
+ const connectionStr = paths.length === 0 ? "No connection found." : `${from} \u2192 ${to} via ${paths.length} path(s).`;
9669
+ const summary = `${connectionStr} Shared imports: [${sharedStr}]. Heritage: ${heritageStr}.`;
9670
+ return { paths, sharedImports, heritage, summary };
9671
+ }
9672
+
9673
+ // src/query/pr-impact.ts
9674
+ function parseDiffFiles(diff) {
9675
+ const files = [];
9676
+ for (const line of diff.split("\n")) {
9677
+ const match = line.match(/^\+\+\+ b\/(.+)/);
9678
+ if (match) {
9679
+ files.push(match[1]);
9680
+ }
9681
+ }
9682
+ return files;
9683
+ }
9684
+ function computePRImpact(graph, changedFiles, maxHops) {
9685
+ const changedSymbolIds = /* @__PURE__ */ new Set();
9686
+ for (const node of graph.allNodes()) {
9687
+ if (!node.filePath) continue;
9688
+ for (const changedFile of changedFiles) {
9689
+ if (node.filePath === changedFile || node.filePath.endsWith(changedFile) || changedFile.endsWith(node.filePath)) {
9690
+ changedSymbolIds.add(node.id);
9691
+ break;
9692
+ }
9693
+ }
9694
+ }
9695
+ const allBlastRadiusNodes = /* @__PURE__ */ new Set();
9696
+ const changedSymbols = [];
9697
+ for (const symbolId of changedSymbolIds) {
9698
+ const symbolNode = graph.getNode(symbolId);
9699
+ if (!symbolNode) continue;
9700
+ const blastRadius = /* @__PURE__ */ new Set();
9701
+ const queue = [{ id: symbolId, depth: 0 }];
9702
+ const visited = /* @__PURE__ */ new Set();
9703
+ while (queue.length > 0) {
9704
+ const { id, depth } = queue.shift();
9705
+ if (visited.has(id) || depth > maxHops) continue;
9706
+ visited.add(id);
9707
+ if (id !== symbolId) blastRadius.add(id);
9708
+ for (const edge of graph.findEdgesTo(id)) {
9709
+ if (edge.kind === "calls" || edge.kind === "imports") {
9710
+ queue.push({ id: edge.source, depth: depth + 1 });
9711
+ }
9712
+ }
9713
+ }
9714
+ for (const id of blastRadius) allBlastRadiusNodes.add(id);
9715
+ const blastCount = blastRadius.size;
9716
+ let risk;
9717
+ if (blastCount > 50) {
9718
+ risk = "HIGH";
9719
+ } else if (blastCount >= 10) {
9720
+ risk = "MEDIUM";
9721
+ } else {
9722
+ risk = "LOW";
9723
+ }
9724
+ let callerCount = 0;
9725
+ for (const edge of graph.findEdgesTo(symbolId)) {
9726
+ if (edge.kind === "calls") callerCount++;
9727
+ }
9728
+ let testCoverage = false;
9729
+ for (const edge of graph.findEdgesTo(symbolId)) {
9730
+ if (edge.kind === "imports") {
9731
+ const callerNode = graph.getNode(edge.source);
9732
+ if (callerNode?.filePath && (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec."))) {
9733
+ testCoverage = true;
9734
+ break;
9735
+ }
9736
+ }
9737
+ }
9738
+ changedSymbols.push({ name: symbolNode.name, risk, callerCount, testCoverage });
9739
+ }
9740
+ const impactedSymbols = [];
9741
+ for (const id of allBlastRadiusNodes) {
9742
+ if (changedSymbolIds.has(id)) continue;
9743
+ const node = graph.getNode(id);
9744
+ if (node) {
9745
+ impactedSymbols.push({ name: node.name, filePath: node.filePath });
9746
+ }
9747
+ }
9748
+ const riskSummary = { HIGH: 0, MEDIUM: 0, LOW: 0 };
9749
+ for (const s of changedSymbols) {
9750
+ riskSummary[s.risk]++;
9751
+ }
9752
+ const coverageGaps = [];
9753
+ for (const s of changedSymbols) {
9754
+ if ((s.risk === "HIGH" || s.risk === "MEDIUM") && !s.testCoverage) {
9755
+ coverageGaps.push(`${s.name} has no test coverage`);
9756
+ }
9757
+ }
9758
+ const fileImpactCount = /* @__PURE__ */ new Map();
9759
+ for (const sym of impactedSymbols) {
9760
+ if (sym.filePath) {
9761
+ fileImpactCount.set(sym.filePath, (fileImpactCount.get(sym.filePath) ?? 0) + 1);
9762
+ }
9763
+ }
9764
+ const filesToReview = [...fileImpactCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([fp]) => fp);
9765
+ return {
9766
+ changedSymbols,
9767
+ impactedSymbols,
9768
+ riskSummary,
9769
+ coverageGaps,
9770
+ filesToReview,
9771
+ crossRepoImpact: null
9772
+ };
9773
+ }
9774
+
9775
+ // src/query/similar-symbols.ts
9776
+ function levenshtein(a, b) {
9777
+ const m = a.length;
9778
+ const n = b.length;
9779
+ const dp = Array.from(
9780
+ { length: m + 1 },
9781
+ (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
9782
+ );
9783
+ for (let i = 1; i <= m; i++) {
9784
+ for (let j = 1; j <= n; j++) {
9785
+ if (a[i - 1] === b[j - 1]) {
9786
+ dp[i][j] = dp[i - 1][j - 1];
9787
+ } else {
9788
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
9789
+ }
9790
+ }
9791
+ }
9792
+ return dp[m][n];
9793
+ }
9794
+ function findSimilarSymbols(graph, symbolName, limit) {
9795
+ const clampedLimit = Math.min(Math.max(1, limit), 50);
9796
+ const allNodes = [...graph.allNodes()];
9797
+ const targetNode = allNodes.find((n) => n.name === symbolName);
9798
+ if (!targetNode) {
9799
+ return { similar: [] };
9800
+ }
9801
+ let targetCluster = null;
9802
+ for (const edge of graph.findEdgesFrom(targetNode.id)) {
9803
+ if (edge.kind === "belongs_to") {
9804
+ const clusterNode = graph.getNode(edge.target);
9805
+ if (clusterNode) {
9806
+ targetCluster = clusterNode.name;
9807
+ break;
9808
+ }
9809
+ }
9810
+ }
9811
+ if (!targetCluster) {
9812
+ for (const edge of graph.findEdgesTo(targetNode.id)) {
9813
+ if (edge.kind === "belongs_to") {
9814
+ const clusterNode = graph.getNode(edge.source);
9815
+ if (clusterNode) {
9816
+ targetCluster = clusterNode.name;
9817
+ break;
9818
+ }
9819
+ }
9820
+ }
9821
+ }
9822
+ const results = [];
9823
+ for (const node of allNodes) {
9824
+ if (node.id === targetNode.id) continue;
9825
+ const maxLen = Math.max(symbolName.length, node.name.length);
9826
+ const nameSim = maxLen === 0 ? 1 : 1 - levenshtein(symbolName, node.name) / maxLen;
9827
+ const structuralSim = node.kind === targetNode.kind ? 0.5 : 0;
9828
+ const combined = 0.5 * nameSim + 0.5 * structuralSim;
9829
+ const reasons = [];
9830
+ if (nameSim >= 0.6) reasons.push("similar name");
9831
+ if (node.kind === targetNode.kind) reasons.push("same kind");
9832
+ if (targetCluster !== null) {
9833
+ let nodeCluster = null;
9834
+ for (const edge of graph.findEdgesFrom(node.id)) {
9835
+ if (edge.kind === "belongs_to") {
9836
+ const clusterNode = graph.getNode(edge.target);
9837
+ if (clusterNode) {
9838
+ nodeCluster = clusterNode.name;
9839
+ break;
9840
+ }
9841
+ }
9842
+ }
9843
+ if (!nodeCluster) {
9844
+ for (const edge of graph.findEdgesTo(node.id)) {
9845
+ if (edge.kind === "belongs_to") {
9846
+ const clusterNode = graph.getNode(edge.source);
9847
+ if (clusterNode) {
9848
+ nodeCluster = clusterNode.name;
9849
+ break;
9850
+ }
9851
+ }
9852
+ }
9853
+ }
9854
+ if (nodeCluster !== null && nodeCluster === targetCluster) {
9855
+ reasons.push("same module");
9856
+ }
9857
+ }
9858
+ if (targetNode.metadata?.["cluster"] !== void 0 && node.metadata?.["cluster"] !== void 0 && node.metadata["cluster"] === targetNode.metadata["cluster"]) {
9859
+ if (!reasons.includes("same module")) reasons.push("same module");
9860
+ }
9861
+ results.push({ name: node.name, similarity: combined, reasons });
9862
+ }
9863
+ results.sort((a, b) => b.similarity - a.similarity);
9864
+ return { similar: results.slice(0, clampedLimit) };
9865
+ }
9866
+
9867
+ // src/query/health-report.ts
9868
+ function computeHealthReport(graph, scope) {
9869
+ const wholeRepo = scope === ".";
9870
+ function inScope(filePath) {
9871
+ if (wholeRepo) return true;
9872
+ return filePath.startsWith(scope) || filePath.includes(scope);
9873
+ }
9874
+ const scopedNodes = [...graph.allNodes()].filter((n) => inScope(n.filePath));
9875
+ const deadCodeKinds = /* @__PURE__ */ new Set(["function", "method", "class"]);
9876
+ const deadCode = [];
9877
+ for (const node of scopedNodes) {
9878
+ if (!deadCodeKinds.has(node.kind)) continue;
9879
+ if (node.exported === true) continue;
9880
+ let hasIncoming = false;
9881
+ for (const _edge of graph.findEdgesTo(node.id)) {
9882
+ hasIncoming = true;
9883
+ break;
9884
+ }
9885
+ if (!hasIncoming) {
9886
+ deadCode.push({ name: node.name, filePath: node.filePath, kind: node.kind });
9887
+ if (deadCode.length >= 20) break;
9888
+ }
9889
+ }
9890
+ const cycles = [];
9891
+ const scopedNodeIds = new Set(scopedNodes.map((n) => n.id));
9892
+ const importAdj = /* @__PURE__ */ new Map();
9893
+ for (const node of scopedNodes) {
9894
+ importAdj.set(node.id, []);
9895
+ }
9896
+ for (const edge of graph.findEdgesByKind("imports")) {
9897
+ if (scopedNodeIds.has(edge.source) && scopedNodeIds.has(edge.target)) {
9898
+ importAdj.get(edge.source).push(edge.target);
9899
+ }
9900
+ }
9901
+ const visited = /* @__PURE__ */ new Set();
9902
+ const inStack = /* @__PURE__ */ new Set();
9903
+ const stackPath = [];
9904
+ function dfs(nodeId) {
9905
+ if (cycles.length >= 5) return;
9906
+ visited.add(nodeId);
9907
+ inStack.add(nodeId);
9908
+ stackPath.push(nodeId);
9909
+ for (const neighborId of importAdj.get(nodeId) ?? []) {
9910
+ if (cycles.length >= 5) break;
9911
+ if (inStack.has(neighborId)) {
9912
+ const cycleStart = stackPath.indexOf(neighborId);
9913
+ const cyclePath = stackPath.slice(cycleStart).map((id) => {
9914
+ const node = graph.getNode(id);
9915
+ return node ? node.name : id;
9916
+ });
9917
+ cycles.push(cyclePath);
9918
+ } else if (!visited.has(neighborId)) {
9919
+ dfs(neighborId);
9920
+ }
9921
+ }
9922
+ stackPath.pop();
9923
+ inStack.delete(nodeId);
9924
+ }
9925
+ for (const node of scopedNodes) {
9926
+ if (cycles.length >= 5) break;
9927
+ if (!visited.has(node.id)) {
9928
+ dfs(node.id);
9929
+ }
9930
+ }
9931
+ const godNodes = [];
9932
+ for (const node of scopedNodes) {
9933
+ let edgeCount = 0;
9934
+ for (const _edge of graph.findEdgesFrom(node.id)) {
9935
+ edgeCount++;
9936
+ }
9937
+ if (edgeCount > 10) {
9938
+ godNodes.push({ name: node.name, edgeCount, filePath: node.filePath });
9939
+ }
9940
+ }
9941
+ godNodes.sort((a, b) => b.edgeCount - a.edgeCount);
9942
+ godNodes.splice(10);
9943
+ const filePathToNodes = /* @__PURE__ */ new Map();
9944
+ for (const node of scopedNodes) {
9945
+ if (!node.filePath) continue;
9946
+ let arr = filePathToNodes.get(node.filePath);
9947
+ if (!arr) {
9948
+ arr = [];
9949
+ filePathToNodes.set(node.filePath, arr);
9950
+ }
9951
+ arr.push(node.id);
9952
+ }
9953
+ const orphanFiles = [];
9954
+ for (const [filePath, nodeIds] of filePathToNodes) {
9955
+ if (orphanFiles.length >= 10) break;
9956
+ let hasAnyEdge = false;
9957
+ for (const nodeId of nodeIds) {
9958
+ let hasOut = false;
9959
+ for (const _edge of graph.findEdgesFrom(nodeId)) {
9960
+ hasOut = true;
9961
+ break;
9962
+ }
9963
+ let hasIn = false;
9964
+ for (const _edge of graph.findEdgesTo(nodeId)) {
9965
+ hasIn = true;
9966
+ break;
9967
+ }
9968
+ if (hasOut || hasIn) {
9969
+ hasAnyEdge = true;
9970
+ break;
9971
+ }
9972
+ }
9973
+ if (!hasAnyEdge) {
9974
+ orphanFiles.push(filePath);
9975
+ }
9976
+ }
9977
+ const hotspotCandidates = [];
9978
+ for (const node of scopedNodes) {
9979
+ const visitedBfs = /* @__PURE__ */ new Set();
9980
+ const queue = [{ id: node.id, depth: 0 }];
9981
+ while (queue.length > 0) {
9982
+ const item = queue.shift();
9983
+ if (item.depth > 5 || visitedBfs.has(item.id)) continue;
9984
+ visitedBfs.add(item.id);
9985
+ for (const edge of graph.findEdgesTo(item.id)) {
9986
+ if (edge.kind === "calls" || edge.kind === "imports") {
9987
+ if (!visitedBfs.has(edge.source)) {
9988
+ queue.push({ id: edge.source, depth: item.depth + 1 });
9989
+ }
9990
+ }
9991
+ }
9992
+ }
9993
+ const blastRadius = visitedBfs.size - 1;
9994
+ hotspotCandidates.push({ name: node.name, blastRadius, filePath: node.filePath });
9995
+ }
9996
+ hotspotCandidates.sort((a, b) => b.blastRadius - a.blastRadius);
9997
+ const complexityHotspots = hotspotCandidates.slice(0, 5);
9998
+ const healthScore = Math.max(
9999
+ 0,
10000
+ Math.min(100, 100 - deadCode.length * 2 - cycles.length * 5 - godNodes.length * 3)
10001
+ );
10002
+ return {
10003
+ healthScore,
10004
+ deadCode,
10005
+ cycles,
10006
+ godNodes,
10007
+ orphanFiles,
10008
+ complexityHotspots
10009
+ };
10010
+ }
10011
+
10012
+ // src/query/suggest-tests.ts
10013
+ function getSuggestedCases(symbolName) {
10014
+ const lower = symbolName.toLowerCase();
10015
+ if (/parse|validate|check|verify/.test(lower)) {
10016
+ return [
10017
+ "Valid input \u2192 success",
10018
+ "Invalid input \u2192 throws error",
10019
+ "Edge case: empty/null input \u2192 handled gracefully"
10020
+ ];
10021
+ }
10022
+ if (/create|add|insert|save/.test(lower)) {
10023
+ return [
10024
+ "Success: valid data \u2192 created",
10025
+ "Duplicate: existing item \u2192 error or no-op",
10026
+ "Missing required fields \u2192 validation error"
10027
+ ];
10028
+ }
10029
+ if (/delete|remove|destroy/.test(lower)) {
10030
+ return [
10031
+ "Existing item \u2192 deleted successfully",
10032
+ "Non-existent item \u2192 no error or 404",
10033
+ "Unauthorized access \u2192 rejected"
10034
+ ];
10035
+ }
10036
+ if (/get|find|fetch|load/.test(lower)) {
10037
+ return [
10038
+ "Found: returns correct data",
10039
+ "Not found: returns null or throws",
10040
+ "Empty collection: returns []"
10041
+ ];
10042
+ }
10043
+ return [
10044
+ "Happy path: valid input \u2192 expected output",
10045
+ "Error case: invalid input \u2192 error handled",
10046
+ "Edge case: boundary values \u2192 correct behavior"
10047
+ ];
10048
+ }
10049
+ function suggestTests(graph, symbolName) {
10050
+ let targetNode = void 0;
10051
+ for (const node of graph.allNodes()) {
10052
+ if (node.name === symbolName) {
10053
+ targetNode = node;
10054
+ break;
10055
+ }
10056
+ }
10057
+ if (!targetNode) {
10058
+ return { error: `Symbol not found: ${symbolName}` };
10059
+ }
10060
+ const targetId = targetNode.id;
10061
+ const callPaths = [];
10062
+ const pathQueue = [{ id: targetId, path: [symbolName], depth: 0 }];
10063
+ while (pathQueue.length > 0 && callPaths.length < 5) {
10064
+ const { id, path: path31, depth } = pathQueue.shift();
10065
+ let hasCallers = false;
10066
+ for (const edge of graph.findEdgesTo(id)) {
10067
+ if (edge.kind !== "calls") continue;
10068
+ const callerNode = graph.getNode(edge.source);
10069
+ if (!callerNode) continue;
10070
+ hasCallers = true;
10071
+ const newPath = [callerNode.name, ...path31];
10072
+ if (depth + 1 >= 3 || callPaths.length >= 5) {
10073
+ if (callPaths.length < 5) callPaths.push(newPath);
10074
+ continue;
10075
+ }
10076
+ pathQueue.push({ id: edge.source, path: newPath, depth: depth + 1 });
10077
+ }
10078
+ if (!hasCallers && path31.length > 1) {
10079
+ callPaths.push(path31);
10080
+ }
10081
+ }
10082
+ if (callPaths.length === 0) {
10083
+ for (const edge of graph.findEdgesTo(targetId)) {
10084
+ if (edge.kind !== "calls") continue;
10085
+ const callerNode = graph.getNode(edge.source);
10086
+ if (!callerNode) continue;
10087
+ callPaths.push([callerNode.name, symbolName]);
10088
+ if (callPaths.length >= 5) break;
10089
+ }
10090
+ }
10091
+ const existingTestFiles = /* @__PURE__ */ new Set();
10092
+ for (const edge of graph.findEdgesTo(targetId)) {
10093
+ if (edge.kind !== "imports") continue;
10094
+ const importerNode = graph.getNode(edge.source);
10095
+ if (!importerNode) continue;
10096
+ if (importerNode.filePath.includes(".test.") || importerNode.filePath.includes(".spec.")) {
10097
+ existingTestFiles.add(importerNode.filePath);
10098
+ }
10099
+ }
10100
+ const existingTests = [...existingTestFiles];
10101
+ const untestedCallers = [];
10102
+ for (const edge of graph.findEdgesTo(targetId)) {
10103
+ if (edge.kind !== "calls") continue;
10104
+ const callerNode = graph.getNode(edge.source);
10105
+ if (!callerNode) continue;
10106
+ if (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec.")) {
10107
+ continue;
10108
+ }
10109
+ let callerHasTest = false;
10110
+ for (const callerImportEdge of graph.findEdgesTo(callerNode.id)) {
10111
+ if (callerImportEdge.kind !== "imports") continue;
10112
+ const importerOfCaller = graph.getNode(callerImportEdge.source);
10113
+ if (!importerOfCaller) continue;
10114
+ if (importerOfCaller.filePath.includes(".test.") || importerOfCaller.filePath.includes(".spec.")) {
10115
+ callerHasTest = true;
10116
+ break;
10117
+ }
10118
+ }
10119
+ if (!callerHasTest) {
10120
+ untestedCallers.push(callerNode.name);
10121
+ }
10122
+ }
10123
+ const suggestedCases = getSuggestedCases(symbolName);
10124
+ return {
10125
+ callPaths,
10126
+ suggestedCases,
10127
+ existingTests,
10128
+ untestedCallers
10129
+ };
10130
+ }
10131
+
10132
+ // src/query/cluster-summary.ts
10133
+ function getPathPrefix(filePath) {
10134
+ const parts = filePath.replace(/\\/g, "/").split("/");
10135
+ return parts.slice(0, 2).join("/");
10136
+ }
10137
+ function summarizeCluster(graph, cluster) {
10138
+ const clusterNodes = [...graph.allNodes()].filter(
10139
+ (n) => n.filePath.startsWith(cluster) || n.metadata?.["cluster"] === cluster
10140
+ );
10141
+ if (clusterNodes.length === 0) {
10142
+ return { error: `Cluster not found: ${cluster}` };
10143
+ }
10144
+ const clusterNodeIds = new Set(clusterNodes.map((n) => n.id));
10145
+ const callerCountMap = /* @__PURE__ */ new Map();
10146
+ for (const node of clusterNodes) {
10147
+ let count = 0;
10148
+ for (const _edge of graph.findEdgesTo(node.id)) {
10149
+ count++;
10150
+ }
10151
+ callerCountMap.set(node.id, count);
10152
+ }
10153
+ const sortedByCallers = [...clusterNodes].sort(
10154
+ (a, b) => (callerCountMap.get(b.id) ?? 0) - (callerCountMap.get(a.id) ?? 0)
10155
+ );
10156
+ const keySymbols = sortedByCallers.slice(0, 5).map((n) => ({
10157
+ name: n.name,
10158
+ callerCount: callerCountMap.get(n.id) ?? 0
10159
+ }));
10160
+ const depsSet = /* @__PURE__ */ new Set();
10161
+ for (const node of clusterNodes) {
10162
+ for (const edge of graph.findEdgesFrom(node.id)) {
10163
+ if (edge.kind !== "imports") continue;
10164
+ const targetNode = graph.getNode(edge.target);
10165
+ if (!targetNode) continue;
10166
+ if (!clusterNodeIds.has(targetNode.id)) {
10167
+ const prefix = getPathPrefix(targetNode.filePath);
10168
+ depsSet.add(prefix);
10169
+ }
10170
+ }
10171
+ }
10172
+ const dependencies = [...depsSet];
10173
+ const dependentsSet = /* @__PURE__ */ new Set();
10174
+ for (const node of clusterNodes) {
10175
+ for (const edge of graph.findEdgesTo(node.id)) {
10176
+ if (edge.kind !== "imports") continue;
10177
+ const sourceNode = graph.getNode(edge.source);
10178
+ if (!sourceNode) continue;
10179
+ if (!clusterNodeIds.has(sourceNode.id)) {
10180
+ const prefix = getPathPrefix(sourceNode.filePath);
10181
+ dependentsSet.add(prefix);
10182
+ }
10183
+ }
10184
+ }
10185
+ const dependents = [...dependentsSet];
10186
+ const healthResult = computeHealthReport(graph, cluster);
10187
+ const health = { score: healthResult.healthScore };
10188
+ const symbolCount = {};
10189
+ for (const node of clusterNodes) {
10190
+ symbolCount[node.kind] = (symbolCount[node.kind] ?? 0) + 1;
10191
+ }
10192
+ let purpose;
10193
+ const topNode = sortedByCallers[0];
10194
+ if (topNode?.metadata?.["summary"] && typeof topNode.metadata["summary"] === "string") {
10195
+ purpose = topNode.metadata["summary"];
10196
+ } else {
10197
+ const clusterName = cluster.split("/").pop() ?? cluster;
10198
+ purpose = `Handles ${clusterName.replace(/[-_/]/g, " ")} functionality`;
10199
+ }
10200
+ return {
10201
+ cluster,
10202
+ purpose,
10203
+ keySymbols,
10204
+ dependencies,
10205
+ dependents,
10206
+ health,
10207
+ symbolCount
10208
+ };
10209
+ }
10210
+
10211
+ // src/mcp-server/server.ts
8381
10212
  function createMcpServer(graph, repoName, workspaceRoot) {
8382
10213
  const server = new Server(
8383
10214
  { name: "code-intel", version: "0.1.0" },
@@ -8407,7 +10238,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8407
10238
  type: "object",
8408
10239
  properties: {
8409
10240
  query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
8410
- limit: { type: "number", description: "Max results to return (default: 20)" },
10241
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10242
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
8411
10243
  ..._tokenProp
8412
10244
  },
8413
10245
  required: ["query"]
@@ -8450,6 +10282,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8450
10282
  type: "object",
8451
10283
  properties: {
8452
10284
  file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' },
10285
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10286
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
8453
10287
  ..._tokenProp
8454
10288
  },
8455
10289
  required: ["file_path"]
@@ -8479,7 +10313,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8479
10313
  type: "string",
8480
10314
  description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
8481
10315
  },
8482
- limit: { type: "number", description: "Max results (default: 100)" },
10316
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10317
+ limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
8483
10318
  ..._tokenProp
8484
10319
  }
8485
10320
  }
@@ -8496,7 +10331,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8496
10331
  inputSchema: {
8497
10332
  type: "object",
8498
10333
  properties: {
8499
- limit: { type: "number", description: "Max clusters to return (default: 50)" },
10334
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10335
+ limit: { type: "number", description: "Max clusters per page (default: 50, max: 500)" },
8500
10336
  ..._tokenProp
8501
10337
  }
8502
10338
  }
@@ -8507,7 +10343,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8507
10343
  inputSchema: {
8508
10344
  type: "object",
8509
10345
  properties: {
8510
- limit: { type: "number", description: "Max flows to return (default: 50)" },
10346
+ offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
10347
+ limit: { type: "number", description: "Max flows per page (default: 50, max: 500)" },
8511
10348
  ..._tokenProp
8512
10349
  }
8513
10350
  }
@@ -8531,6 +10368,23 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8531
10368
  }
8532
10369
  }
8533
10370
  },
10371
+ // ── query (GQL) ────────────────────────────────────────────────────────
10372
+ {
10373
+ name: "query",
10374
+ description: "Execute a GQL (Graph Query Language) query. Supports FIND, TRAVERSE, PATH, and COUNT. More expressive than raw_query.",
10375
+ inputSchema: {
10376
+ type: "object",
10377
+ properties: {
10378
+ gql: {
10379
+ type: "string",
10380
+ 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"'
10381
+ },
10382
+ limit: { type: "number", description: "Override LIMIT in the query (optional)" },
10383
+ ..._tokenProp
10384
+ },
10385
+ required: ["gql"]
10386
+ }
10387
+ },
8534
10388
  // ── Raw query ─────────────────────────────────────────────────────────
8535
10389
  {
8536
10390
  name: "raw_query",
@@ -8612,6 +10466,91 @@ function createMcpServer(graph, repoName, workspaceRoot) {
8612
10466
  },
8613
10467
  required: ["name"]
8614
10468
  }
10469
+ },
10470
+ // ── Reasoning / analysis tools ────────────────────────────────────────
10471
+ {
10472
+ name: "explain_relationship",
10473
+ 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.",
10474
+ inputSchema: {
10475
+ type: "object",
10476
+ properties: {
10477
+ from: { type: "string", description: "Source symbol name" },
10478
+ to: { type: "string", description: "Target symbol name" },
10479
+ ..._tokenProp
10480
+ },
10481
+ required: ["from", "to"]
10482
+ }
10483
+ },
10484
+ {
10485
+ name: "pr_impact",
10486
+ 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.",
10487
+ inputSchema: {
10488
+ type: "object",
10489
+ properties: {
10490
+ changedFiles: {
10491
+ type: "array",
10492
+ items: { type: "string" },
10493
+ description: "List of changed file paths (relative or absolute)"
10494
+ },
10495
+ diff: {
10496
+ type: "string",
10497
+ description: "Raw unified diff text. Changed files are extracted automatically."
10498
+ },
10499
+ maxHops: {
10500
+ type: "number",
10501
+ description: "Maximum BFS depth for blast radius (default: 5)"
10502
+ },
10503
+ ..._tokenProp
10504
+ }
10505
+ }
10506
+ },
10507
+ {
10508
+ name: "similar_symbols",
10509
+ description: "Find symbols with similar names or structure using Levenshtein distance and kind matching. Useful for finding related functions, classes, or interfaces.",
10510
+ inputSchema: {
10511
+ type: "object",
10512
+ properties: {
10513
+ symbol: { type: "string", description: "Symbol name to find similar symbols for" },
10514
+ limit: { type: "number", description: "Maximum number of results (default: 10, max: 50)" },
10515
+ ..._tokenProp
10516
+ },
10517
+ required: ["symbol"]
10518
+ }
10519
+ },
10520
+ {
10521
+ name: "health_report",
10522
+ description: "Code health signals for a scope: dead code, cycles, god nodes, orphan files, complexity hotspots",
10523
+ inputSchema: {
10524
+ type: "object",
10525
+ properties: {
10526
+ scope: { type: "string", description: "Directory scope, e.g. 'src/api/' or '.' for whole repo" },
10527
+ ..._tokenProp
10528
+ }
10529
+ }
10530
+ },
10531
+ {
10532
+ name: "suggest_tests",
10533
+ description: "Suggest test cases for a symbol: call paths, suggested cases, existing tests, untested callers",
10534
+ inputSchema: {
10535
+ type: "object",
10536
+ properties: {
10537
+ symbol: { type: "string", description: "Symbol name to generate test suggestions for" },
10538
+ ..._tokenProp
10539
+ },
10540
+ required: ["symbol"]
10541
+ }
10542
+ },
10543
+ {
10544
+ name: "cluster_summary",
10545
+ description: "Rich summary of a module/cluster: purpose, key symbols, dependencies, health",
10546
+ inputSchema: {
10547
+ type: "object",
10548
+ properties: {
10549
+ cluster: { type: "string", description: "Cluster path e.g. 'src/auth'" },
10550
+ ..._tokenProp
10551
+ },
10552
+ required: ["cluster"]
10553
+ }
8615
10554
  }
8616
10555
  ]
8617
10556
  }));
@@ -8682,8 +10621,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8682
10621
  for (const edge of graph.allEdges()) {
8683
10622
  edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
8684
10623
  }
8685
- const { computeHealthReport: computeHealthReport2 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
8686
- const healthReport = computeHealthReport2(graph);
10624
+ const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
10625
+ const healthReport = computeHealthReport3(graph);
8687
10626
  const health = {
8688
10627
  score: Math.round(healthReport.score),
8689
10628
  grade: healthReport.grade,
@@ -8708,10 +10647,37 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8708
10647
  // ── search ─────────────────────────────────────────────────────────────
8709
10648
  case "search": {
8710
10649
  const query = a.query;
8711
- const limit = a.limit ?? 20;
10650
+ const offset = a.offset ?? 0;
10651
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
8712
10652
  const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
8713
- const { results, searchMode } = await hybridSearch(graph, query, limit, { vectorDbPath: vdbPath });
8714
- return { content: [{ type: "text", text: JSON.stringify({ results, searchMode }, null, 2) }] };
10653
+ const fetchLimit = Math.min(offset + effectiveLimit, 500);
10654
+ const { results: allResults, searchMode } = await hybridSearch(graph, query, fetchLimit, { vectorDbPath: vdbPath });
10655
+ const total = allResults.length;
10656
+ const results = allResults.slice(offset, offset + effectiveLimit);
10657
+ const hasMore = offset + effectiveLimit < total;
10658
+ const suggestNextTools = [];
10659
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
10660
+ if (suggestEnabled && results.length > 0) {
10661
+ const topName = results[0].name;
10662
+ suggestNextTools.push(
10663
+ { tool: "inspect", reason: "Inspect the top result in detail", input: { symbol: topName } },
10664
+ { tool: "similar_symbols", reason: "Find symbols similar to the top result", input: { symbol: topName } }
10665
+ );
10666
+ }
10667
+ return {
10668
+ content: [{
10669
+ type: "text",
10670
+ text: JSON.stringify({
10671
+ results,
10672
+ searchMode,
10673
+ total,
10674
+ offset,
10675
+ limit: effectiveLimit,
10676
+ hasMore,
10677
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
10678
+ }, null, 2)
10679
+ }]
10680
+ };
8715
10681
  }
8716
10682
  // ── inspect ────────────────────────────────────────────────────────────
8717
10683
  case "inspect": {
@@ -8720,6 +10686,26 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8720
10686
  if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
8721
10687
  const incoming = [...graph.findEdgesTo(node.id)];
8722
10688
  const outgoing = [...graph.findEdgesFrom(node.id)];
10689
+ const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
10690
+ id: e.source,
10691
+ name: graph.getNode(e.source)?.name,
10692
+ file: graph.getNode(e.source)?.filePath
10693
+ }));
10694
+ const callees = outgoing.filter((e) => e.kind === "calls").map((e) => ({
10695
+ id: e.target,
10696
+ name: graph.getNode(e.target)?.name,
10697
+ file: graph.getNode(e.target)?.filePath
10698
+ }));
10699
+ const cluster = incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0];
10700
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
10701
+ const suggestNextTools = [];
10702
+ if (suggestEnabled) {
10703
+ const topCallerName = callers[0]?.name;
10704
+ suggestNextTools.push(
10705
+ ...topCallerName ? [{ tool: "explain_relationship", reason: "Explain connection to a related symbol", input: { from: node.name, to: topCallerName } }] : [],
10706
+ ...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 } }]
10707
+ );
10708
+ }
8723
10709
  return {
8724
10710
  content: [{
8725
10711
  type: "text",
@@ -8733,16 +10719,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8733
10719
  endLine: node.endLine,
8734
10720
  exported: node.exported
8735
10721
  },
8736
- callers: incoming.filter((e) => e.kind === "calls").map((e) => ({
8737
- id: e.source,
8738
- name: graph.getNode(e.source)?.name,
8739
- file: graph.getNode(e.source)?.filePath
8740
- })),
8741
- callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
8742
- id: e.target,
8743
- name: graph.getNode(e.target)?.name,
8744
- file: graph.getNode(e.target)?.filePath
8745
- })),
10722
+ callers,
10723
+ callees,
8746
10724
  imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
8747
10725
  importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
8748
10726
  extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
@@ -8751,8 +10729,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8751
10729
  name: graph.getNode(e.target)?.name,
8752
10730
  kind: graph.getNode(e.target)?.kind
8753
10731
  })),
8754
- cluster: incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0],
8755
- content: node.content?.slice(0, 500)
10732
+ cluster,
10733
+ content: node.content?.slice(0, 500),
10734
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
8756
10735
  }, null, 2)
8757
10736
  }]
8758
10737
  };
@@ -8788,6 +10767,16 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8788
10767
  return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
8789
10768
  });
8790
10769
  const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
10770
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
10771
+ const suggestNextTools = [];
10772
+ if (suggestEnabled) {
10773
+ const highestRiskSymbol = node.name;
10774
+ const firstFilePath = affectedDetails[0]?.filePath ?? "";
10775
+ suggestNextTools.push(
10776
+ { tool: "suggest_tests", reason: "Generate tests for the highest-risk symbol", input: { symbol: highestRiskSymbol } },
10777
+ { tool: "pr_impact", reason: "Compute full PR impact for changed files", input: { changedFiles: [firstFilePath] } }
10778
+ );
10779
+ }
8791
10780
  return {
8792
10781
  content: [{
8793
10782
  type: "text",
@@ -8795,7 +10784,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8795
10784
  target: node.name,
8796
10785
  affectedCount: affected.size,
8797
10786
  riskLevel: risk,
8798
- affected: affectedDetails
10787
+ affected: affectedDetails,
10788
+ ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
8799
10789
  }, null, 2)
8800
10790
  }]
8801
10791
  };
@@ -8803,17 +10793,27 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8803
10793
  // ── file_symbols ───────────────────────────────────────────────────────
8804
10794
  case "file_symbols": {
8805
10795
  const filePath = a.file_path;
8806
- const matches = [];
10796
+ const offset = a.offset ?? 0;
10797
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
10798
+ const allMatches = [];
8807
10799
  for (const node of graph.allNodes()) {
8808
10800
  if (node.filePath && node.filePath.includes(filePath)) {
8809
- matches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
10801
+ allMatches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
8810
10802
  }
8811
10803
  }
8812
- if (matches.length === 0) {
10804
+ if (allMatches.length === 0) {
8813
10805
  return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
8814
10806
  }
8815
- matches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
8816
- return { content: [{ type: "text", text: JSON.stringify(matches, null, 2) }] };
10807
+ allMatches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
10808
+ const total = allMatches.length;
10809
+ const matches = allMatches.slice(offset, offset + effectiveLimit);
10810
+ const hasMore = offset + effectiveLimit < total;
10811
+ return {
10812
+ content: [{
10813
+ type: "text",
10814
+ text: JSON.stringify({ symbols: matches, total, offset, limit: effectiveLimit, hasMore }, null, 2)
10815
+ }]
10816
+ };
8817
10817
  }
8818
10818
  // ── find_path ──────────────────────────────────────────────────────────
8819
10819
  case "find_path": {
@@ -8859,15 +10859,23 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8859
10859
  // ── list_exports ───────────────────────────────────────────────────────
8860
10860
  case "list_exports": {
8861
10861
  const kindFilter = a.kind;
8862
- const limit = a.limit ?? 100;
8863
- const exports$1 = [];
10862
+ const offset = a.offset ?? 0;
10863
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
10864
+ const allExports = [];
8864
10865
  for (const node of graph.allNodes()) {
8865
10866
  if (!node.exported) continue;
8866
10867
  if (kindFilter && node.kind !== kindFilter) continue;
8867
- exports$1.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
8868
- if (exports$1.length >= limit) break;
10868
+ allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
8869
10869
  }
8870
- return { content: [{ type: "text", text: JSON.stringify({ total: exports$1.length, exports: exports$1 }, null, 2) }] };
10870
+ const total = allExports.length;
10871
+ const exports$1 = allExports.slice(offset, offset + effectiveLimit);
10872
+ const hasMore = offset + effectiveLimit < total;
10873
+ return {
10874
+ content: [{
10875
+ type: "text",
10876
+ text: JSON.stringify({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore }, null, 2)
10877
+ }]
10878
+ };
8871
10879
  }
8872
10880
  // ── routes ─────────────────────────────────────────────────────────────
8873
10881
  case "routes": {
@@ -8881,8 +10889,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8881
10889
  }
8882
10890
  // ── clusters ───────────────────────────────────────────────────────────
8883
10891
  case "clusters": {
8884
- const limit = a.limit ?? 50;
8885
- const clusters = [];
10892
+ const offset = a.offset ?? 0;
10893
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
10894
+ const allClusters = [];
8886
10895
  for (const node of graph.allNodes()) {
8887
10896
  if (node.kind === "cluster") {
8888
10897
  const members = [];
@@ -8894,35 +10903,50 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8894
10903
  }
8895
10904
  }
8896
10905
  }
8897
- clusters.push({
10906
+ allClusters.push({
8898
10907
  id: node.id,
8899
10908
  name: node.name,
8900
10909
  memberCount: node.metadata?.memberCount ?? members.length,
8901
10910
  topSymbols: members.slice(0, 10)
8902
10911
  });
8903
- if (clusters.length >= limit) break;
8904
10912
  }
8905
10913
  }
8906
- return { content: [{ type: "text", text: JSON.stringify(clusters, null, 2) }] };
10914
+ const total = allClusters.length;
10915
+ const clusters = allClusters.slice(offset, offset + effectiveLimit);
10916
+ const hasMore = offset + effectiveLimit < total;
10917
+ return {
10918
+ content: [{
10919
+ type: "text",
10920
+ text: JSON.stringify({ clusters, total, offset, limit: effectiveLimit, hasMore }, null, 2)
10921
+ }]
10922
+ };
8907
10923
  }
8908
10924
  // ── flows ──────────────────────────────────────────────────────────────
8909
10925
  case "flows": {
8910
- const limit = a.limit ?? 50;
8911
- const flows = [];
10926
+ const offset = a.offset ?? 0;
10927
+ const effectiveLimit = Math.min(a.limit ?? 50, 500);
10928
+ const allFlows = [];
8912
10929
  for (const node of graph.allNodes()) {
8913
10930
  if (node.kind === "flow") {
8914
10931
  const steps = node.metadata?.steps;
8915
- flows.push({
10932
+ allFlows.push({
8916
10933
  id: node.id,
8917
10934
  name: node.name,
8918
10935
  entryPoint: node.metadata?.entryPoint,
8919
10936
  steps: steps ?? [],
8920
10937
  stepCount: Array.isArray(steps) ? steps.length : 0
8921
10938
  });
8922
- if (flows.length >= limit) break;
8923
10939
  }
8924
10940
  }
8925
- return { content: [{ type: "text", text: JSON.stringify(flows, null, 2) }] };
10941
+ const total = allFlows.length;
10942
+ const flows = allFlows.slice(offset, offset + effectiveLimit);
10943
+ const hasMore = offset + effectiveLimit < total;
10944
+ return {
10945
+ content: [{
10946
+ type: "text",
10947
+ text: JSON.stringify({ flows, total, offset, limit: effectiveLimit, hasMore }, null, 2)
10948
+ }]
10949
+ };
8926
10950
  }
8927
10951
  // ── detect_changes ─────────────────────────────────────────────────────
8928
10952
  case "detect_changes": {
@@ -8950,7 +10974,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8950
10974
  for (const { filePath: changedFile, changedLines } of changedFiles) {
8951
10975
  for (const node of graph.allNodes()) {
8952
10976
  if (!node.filePath) continue;
8953
- const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path29.sep, "");
10977
+ const normNode = node.filePath.replace(repoRoot + "/", "").replace(repoRoot + path30.sep, "");
8954
10978
  const normChanged = changedFile.replace(/^a\/|^b\//, "");
8955
10979
  if (!normNode.endsWith(normChanged) && !normChanged.endsWith(normNode)) continue;
8956
10980
  if (node.startLine !== void 0 && node.endLine !== void 0) {
@@ -8999,16 +11023,51 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
8999
11023
  }]
9000
11024
  };
9001
11025
  }
11026
+ // ── query (GQL) ───────────────────────────────────────────────────────────
11027
+ case "query": {
11028
+ const gqlInput = a.gql;
11029
+ if (!gqlInput) {
11030
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Missing required parameter: gql" }) }], isError: true };
11031
+ }
11032
+ const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
11033
+ const { executeGQL: executeGQL2 } = await Promise.resolve().then(() => (init_gql_executor(), gql_executor_exports));
11034
+ const ast = parseGQL2(gqlInput);
11035
+ if (isGQLParseError2(ast)) {
11036
+ return {
11037
+ content: [{ type: "text", text: JSON.stringify({ error: `GQL parse error: ${ast.message}`, pos: ast.pos, expected: ast.expected, got: ast.got }) }],
11038
+ isError: true
11039
+ };
11040
+ }
11041
+ if (a.limit !== void 0 && ast.type === "FIND") {
11042
+ ast.limit = a.limit;
11043
+ }
11044
+ const result = executeGQL2(ast, graph);
11045
+ return {
11046
+ content: [{
11047
+ type: "text",
11048
+ text: JSON.stringify({
11049
+ nodes: result.nodes,
11050
+ edges: result.edges,
11051
+ groups: result.groups,
11052
+ path: result.path,
11053
+ executionTimeMs: result.executionTimeMs,
11054
+ truncated: result.truncated,
11055
+ totalCount: result.totalCount
11056
+ }, null, 2)
11057
+ }]
11058
+ };
11059
+ }
9002
11060
  // ── raw_query ──────────────────────────────────────────────────────────
9003
11061
  case "raw_query": {
9004
11062
  const q = a.cypher;
11063
+ const deprecationWarning = "raw_query is deprecated, use query instead";
9005
11064
  const nameMatch = q?.match(/name\s*=\s*['"]([^'"]+)['"]/i);
9006
11065
  if (nameMatch) {
9007
11066
  const results = [];
9008
11067
  for (const node of graph.allNodes()) {
9009
11068
  if (node.name === nameMatch[1]) results.push(node);
9010
11069
  }
9011
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
11070
+ return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, results }, null, 2) }] };
9012
11071
  }
9013
11072
  const kindMatch = q?.match(/:\s*(\w+)/);
9014
11073
  if (kindMatch) {
@@ -9017,9 +11076,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
9017
11076
  if (node.kind === kindMatch[1]) results.push(node);
9018
11077
  if (results.length >= 50) break;
9019
11078
  }
9020
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
11079
+ return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, results }, null, 2) }] };
9021
11080
  }
9022
- return { content: [{ type: "text", text: "Query not recognized. Use name='X' or :kind syntax." }] };
11081
+ 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." }) }] };
9023
11082
  }
9024
11083
  // ── group_list ─────────────────────────────────────────────────────────
9025
11084
  case "group_list": {
@@ -9137,6 +11196,57 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
9137
11196
  }]
9138
11197
  };
9139
11198
  }
11199
+ // ── explain_relationship ───────────────────────────────────────────────
11200
+ case "explain_relationship": {
11201
+ const fromName = a.from;
11202
+ const toName = a.to;
11203
+ const result = explainRelationship(graph, fromName, toName);
11204
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11205
+ }
11206
+ // ── pr_impact ──────────────────────────────────────────────────────────
11207
+ case "pr_impact": {
11208
+ const maxHops = a.maxHops ?? 5;
11209
+ let changedFiles = a.changedFiles ?? [];
11210
+ if (a.diff && typeof a.diff === "string") {
11211
+ const diffFiles = parseDiffFiles(a.diff);
11212
+ changedFiles = [.../* @__PURE__ */ new Set([...changedFiles, ...diffFiles])];
11213
+ }
11214
+ if (changedFiles.length === 0) {
11215
+ return {
11216
+ content: [{
11217
+ type: "text",
11218
+ text: JSON.stringify({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
11219
+ }]
11220
+ };
11221
+ }
11222
+ const result = computePRImpact(graph, changedFiles, maxHops);
11223
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11224
+ }
11225
+ // ── similar_symbols ────────────────────────────────────────────────────
11226
+ case "similar_symbols": {
11227
+ const symbolName = a.symbol;
11228
+ const limit = a.limit ?? 10;
11229
+ const result = findSimilarSymbols(graph, symbolName, limit);
11230
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11231
+ }
11232
+ // ── health_report ──────────────────────────────────────────────────────
11233
+ case "health_report": {
11234
+ const scope = a.scope ?? ".";
11235
+ const result = computeHealthReport(graph, scope);
11236
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11237
+ }
11238
+ // ── suggest_tests ──────────────────────────────────────────────────────
11239
+ case "suggest_tests": {
11240
+ const sym = a.symbol;
11241
+ const result = suggestTests(graph, sym);
11242
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11243
+ }
11244
+ // ── cluster_summary ────────────────────────────────────────────────────
11245
+ case "cluster_summary": {
11246
+ const cluster = a.cluster;
11247
+ const result = summarizeCluster(graph, cluster);
11248
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
11249
+ }
9140
11250
  default:
9141
11251
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
9142
11252
  }
@@ -9223,20 +11333,20 @@ function parseDiff(diffText) {
9223
11333
  // src/cli/main.ts
9224
11334
  init_metadata();
9225
11335
  async function writeSkillFiles(graph, workspaceRoot, projectName) {
9226
- const outputDir = path29.join(workspaceRoot, ".claude", "skills", "code-intel");
11336
+ const outputDir = path30.join(workspaceRoot, ".claude", "skills", "code-intel");
9227
11337
  const areas = buildAreaMap(graph, workspaceRoot);
9228
11338
  if (areas.length === 0) return { skills: [], outputDir };
9229
- fs27.rmSync(outputDir, { recursive: true, force: true });
9230
- fs27.mkdirSync(outputDir, { recursive: true });
11339
+ fs28.rmSync(outputDir, { recursive: true, force: true });
11340
+ fs28.mkdirSync(outputDir, { recursive: true });
9231
11341
  const skills = [];
9232
11342
  const usedNames = /* @__PURE__ */ new Set();
9233
11343
  for (const area of areas) {
9234
11344
  const kebab = uniqueKebab(area.label, usedNames);
9235
11345
  usedNames.add(kebab);
9236
11346
  const content = renderSkill(area, projectName, kebab);
9237
- const dir = path29.join(outputDir, kebab);
9238
- fs27.mkdirSync(dir, { recursive: true });
9239
- fs27.writeFileSync(path29.join(dir, "SKILL.md"), content, "utf-8");
11347
+ const dir = path30.join(outputDir, kebab);
11348
+ fs28.mkdirSync(dir, { recursive: true });
11349
+ fs28.writeFileSync(path30.join(dir, "SKILL.md"), content, "utf-8");
9240
11350
  skills.push({ name: kebab, label: area.label, symbolCount: area.nodes.length, fileCount: area.files.size });
9241
11351
  }
9242
11352
  return { skills, outputDir };
@@ -9416,8 +11526,8 @@ var BLOCK_START = "<!-- code-intel:start -->";
9416
11526
  var BLOCK_END = "<!-- code-intel:end -->";
9417
11527
  function writeContextFiles(workspaceRoot, projectName, stats, skills) {
9418
11528
  const block = buildBlock(projectName, stats, skills);
9419
- upsertFile(path29.join(workspaceRoot, "AGENTS.md"), block, "AGENTS.md");
9420
- upsertFile(path29.join(workspaceRoot, "CLAUDE.md"), block, "CLAUDE.md");
11529
+ upsertFile(path30.join(workspaceRoot, "AGENTS.md"), block, "AGENTS.md");
11530
+ upsertFile(path30.join(workspaceRoot, "CLAUDE.md"), block, "CLAUDE.md");
9421
11531
  }
9422
11532
  function buildBlock(projectName, stats, skills) {
9423
11533
  const skillRows = skills.map(
@@ -9471,7 +11581,7 @@ ${skillTable}
9471
11581
  ${BLOCK_END}`;
9472
11582
  }
9473
11583
  function upsertFile(filePath, block, fileName) {
9474
- if (!fs27.existsSync(filePath)) {
11584
+ if (!fs28.existsSync(filePath)) {
9475
11585
  const newContent = [
9476
11586
  `# ${fileName}`,
9477
11587
  "",
@@ -9482,17 +11592,17 @@ function upsertFile(filePath, block, fileName) {
9482
11592
  "<!-- Add your own custom notes below this line. They will never be overwritten by code-intel. -->",
9483
11593
  ""
9484
11594
  ].join("\n");
9485
- fs27.writeFileSync(filePath, newContent, "utf-8");
11595
+ fs28.writeFileSync(filePath, newContent, "utf-8");
9486
11596
  return;
9487
11597
  }
9488
- const existing = fs27.readFileSync(filePath, "utf-8");
11598
+ const existing = fs28.readFileSync(filePath, "utf-8");
9489
11599
  const startIdx = findLineMarker(existing, BLOCK_START);
9490
11600
  const endIdx = findLineMarker(existing, BLOCK_END, startIdx === -1 ? 0 : startIdx);
9491
11601
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
9492
11602
  const before = existing.slice(0, startIdx);
9493
11603
  const after = existing.slice(endIdx + BLOCK_END.length);
9494
11604
  const updated = (before + block + after).trimEnd() + "\n";
9495
- fs27.writeFileSync(filePath, updated, "utf-8");
11605
+ fs28.writeFileSync(filePath, updated, "utf-8");
9496
11606
  return;
9497
11607
  }
9498
11608
  const appended = [
@@ -9505,7 +11615,7 @@ function upsertFile(filePath, block, fileName) {
9505
11615
  block,
9506
11616
  ""
9507
11617
  ].join("\n");
9508
- fs27.writeFileSync(filePath, appended, "utf-8");
11618
+ fs28.writeFileSync(filePath, appended, "utf-8");
9509
11619
  }
9510
11620
  function findLineMarker(content, marker, startFrom = 0) {
9511
11621
  let idx = content.indexOf(marker, startFrom);
@@ -9547,14 +11657,14 @@ function getChangedFilesSince(workspaceRoot, baseHash) {
9547
11657
  function filterChangedByMtime(allFilePaths, workspaceRoot, storedMtimes) {
9548
11658
  const changed = [];
9549
11659
  for (const absPath of allFilePaths) {
9550
- const rel = path29.relative(workspaceRoot, absPath);
11660
+ const rel = path30.relative(workspaceRoot, absPath);
9551
11661
  const stored = storedMtimes[rel];
9552
11662
  if (stored === void 0) {
9553
11663
  changed.push(absPath);
9554
11664
  continue;
9555
11665
  }
9556
11666
  try {
9557
- const { mtimeMs } = fs27.statSync(absPath);
11667
+ const { mtimeMs } = fs28.statSync(absPath);
9558
11668
  if (mtimeMs > stored) changed.push(absPath);
9559
11669
  } catch {
9560
11670
  changed.push(absPath);
@@ -9566,8 +11676,8 @@ function buildMtimeSnapshot(filePaths, workspaceRoot) {
9566
11676
  const snap = {};
9567
11677
  for (const absPath of filePaths) {
9568
11678
  try {
9569
- const { mtimeMs } = fs27.statSync(absPath);
9570
- snap[path29.relative(workspaceRoot, absPath)] = mtimeMs;
11679
+ const { mtimeMs } = fs28.statSync(absPath);
11680
+ snap[path30.relative(workspaceRoot, absPath)] = mtimeMs;
9571
11681
  } catch {
9572
11682
  }
9573
11683
  }
@@ -9578,8 +11688,8 @@ function decideIncremental(workspaceRoot, allFilePaths, prevCommitHash, storedMt
9578
11688
  if (prevCommitHash) {
9579
11689
  const changed = getChangedFilesSince(workspaceRoot, prevCommitHash);
9580
11690
  if (changed !== null) {
9581
- const scanSet = new Set(allFilePaths.map((p) => path29.relative(workspaceRoot, p)));
9582
- const changedInScan = changed.filter((rel) => scanSet.has(rel)).map((rel) => path29.join(workspaceRoot, rel));
11691
+ const scanSet = new Set(allFilePaths.map((p) => path30.relative(workspaceRoot, p)));
11692
+ const changedInScan = changed.filter((rel) => scanSet.has(rel)).map((rel) => path30.join(workspaceRoot, rel));
9583
11693
  if (total > 0 && changedInScan.length / total > 0.2) {
9584
11694
  return { incremental: false, fallbackReason: `changed files (${changedInScan.length}) > 20% of total (${total})` };
9585
11695
  }
@@ -9693,17 +11803,17 @@ var MigrationRunner = class {
9693
11803
  autoBackupBeforeMigration() {
9694
11804
  try {
9695
11805
  const dbFile = this.db.name;
9696
- if (!dbFile || !fs27.existsSync(dbFile)) return;
9697
- const backupDir = path29.join(os12.homedir(), ".code-intel", "backups", "pre-migration");
9698
- fs27.mkdirSync(backupDir, { recursive: true });
11806
+ if (!dbFile || !fs28.existsSync(dbFile)) return;
11807
+ const backupDir = path30.join(os12.homedir(), ".code-intel", "backups", "pre-migration");
11808
+ fs28.mkdirSync(backupDir, { recursive: true });
9699
11809
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
9700
- const baseName = path29.basename(dbFile, ".db");
9701
- const backupPath = path29.join(backupDir, `${baseName}-pre-migration-${ts}.db`);
11810
+ const baseName = path30.basename(dbFile, ".db");
11811
+ const backupPath = path30.join(backupDir, `${baseName}-pre-migration-${ts}.db`);
9702
11812
  try {
9703
11813
  this.db.pragma("wal_checkpoint(TRUNCATE)");
9704
11814
  } catch {
9705
11815
  }
9706
- fs27.copyFileSync(dbFile, backupPath);
11816
+ fs28.copyFileSync(dbFile, backupPath);
9707
11817
  } catch {
9708
11818
  }
9709
11819
  }
@@ -9919,7 +12029,7 @@ program.name("code-intel").description("Code Intelligence Platform \u2014 Static
9919
12029
  Docs: https://github.com/vohongtho/code-intel-platform
9920
12030
  `);
9921
12031
  async function analyzeWorkspace(targetPath, options) {
9922
- const workspaceRoot = path29.resolve(targetPath);
12032
+ const workspaceRoot = path30.resolve(targetPath);
9923
12033
  if (!options?.silent) console.log(`Analyzing: ${workspaceRoot}`);
9924
12034
  logger_default.info(`analyze started: ${workspaceRoot}`);
9925
12035
  if (options?.force) {
@@ -9940,14 +12050,14 @@ async function analyzeWorkspace(targetPath, options) {
9940
12050
  ];
9941
12051
  for (const f of wipeFiles) {
9942
12052
  try {
9943
- if (fs27.existsSync(f)) fs27.unlinkSync(f);
12053
+ if (fs28.existsSync(f)) fs28.unlinkSync(f);
9944
12054
  } catch {
9945
12055
  }
9946
12056
  }
9947
12057
  }
9948
12058
  if (!options?.skipGit) {
9949
- const gitDir = path29.join(workspaceRoot, ".git");
9950
- if (!fs27.existsSync(gitDir)) {
12059
+ const gitDir = path30.join(workspaceRoot, ".git");
12060
+ if (!fs28.existsSync(gitDir)) {
9951
12061
  logger_default.warn(`${workspaceRoot} is not a Git repository`);
9952
12062
  }
9953
12063
  }
@@ -10058,17 +12168,17 @@ async function analyzeWorkspace(targetPath, options) {
10058
12168
  const result = await runPipeline(phases, context2);
10059
12169
  if (isIncremental && incrementalChangedFiles && incrementalChangedFiles.length > 0) {
10060
12170
  const dbPath = getDbPath(workspaceRoot);
10061
- if (fs27.existsSync(dbPath)) {
12171
+ if (fs28.existsSync(dbPath)) {
10062
12172
  try {
10063
12173
  const db = new DbManager(dbPath);
10064
12174
  await db.init();
10065
12175
  for (const absPath of incrementalChangedFiles) {
10066
- const rel = path29.relative(workspaceRoot, absPath);
12176
+ const rel = path30.relative(workspaceRoot, absPath);
10067
12177
  await removeNodesForFile(rel, db);
10068
12178
  }
10069
12179
  const { upsertNodes: upsertNodesBatch } = await Promise.resolve().then(() => (init_graph_loader(), graph_loader_exports));
10070
12180
  const changedRelPaths = new Set(
10071
- incrementalChangedFiles.map((f) => path29.relative(workspaceRoot, f))
12181
+ incrementalChangedFiles.map((f) => path30.relative(workspaceRoot, f))
10072
12182
  );
10073
12183
  const nodesToUpsert = [...graph.allNodes()].filter(
10074
12184
  (n) => changedRelPaths.has(n.filePath)
@@ -10093,7 +12203,7 @@ async function analyzeWorkspace(targetPath, options) {
10093
12203
  mergedMtimes = newMtimes;
10094
12204
  }
10095
12205
  const currentCommitHash = getCurrentCommitHash(workspaceRoot) ?? void 0;
10096
- const repoName = path29.basename(workspaceRoot);
12206
+ const repoName = path30.basename(workspaceRoot);
10097
12207
  const indexVersion = v4();
10098
12208
  saveMetadata(workspaceRoot, {
10099
12209
  indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -10131,7 +12241,7 @@ async function analyzeWorkspace(targetPath, options) {
10131
12241
  ];
10132
12242
  for (const f of newStaleFiles) {
10133
12243
  try {
10134
- if (fs27.existsSync(f)) fs27.unlinkSync(f);
12244
+ if (fs28.existsSync(f)) fs28.unlinkSync(f);
10135
12245
  } catch {
10136
12246
  }
10137
12247
  }
@@ -10148,21 +12258,21 @@ async function analyzeWorkspace(targetPath, options) {
10148
12258
  ];
10149
12259
  for (const f of staleFiles) {
10150
12260
  try {
10151
- if (fs27.existsSync(f)) fs27.unlinkSync(f);
12261
+ if (fs28.existsSync(f)) fs28.unlinkSync(f);
10152
12262
  } catch {
10153
12263
  }
10154
12264
  }
10155
12265
  for (const f of newStaleFiles) {
10156
12266
  if (f === dbPathNew) continue;
10157
- if (fs27.existsSync(f)) {
12267
+ if (fs28.existsSync(f)) {
10158
12268
  const dest = f.replace(dbPathNew, dbPath);
10159
12269
  try {
10160
- fs27.renameSync(f, dest);
12270
+ fs28.renameSync(f, dest);
10161
12271
  } catch {
10162
12272
  }
10163
12273
  }
10164
12274
  }
10165
- fs27.renameSync(dbPathNew, dbPath);
12275
+ fs28.renameSync(dbPathNew, dbPath);
10166
12276
  stopSpinner();
10167
12277
  logger_default.info(`DB persisted: ${nodeCount} nodes, ${edgeCount} edges`);
10168
12278
  if (!options?.silent) {
@@ -10183,7 +12293,7 @@ async function analyzeWorkspace(targetPath, options) {
10183
12293
  const staleVdb = [vdbPath, `${vdbPath}-shm`, `${vdbPath}-wal`];
10184
12294
  for (const f of staleVdb) {
10185
12295
  try {
10186
- if (fs27.existsSync(f)) fs27.unlinkSync(f);
12296
+ if (fs28.existsSync(f)) fs28.unlinkSync(f);
10187
12297
  } catch {
10188
12298
  }
10189
12299
  }
@@ -10281,8 +12391,8 @@ program.command("setup").description("Configure MCP server for your editors (one
10281
12391
  const configFile = `${configDir}/claude_desktop_config.json`;
10282
12392
  try {
10283
12393
  let existing = {};
10284
- if (fs27.existsSync(configFile)) {
10285
- existing = JSON.parse(fs27.readFileSync(configFile, "utf-8"));
12394
+ if (fs28.existsSync(configFile)) {
12395
+ existing = JSON.parse(fs28.readFileSync(configFile, "utf-8"));
10286
12396
  }
10287
12397
  const merged = {
10288
12398
  ...existing,
@@ -10291,8 +12401,8 @@ program.command("setup").description("Configure MCP server for your editors (one
10291
12401
  ...mcpConfig.mcpServers
10292
12402
  }
10293
12403
  };
10294
- fs27.mkdirSync(configDir, { recursive: true });
10295
- fs27.writeFileSync(configFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
12404
+ fs28.mkdirSync(configDir, { recursive: true });
12405
+ fs28.writeFileSync(configFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
10296
12406
  console.log(`
10297
12407
  \u2705 Written to ${configFile}`);
10298
12408
  } catch (err) {
@@ -10340,8 +12450,14 @@ program.command("analyze").description("Index a repository and build the knowled
10340
12450
  summarize: opts.summarize,
10341
12451
  llmProvider: opts.llmProvider,
10342
12452
  llmModel: opts.llmModel,
10343
- llmBatchSize: opts.llmBatchSize ? parseInt(opts.llmBatchSize, 10) : void 0,
10344
- llmMaxNodes: opts.llmMaxNodes ? parseInt(opts.llmMaxNodes, 10) : void 0
12453
+ llmBatchSize: (() => {
12454
+ const v = parseInt(opts.llmBatchSize ?? "", 10);
12455
+ return Number.isFinite(v) && v >= 1 ? v : void 0;
12456
+ })(),
12457
+ llmMaxNodes: (() => {
12458
+ const v = parseInt(opts.llmMaxNodes ?? "", 10);
12459
+ return Number.isFinite(v) && v >= 1 ? v : void 0;
12460
+ })()
10345
12461
  });
10346
12462
  process.exit(0);
10347
12463
  });
@@ -10356,10 +12472,10 @@ program.command("mcp").description("Start MCP server over stdio \u2014 exposes a
10356
12472
  $ code-intel mcp
10357
12473
  $ code-intel mcp ./my-project
10358
12474
  `).action(async (targetPath) => {
10359
- const workspaceRoot = path29.resolve(targetPath);
10360
- const repoName = path29.basename(workspaceRoot);
12475
+ const workspaceRoot = path30.resolve(targetPath);
12476
+ const repoName = path30.basename(workspaceRoot);
10361
12477
  const dbPath = getDbPath(workspaceRoot);
10362
- const existingIndex = fs27.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
12478
+ const existingIndex = fs28.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
10363
12479
  if (existingIndex) {
10364
12480
  const graph = createKnowledgeGraph();
10365
12481
  const db = new DbManager(dbPath);
@@ -10390,10 +12506,10 @@ program.command("serve").description("Start the local HTTP server + web UI for g
10390
12506
  $ code-intel serve --port 8080
10391
12507
  $ code-intel serve --force
10392
12508
  `).action(async (targetPath, options) => {
10393
- const workspaceRoot = path29.resolve(targetPath);
10394
- const repoName = path29.basename(workspaceRoot);
12509
+ const workspaceRoot = path30.resolve(targetPath);
12510
+ const repoName = path30.basename(workspaceRoot);
10395
12511
  const dbPath = getDbPath(workspaceRoot);
10396
- const existingIndex = !options.force && fs27.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
12512
+ const existingIndex = !options.force && fs28.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
10397
12513
  if (existingIndex) {
10398
12514
  const meta = loadMetadata(workspaceRoot);
10399
12515
  if (meta.parser === "regex" || meta.parser === void 0) {
@@ -10427,10 +12543,10 @@ program.command("watch").description("Start HTTP server + file watcher (auto-rei
10427
12543
  `).action(async (targetPath, options) => {
10428
12544
  const { FileWatcher: FileWatcher2 } = await Promise.resolve().then(() => (init_file_watcher(), file_watcher_exports));
10429
12545
  const { IncrementalIndexer: IncrementalIndexer2 } = await Promise.resolve().then(() => (init_incremental_indexer(), incremental_indexer_exports));
10430
- const workspaceRoot = path29.resolve(targetPath);
10431
- const repoName = path29.basename(workspaceRoot);
12546
+ const workspaceRoot = path30.resolve(targetPath);
12547
+ const repoName = path30.basename(workspaceRoot);
10432
12548
  const dbPath = getDbPath(workspaceRoot);
10433
- const existingIndex = !options.force && fs27.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
12549
+ const existingIndex = !options.force && fs28.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
10434
12550
  let graph;
10435
12551
  if (existingIndex) {
10436
12552
  const meta = loadMetadata(workspaceRoot);
@@ -10465,7 +12581,7 @@ program.command("watch").description("Start HTTP server + file watcher (auto-rei
10465
12581
  type: "graph:updated",
10466
12582
  indexVersion: meta?.indexVersion ?? "unknown",
10467
12583
  stats: { nodes: graph.size.nodes, edges: graph.size.edges },
10468
- changedFiles: changedFiles.map((f) => path29.relative(workspaceRoot, f)),
12584
+ changedFiles: changedFiles.map((f) => path30.relative(workspaceRoot, f)),
10469
12585
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10470
12586
  });
10471
12587
  }
@@ -10516,7 +12632,7 @@ program.command("status").description("Show index freshness and statistics for a
10516
12632
  $ code-intel status
10517
12633
  $ code-intel status ./my-project
10518
12634
  `).action((targetPath) => {
10519
- const workspaceRoot = path29.resolve(targetPath);
12635
+ const workspaceRoot = path30.resolve(targetPath);
10520
12636
  const meta = loadMetadata(workspaceRoot);
10521
12637
  if (!meta) {
10522
12638
  console.log(`
@@ -10540,18 +12656,18 @@ function trashDirName(repoPath) {
10540
12656
  return `.code-intel-trash-${date}`;
10541
12657
  }
10542
12658
  function softDeleteCodeIntel(repoPath) {
10543
- const codeIntelDir = path29.join(repoPath, ".code-intel");
10544
- if (!fs27.existsSync(codeIntelDir)) return;
12659
+ const codeIntelDir = path30.join(repoPath, ".code-intel");
12660
+ if (!fs28.existsSync(codeIntelDir)) return;
10545
12661
  const trashName = trashDirName();
10546
- const trashDir = path29.join(repoPath, trashName);
12662
+ const trashDir = path30.join(repoPath, trashName);
10547
12663
  let dest = trashDir;
10548
12664
  let counter = 1;
10549
- while (fs27.existsSync(dest)) {
12665
+ while (fs28.existsSync(dest)) {
10550
12666
  dest = `${trashDir}-${counter++}`;
10551
12667
  }
10552
- fs27.renameSync(codeIntelDir, dest);
10553
- fs27.writeFileSync(
10554
- path29.join(dest, "TRASH_META.json"),
12668
+ fs28.renameSync(codeIntelDir, dest);
12669
+ fs28.writeFileSync(
12670
+ path30.join(dest, "TRASH_META.json"),
10555
12671
  JSON.stringify({ deletedAt: (/* @__PURE__ */ new Date()).toISOString(), repoPath, permanent: false }, null, 2)
10556
12672
  );
10557
12673
  console.log(` \u2713 Moved to trash: ${dest}`);
@@ -10560,15 +12676,15 @@ function softDeleteCodeIntel(repoPath) {
10560
12676
  function purgeStaleTrashes(repoPath) {
10561
12677
  const cutoff = Date.now() - TRASH_TTL_DAYS * 24 * 60 * 60 * 1e3;
10562
12678
  try {
10563
- for (const entry of fs27.readdirSync(repoPath)) {
12679
+ for (const entry of fs28.readdirSync(repoPath)) {
10564
12680
  if (!entry.startsWith(".code-intel-trash-")) continue;
10565
- const fullPath = path29.join(repoPath, entry);
10566
- const metaPath = path29.join(fullPath, "TRASH_META.json");
10567
- if (fs27.existsSync(metaPath)) {
12681
+ const fullPath = path30.join(repoPath, entry);
12682
+ const metaPath = path30.join(fullPath, "TRASH_META.json");
12683
+ if (fs28.existsSync(metaPath)) {
10568
12684
  try {
10569
- const meta = JSON.parse(fs27.readFileSync(metaPath, "utf-8"));
12685
+ const meta = JSON.parse(fs28.readFileSync(metaPath, "utf-8"));
10570
12686
  if (new Date(meta.deletedAt).getTime() < cutoff) {
10571
- fs27.rmSync(fullPath, { recursive: true, force: true });
12687
+ fs28.rmSync(fullPath, { recursive: true, force: true });
10572
12688
  console.log(` \u2713 Auto-purged stale trash: ${fullPath}`);
10573
12689
  }
10574
12690
  } catch {
@@ -10595,18 +12711,18 @@ program.command("clean").description("Soft-delete the knowledge graph index for
10595
12711
  if (opts.listTrash) {
10596
12712
  const repos = loadRegistry();
10597
12713
  const roots = repos.map((r) => r.path);
10598
- if (roots.length === 0) roots.push(path29.resolve("."));
12714
+ if (roots.length === 0) roots.push(path30.resolve("."));
10599
12715
  let found = 0;
10600
12716
  for (const root of roots) {
10601
12717
  try {
10602
- for (const entry of fs27.readdirSync(root)) {
12718
+ for (const entry of fs28.readdirSync(root)) {
10603
12719
  if (!entry.startsWith(".code-intel-trash-")) continue;
10604
- const fullPath = path29.join(root, entry);
10605
- const metaPath = path29.join(fullPath, "TRASH_META.json");
12720
+ const fullPath = path30.join(root, entry);
12721
+ const metaPath = path30.join(fullPath, "TRASH_META.json");
10606
12722
  let deletedAt = "unknown";
10607
- if (fs27.existsSync(metaPath)) {
12723
+ if (fs28.existsSync(metaPath)) {
10608
12724
  try {
10609
- deletedAt = JSON.parse(fs27.readFileSync(metaPath, "utf-8")).deletedAt;
12725
+ deletedAt = JSON.parse(fs28.readFileSync(metaPath, "utf-8")).deletedAt;
10610
12726
  } catch {
10611
12727
  }
10612
12728
  }
@@ -10634,9 +12750,9 @@ program.command("clean").description("Soft-delete the knowledge graph index for
10634
12750
  }
10635
12751
  for (const r of repos) {
10636
12752
  if (opts.purge) {
10637
- const codeIntelDir = path29.join(r.path, ".code-intel");
10638
- if (fs27.existsSync(codeIntelDir)) {
10639
- fs27.rmSync(codeIntelDir, { recursive: true, force: true });
12753
+ const codeIntelDir = path30.join(r.path, ".code-intel");
12754
+ if (fs28.existsSync(codeIntelDir)) {
12755
+ fs28.rmSync(codeIntelDir, { recursive: true, force: true });
10640
12756
  console.log(` \u2713 Hard-deleted ${codeIntelDir}`);
10641
12757
  }
10642
12758
  } else {
@@ -10650,11 +12766,11 @@ program.command("clean").description("Soft-delete the knowledge graph index for
10650
12766
  `);
10651
12767
  return;
10652
12768
  }
10653
- const workspaceRoot = path29.resolve(targetPath);
12769
+ const workspaceRoot = path30.resolve(targetPath);
10654
12770
  if (opts.purge) {
10655
- const codeIntelDir = path29.join(workspaceRoot, ".code-intel");
10656
- if (fs27.existsSync(codeIntelDir)) {
10657
- fs27.rmSync(codeIntelDir, { recursive: true, force: true });
12771
+ const codeIntelDir = path30.join(workspaceRoot, ".code-intel");
12772
+ if (fs28.existsSync(codeIntelDir)) {
12773
+ fs28.rmSync(codeIntelDir, { recursive: true, force: true });
10658
12774
  console.log(`
10659
12775
  \u2713 Hard-deleted ${codeIntelDir}`);
10660
12776
  }
@@ -10666,16 +12782,16 @@ program.command("clean").description("Soft-delete the knowledge graph index for
10666
12782
  console.log(" Index cleaned.\n");
10667
12783
  });
10668
12784
  async function loadOrAnalyzeWorkspace(targetPath) {
10669
- const workspaceRoot = path29.resolve(targetPath);
12785
+ const workspaceRoot = path30.resolve(targetPath);
10670
12786
  const dbPath = getDbPath(workspaceRoot);
10671
- const existingIndex = fs27.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
12787
+ const existingIndex = fs28.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
10672
12788
  if (existingIndex) {
10673
12789
  const graph = createKnowledgeGraph();
10674
12790
  const db = new DbManager(dbPath);
10675
12791
  await db.init();
10676
12792
  await loadGraphFromDB(graph, db);
10677
12793
  db.close();
10678
- return { graph, workspaceRoot, repoName: path29.basename(workspaceRoot) };
12794
+ return { graph, workspaceRoot, repoName: path30.basename(workspaceRoot) };
10679
12795
  }
10680
12796
  return analyzeWorkspace(targetPath, { silent: true });
10681
12797
  }
@@ -11103,9 +13219,9 @@ groupCmd.command("status <name>").description("Check index freshness and sync st
11103
13219
  console.log(` \u2717 ${m.groupPath.padEnd(35)} [${m.registryName}] \u2014 NOT IN REGISTRY`);
11104
13220
  continue;
11105
13221
  }
11106
- const metaPath = path29.join(regEntry.path, ".code-intel", "meta.json");
13222
+ const metaPath = path30.join(regEntry.path, ".code-intel", "meta.json");
11107
13223
  try {
11108
- const meta = JSON.parse(fs27.readFileSync(metaPath, "utf-8"));
13224
+ const meta = JSON.parse(fs28.readFileSync(metaPath, "utf-8"));
11109
13225
  const indexedAt = meta.indexedAt;
11110
13226
  const ageMin = Math.round((now - new Date(indexedAt).getTime()) / 6e4);
11111
13227
  const stale = ageMin > 1440 ? " \u26A0 STALE (>24h)" : "";
@@ -11322,7 +13438,7 @@ backupCmd.command("create [path]").description("Create an encrypted backup of th
11322
13438
  $ code-intel backup create
11323
13439
  $ code-intel backup create ./my-project
11324
13440
  `).action((targetPath = ".") => {
11325
- const repoPath = path29.resolve(targetPath);
13441
+ const repoPath = path30.resolve(targetPath);
11326
13442
  const svc = new BackupService();
11327
13443
  try {
11328
13444
  const entry = svc.createBackup(repoPath);
@@ -11351,7 +13467,7 @@ backupCmd.command("list").description("List all available backups").action(() =>
11351
13467
  Backups (${entries.length}):
11352
13468
  `);
11353
13469
  for (const e of entries) {
11354
- const exists = fs27.existsSync(e.path);
13470
+ const exists = fs28.existsSync(e.path);
11355
13471
  const status = exists ? "\u2713" : "\u2717 (missing)";
11356
13472
  console.log(` ${status} ${e.id.slice(0, 8)} ${e.createdAt} ${(e.size / 1024).toFixed(1)} KB \u2192 ${e.repoPath}`);
11357
13473
  }
@@ -11364,7 +13480,7 @@ backupCmd.command("restore <id>").description("Restore a backup by ID").option("
11364
13480
  `).action((id, opts) => {
11365
13481
  const svc = new BackupService();
11366
13482
  try {
11367
- const targetPath = opts.target ? path29.resolve(opts.target) : void 0;
13483
+ const targetPath = opts.target ? path30.resolve(opts.target) : void 0;
11368
13484
  svc.restoreBackup(id, targetPath);
11369
13485
  console.log(`
11370
13486
  \u2705 Backup "${id}" restored successfully.
@@ -11383,8 +13499,8 @@ program.command("migrate").description("Manage database schema migrations").opti
11383
13499
  $ code-intel migrate
11384
13500
  $ code-intel migrate --rollback
11385
13501
  `).action((opts) => {
11386
- const dbPath = opts.db ?? path29.join(os12.homedir(), ".code-intel", "users.db");
11387
- if (!fs27.existsSync(dbPath)) {
13502
+ const dbPath = opts.db ?? path30.join(os12.homedir(), ".code-intel", "users.db");
13503
+ if (!fs28.existsSync(dbPath)) {
11388
13504
  console.error(`
11389
13505
  \u2717 Database not found: ${dbPath}
11390
13506
  Run \`code-intel serve\` or \`code-intel user create\` first.
@@ -11499,15 +13615,15 @@ authCmd.command("login").description("Authenticate via OIDC device flow \u2014 o
11499
13615
  }
11500
13616
  try {
11501
13617
  const tokens = await pollDeviceFlow3(config, deviceResponse);
11502
- const tokenPath = path29.join(os12.homedir(), ".code-intel", "oidc-token.json");
13618
+ const tokenPath = path30.join(os12.homedir(), ".code-intel", "oidc-token.json");
11503
13619
  const tokenData = {
11504
13620
  accessToken: tokens.accessToken,
11505
13621
  refreshToken: tokens.refreshToken,
11506
13622
  server: serverUrl,
11507
13623
  storedAt: (/* @__PURE__ */ new Date()).toISOString()
11508
13624
  };
11509
- fs27.mkdirSync(path29.dirname(tokenPath), { recursive: true });
11510
- fs27.writeFileSync(tokenPath, JSON.stringify(tokenData, null, 2), { mode: 384 });
13625
+ fs28.mkdirSync(path30.dirname(tokenPath), { recursive: true });
13626
+ fs28.writeFileSync(tokenPath, JSON.stringify(tokenData, null, 2), { mode: 384 });
11511
13627
  console.log(` \u2705 Authenticated successfully!`);
11512
13628
  console.log(` Token stored at: ${tokenPath}`);
11513
13629
  console.log(` Use CODE_INTEL_TOKEN env var or --token flag to use it with CLI/MCP.
@@ -11520,13 +13636,13 @@ authCmd.command("login").description("Authenticate via OIDC device flow \u2014 o
11520
13636
  }
11521
13637
  });
11522
13638
  authCmd.command("status").description("Show the current OIDC authentication status").action(() => {
11523
- const tokenPath = path29.join(os12.homedir(), ".code-intel", "oidc-token.json");
11524
- if (!fs27.existsSync(tokenPath)) {
13639
+ const tokenPath = path30.join(os12.homedir(), ".code-intel", "oidc-token.json");
13640
+ if (!fs28.existsSync(tokenPath)) {
11525
13641
  console.log("\n Not authenticated via OIDC. Run: code-intel auth login\n");
11526
13642
  return;
11527
13643
  }
11528
13644
  try {
11529
- const data = JSON.parse(fs27.readFileSync(tokenPath, "utf-8"));
13645
+ const data = JSON.parse(fs28.readFileSync(tokenPath, "utf-8"));
11530
13646
  console.log(`
11531
13647
  \u2705 OIDC token stored`);
11532
13648
  console.log(` Server : ${data.server ?? "unknown"}`);
@@ -11538,9 +13654,9 @@ authCmd.command("status").description("Show the current OIDC authentication stat
11538
13654
  }
11539
13655
  });
11540
13656
  authCmd.command("logout").description("Remove locally stored OIDC token").action(() => {
11541
- const tokenPath = path29.join(os12.homedir(), ".code-intel", "oidc-token.json");
11542
- if (fs27.existsSync(tokenPath)) {
11543
- fs27.unlinkSync(tokenPath);
13657
+ const tokenPath = path30.join(os12.homedir(), ".code-intel", "oidc-token.json");
13658
+ if (fs28.existsSync(tokenPath)) {
13659
+ fs28.unlinkSync(tokenPath);
11544
13660
  console.log("\n \u2705 OIDC token removed. You are now logged out.\n");
11545
13661
  } else {
11546
13662
  console.log("\n No stored token found.\n");
@@ -11664,8 +13780,8 @@ program.command("config-validate <file>").description("Validate a JSON config fi
11664
13780
  $ code-intel config-validate ./config.json
11665
13781
  $ code-intel config-validate ~/.code-intel/config.json
11666
13782
  `).action((file) => {
11667
- const filePath = path29.resolve(file);
11668
- if (!fs27.existsSync(filePath)) {
13783
+ const filePath = path30.resolve(file);
13784
+ if (!fs28.existsSync(filePath)) {
11669
13785
  console.error(`
11670
13786
  \u2717 File not found: ${filePath}
11671
13787
  `);
@@ -11673,7 +13789,7 @@ program.command("config-validate <file>").description("Validate a JSON config fi
11673
13789
  }
11674
13790
  let cfg;
11675
13791
  try {
11676
- cfg = JSON.parse(fs27.readFileSync(filePath, "utf-8"));
13792
+ cfg = JSON.parse(fs28.readFileSync(filePath, "utf-8"));
11677
13793
  } catch (err) {
11678
13794
  console.error(`
11679
13795
  \u2717 Could not parse JSON: ${err instanceof Error ? err.message : err}
@@ -11694,7 +13810,7 @@ ${err instanceof Error ? err.message : err}
11694
13810
  });
11695
13811
  (function ensurePermissions() {
11696
13812
  try {
11697
- const dir = path29.join(os12.homedir(), ".code-intel");
13813
+ const dir = path30.join(os12.homedir(), ".code-intel");
11698
13814
  secureMkdir(dir);
11699
13815
  tightenDbFiles(dir);
11700
13816
  } catch {
@@ -11711,22 +13827,22 @@ program.command("health").description("Run code health checks: dead code, circul
11711
13827
  $ code-intel health --json
11712
13828
  $ code-intel health --threshold 80
11713
13829
  `).action(async (targetPath, opts) => {
11714
- const workspaceRoot = path29.resolve(targetPath);
13830
+ const workspaceRoot = path30.resolve(targetPath);
11715
13831
  const dbPath = getDbPath(workspaceRoot);
11716
13832
  const meta = loadMetadata(workspaceRoot);
11717
- if (!meta || !fs27.existsSync(dbPath)) {
13833
+ if (!meta || !fs28.existsSync(dbPath)) {
11718
13834
  console.error(`
11719
13835
  \u2717 ${workspaceRoot} is not indexed.`);
11720
13836
  console.error(" Run `code-intel analyze` first to build the index.\n");
11721
13837
  process.exit(1);
11722
13838
  }
11723
- const { computeHealthReport: computeHealthReport2 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
13839
+ const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
11724
13840
  const graph = createKnowledgeGraph();
11725
13841
  const db = new DbManager(dbPath);
11726
13842
  await db.init();
11727
13843
  await loadGraphFromDB(graph, db);
11728
13844
  db.close();
11729
- const report = computeHealthReport2(graph);
13845
+ const report = computeHealthReport3(graph);
11730
13846
  if (opts.json) {
11731
13847
  console.log(JSON.stringify({
11732
13848
  score: report.score,
@@ -11778,6 +13894,207 @@ program.command("health").description("Run code health checks: dead code, circul
11778
13894
  }
11779
13895
  }
11780
13896
  });
13897
+ program.command("query").description("Execute a GQL (Graph Query Language) query against the knowledge graph").argument("[gql]", `GQL query string (e.g. "FIND function WHERE name CONTAINS 'auth'")`).option("-f, --file <path>", "Read GQL query from a file").option("--format <format>", "Output format: table|json|csv (default: table)", "table").option("-l, --limit <n>", "Override LIMIT in the query").option("-p, --path <path>", "Path to the repository (default: current directory)", ".").option("--save <name>", "Save the GQL query under a name").option("--run <name>", "Run a saved query by name").option("--list", "List all saved queries").option("--delete <name>", "Delete a saved query by name").addHelpText("after", `
13898
+ Execute GQL queries against the knowledge graph.
13899
+
13900
+ Syntax:
13901
+ FIND function WHERE name CONTAINS "auth"
13902
+ FIND * WHERE kind IN [function, method] LIMIT 50
13903
+ TRAVERSE CALLS FROM "handleLogin" DEPTH 3
13904
+ PATH FROM "createUser" TO "sendEmail"
13905
+ COUNT function GROUP BY cluster
13906
+
13907
+ Examples:
13908
+ $ code-intel query "FIND function WHERE name CONTAINS 'auth'"
13909
+ $ code-intel query --file ./my.gql
13910
+ $ code-intel query "FIND class" --format json
13911
+ $ code-intel query "FIND class" --format csv
13912
+ $ code-intel query "FIND class" --limit 20
13913
+ $ code-intel query --save auth-search "FIND function WHERE name CONTAINS 'auth'"
13914
+ $ code-intel query --run auth-search
13915
+ $ code-intel query --list
13916
+ $ code-intel query --delete auth-search
13917
+ `).action(async (gqlArg, opts) => {
13918
+ const workspaceRoot = path30.resolve(opts.path);
13919
+ const { saveQuery: saveQuery2, loadQuery: loadQuery2, listQueries: listQueries2, deleteQuery: deleteQuery2 } = await Promise.resolve().then(() => (init_saved_queries(), saved_queries_exports));
13920
+ if (opts.list) {
13921
+ const queries = listQueries2(workspaceRoot);
13922
+ if (queries.length === 0) {
13923
+ console.log("\n No saved queries found.");
13924
+ console.log(` Save one with: code-intel query --save <name> "<gql>"
13925
+ `);
13926
+ return;
13927
+ }
13928
+ console.log(`
13929
+ Saved queries (${queries.length}):
13930
+ `);
13931
+ for (const q of queries) {
13932
+ console.log(` \u25C6 ${q.name.padEnd(25)} ${q.content.slice(0, 60)}${q.content.length > 60 ? "\u2026" : ""}`);
13933
+ }
13934
+ console.log("");
13935
+ return;
13936
+ }
13937
+ if (opts.delete) {
13938
+ const { deleteQuery: dq } = await Promise.resolve().then(() => (init_saved_queries(), saved_queries_exports));
13939
+ const deleted = dq(workspaceRoot, opts.delete);
13940
+ if (!deleted) {
13941
+ console.error(`
13942
+ \u2717 Saved query "${opts.delete}" not found.
13943
+ `);
13944
+ process.exit(1);
13945
+ }
13946
+ console.log(`
13947
+ \u2705 Deleted saved query "${opts.delete}"
13948
+ `);
13949
+ return;
13950
+ }
13951
+ if (opts.save) {
13952
+ const gqlContent = gqlArg;
13953
+ if (!gqlContent) {
13954
+ console.error("\n \u2717 Provide a GQL query string to save.");
13955
+ console.error(' Example: code-intel query --save my-query "FIND function"\n');
13956
+ process.exit(1);
13957
+ }
13958
+ saveQuery2(workspaceRoot, opts.save, gqlContent);
13959
+ console.log(`
13960
+ \u2705 Saved query "${opts.save}"`);
13961
+ console.log(` Run with: code-intel query --run ${opts.save}
13962
+ `);
13963
+ return;
13964
+ }
13965
+ let gqlInput;
13966
+ if (opts.run) {
13967
+ const content = loadQuery2(workspaceRoot, opts.run);
13968
+ if (!content) {
13969
+ console.error(`
13970
+ \u2717 Saved query "${opts.run}" not found.`);
13971
+ console.error(` List saved queries with: code-intel query --list
13972
+ `);
13973
+ process.exit(1);
13974
+ }
13975
+ gqlInput = content;
13976
+ } else if (opts.file) {
13977
+ const filePath = path30.resolve(opts.file);
13978
+ if (!fs28.existsSync(filePath)) {
13979
+ console.error(`
13980
+ \u2717 File not found: ${filePath}
13981
+ `);
13982
+ process.exit(1);
13983
+ }
13984
+ gqlInput = fs28.readFileSync(filePath, "utf-8");
13985
+ } else if (gqlArg) {
13986
+ gqlInput = gqlArg;
13987
+ } else {
13988
+ console.error("\n \u2717 Provide a GQL query string, --file <path>, or --run <name>.\n");
13989
+ program.help();
13990
+ process.exit(1);
13991
+ }
13992
+ const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
13993
+ let ast = parseGQL2(gqlInput);
13994
+ if (opts.limit && !isGQLParseError2(ast)) {
13995
+ const limitN = parseInt(opts.limit, 10);
13996
+ if (!isNaN(limitN) && ast.type === "FIND") {
13997
+ ast.limit = limitN;
13998
+ }
13999
+ }
14000
+ if (isGQLParseError2(ast)) {
14001
+ console.error(`
14002
+ \u2717 Parse error: ${ast.message}`);
14003
+ if (ast.pos !== void 0) console.error(` Position: ${ast.pos}`);
14004
+ console.error("");
14005
+ process.exit(1);
14006
+ }
14007
+ const { graph } = await loadOrAnalyzeWorkspace(opts.path);
14008
+ const { executeGQL: executeGQL2 } = await Promise.resolve().then(() => (init_gql_executor(), gql_executor_exports));
14009
+ const result = executeGQL2(ast, graph);
14010
+ const format = opts.format.toLowerCase();
14011
+ if (result.totalCount === 0 && !result.groups?.length && result.path === null) {
14012
+ console.log(`
14013
+ No results found.
14014
+ `);
14015
+ return;
14016
+ }
14017
+ if (result.path !== null && result.path !== void 0) {
14018
+ if (result.path.length === 0) {
14019
+ console.log("\n No path found.\n");
14020
+ return;
14021
+ }
14022
+ if (format === "json") {
14023
+ console.log(JSON.stringify({ path: result.path, executionTimeMs: result.executionTimeMs }, null, 2));
14024
+ } else if (format === "csv") {
14025
+ console.log("kind,name,filePath");
14026
+ for (const n of result.path) {
14027
+ console.log(`${n.kind},${JSON.stringify(n.name)},${JSON.stringify(n.filePath)}`);
14028
+ }
14029
+ } else {
14030
+ console.log(`
14031
+ Path (${result.path.length} nodes):
14032
+ `);
14033
+ for (let i = 0; i < result.path.length; i++) {
14034
+ const n = result.path[i];
14035
+ const arrow = i < result.path.length - 1 ? " \u2192" : "";
14036
+ console.log(` ${n.kind.padEnd(14)} ${n.name.padEnd(32)} ${n.filePath}${arrow}`);
14037
+ }
14038
+ console.log(`
14039
+ Execution time: ${result.executionTimeMs}ms
14040
+ `);
14041
+ }
14042
+ return;
14043
+ }
14044
+ if (result.groups && !result.nodes?.length) {
14045
+ if (format === "json") {
14046
+ console.log(JSON.stringify({ groups: result.groups, totalCount: result.totalCount, executionTimeMs: result.executionTimeMs }, null, 2));
14047
+ } else if (format === "csv") {
14048
+ console.log("key,count");
14049
+ for (const g of result.groups) {
14050
+ console.log(`${JSON.stringify(g.key)},${g.count}`);
14051
+ }
14052
+ } else {
14053
+ console.log(`
14054
+ Count results (total: ${result.totalCount}):
14055
+ `);
14056
+ for (const g of result.groups) {
14057
+ console.log(` ${g.key.padEnd(30)} ${String(g.count).padStart(6)}`);
14058
+ }
14059
+ console.log(`
14060
+ Execution time: ${result.executionTimeMs}ms${result.truncated ? " \u26A0 (truncated)" : ""}
14061
+ `);
14062
+ }
14063
+ return;
14064
+ }
14065
+ const nodes = result.nodes ?? [];
14066
+ if (nodes.length === 0) {
14067
+ console.log("\n No results found.\n");
14068
+ return;
14069
+ }
14070
+ if (format === "json") {
14071
+ console.log(JSON.stringify({
14072
+ nodes,
14073
+ edges: result.edges,
14074
+ totalCount: result.totalCount,
14075
+ executionTimeMs: result.executionTimeMs,
14076
+ truncated: result.truncated
14077
+ }, null, 2));
14078
+ } else if (format === "csv") {
14079
+ console.log("kind,name,filePath,exported");
14080
+ for (const n of nodes) {
14081
+ console.log(`${n.kind},${JSON.stringify(n.name)},${JSON.stringify(n.filePath)},${n.exported ?? ""}`);
14082
+ }
14083
+ } else {
14084
+ const showing = nodes.length;
14085
+ const total = result.totalCount;
14086
+ const header = result.truncated ? ` (truncated)` : "";
14087
+ console.log(`
14088
+ ${showing} result(s)${total > showing ? ` (of ${total})` : ""}${header}:
14089
+ `);
14090
+ for (const n of nodes) {
14091
+ console.log(` ${n.kind.padEnd(14)} ${n.name.padEnd(32)} ${n.filePath}`);
14092
+ }
14093
+ console.log(`
14094
+ Execution time: ${result.executionTimeMs}ms
14095
+ `);
14096
+ }
14097
+ });
11781
14098
  program.parse();
11782
14099
  //# sourceMappingURL=main.js.map
11783
14100
  //# sourceMappingURL=main.js.map