@vohongtho.infotech/code-intel 0.1.4 → 0.1.5

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