@vohongtho.infotech/code-intel 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -7,9 +7,9 @@ import { SEMRESATTRS_DEPLOYMENT_ENVIRONMENT, SEMRESATTRS_SERVICE_NAME } from '@o
7
7
  import { SpanStatusCode, trace, context } from '@opentelemetry/api';
8
8
  import winston from 'winston';
9
9
  import DailyRotateFile from 'winston-daily-rotate-file';
10
- import fs38, { readFileSync, existsSync } from 'fs';
10
+ import fs39, { readFileSync, existsSync } from 'fs';
11
11
  import path39, { dirname, join } from 'path';
12
- import os13 from 'os';
12
+ import os18 from 'os';
13
13
  import { Registry, collectDefaultMetrics, Counter, Histogram, Gauge } from 'prom-client';
14
14
  import { createRequire } from 'module';
15
15
  import { fileURLToPath } from 'url';
@@ -38,6 +38,7 @@ import https from 'https';
38
38
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
39
39
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
40
40
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
41
+ import process2 from 'process';
41
42
 
42
43
  var __defProp = Object.defineProperty;
43
44
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -327,17 +328,17 @@ var init_logger = __esm({
327
328
  };
328
329
  }
329
330
  /** Global log directory: ~/.code-intel/logs */
330
- static LOG_DIR = path39.join(os13.homedir(), ".code-intel", "logs");
331
+ static LOG_DIR = path39.join(os18.homedir(), ".code-intel", "logs");
331
332
  static getLogger() {
332
333
  if (!_Logger.instance) {
333
334
  const isProduction = process.env.NODE_ENV === "production";
334
335
  const logLevel = process.env.LOG_LEVEL ?? "info";
335
336
  const transports = [];
336
- transports.push(new winston.transports.Console());
337
+ transports.push(new winston.transports.Console({ stderrLevels: ["error", "warn", "info", "http", "verbose", "debug", "silly"] }));
337
338
  if (!isProduction) {
338
339
  try {
339
- if (!fs38.existsSync(_Logger.LOG_DIR)) {
340
- fs38.mkdirSync(_Logger.LOG_DIR, { recursive: true });
340
+ if (!fs39.existsSync(_Logger.LOG_DIR)) {
341
+ fs39.mkdirSync(_Logger.LOG_DIR, { recursive: true });
341
342
  }
342
343
  transports.push(
343
344
  new DailyRotateFile({
@@ -394,6 +395,10 @@ var init_logger = __esm({
394
395
  });
395
396
 
396
397
  // src/graph/knowledge-graph.ts
398
+ var knowledge_graph_exports = {};
399
+ __export(knowledge_graph_exports, {
400
+ createKnowledgeGraph: () => createKnowledgeGraph
401
+ });
397
402
  function createKnowledgeGraph() {
398
403
  const nodes = /* @__PURE__ */ new Map();
399
404
  const edges = /* @__PURE__ */ new Map();
@@ -889,7 +894,7 @@ var init_id_generator = __esm({
889
894
  });
890
895
  function loadIgnorePatterns(workspaceRoot) {
891
896
  try {
892
- const raw = fs38.readFileSync(path39.join(workspaceRoot, ".codeintelignore"), "utf-8");
897
+ const raw = fs39.readFileSync(path39.join(workspaceRoot, ".codeintelignore"), "utf-8");
893
898
  const extras = /* @__PURE__ */ new Set();
894
899
  for (const line of raw.split("\n")) {
895
900
  const trimmed = line.trim();
@@ -949,7 +954,7 @@ var init_scan_phase = __esm({
949
954
  function walk2(dir) {
950
955
  let entries;
951
956
  try {
952
- entries = fs38.readdirSync(dir, { withFileTypes: true });
957
+ entries = fs39.readdirSync(dir, { withFileTypes: true });
953
958
  } catch {
954
959
  return;
955
960
  }
@@ -966,7 +971,7 @@ var init_scan_phase = __esm({
966
971
  if (!extensions.has(ext)) continue;
967
972
  const fullPath = path39.join(dir, name);
968
973
  try {
969
- const stat = fs38.statSync(fullPath);
974
+ const stat = fs39.statSync(fullPath);
970
975
  if (stat.size > MAX_FILE_SIZE_BYTES) continue;
971
976
  } catch {
972
977
  continue;
@@ -2519,7 +2524,7 @@ var init_parse_phase = __esm({
2519
2524
  const batch = filePaths.slice(i, i + CONCURRENCY);
2520
2525
  await Promise.all(batch.map(async (filePath) => {
2521
2526
  try {
2522
- const source = await fs38.promises.readFile(filePath, "utf-8");
2527
+ const source = await fs39.promises.readFile(filePath, "utf-8");
2523
2528
  context2.fileCache.set(filePath, source);
2524
2529
  } catch {
2525
2530
  }
@@ -3111,7 +3116,7 @@ var init_llm_governance = __esm({
3111
3116
  }
3112
3117
  /** Path to the JSONL log file. */
3113
3118
  getLogPath() {
3114
- return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path39.join(os13.homedir(), ".code-intel", "llm-governance.jsonl");
3119
+ return process.env["CODE_INTEL_GOVERNANCE_LOG_PATH"] ?? path39.join(os18.homedir(), ".code-intel", "llm-governance.jsonl");
3115
3120
  }
3116
3121
  /**
3117
3122
  * Append an entry to the governance log.
@@ -3127,8 +3132,8 @@ var init_llm_governance = __esm({
3127
3132
  ...entry
3128
3133
  };
3129
3134
  const logPath = this.getLogPath();
3130
- fs38.mkdirSync(path39.dirname(logPath), { recursive: true });
3131
- fs38.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
3135
+ fs39.mkdirSync(path39.dirname(logPath), { recursive: true });
3136
+ fs39.appendFileSync(logPath, JSON.stringify(full) + "\n", "utf-8");
3132
3137
  } catch {
3133
3138
  }
3134
3139
  }
@@ -3138,7 +3143,7 @@ var init_llm_governance = __esm({
3138
3143
  */
3139
3144
  readLog(limit = 100) {
3140
3145
  try {
3141
- const raw = fs38.readFileSync(this.getLogPath(), "utf-8");
3146
+ const raw = fs39.readFileSync(this.getLogPath(), "utf-8");
3142
3147
  const lines = raw.split("\n").filter((l) => l.trim().length > 0).slice(-limit);
3143
3148
  return lines.map((l) => JSON.parse(l));
3144
3149
  } catch {
@@ -3272,6 +3277,49 @@ var init_anthropic = __esm({
3272
3277
  }
3273
3278
  });
3274
3279
 
3280
+ // src/llm/providers/custom.ts
3281
+ var custom_exports = {};
3282
+ __export(custom_exports, {
3283
+ CustomProvider: () => CustomProvider
3284
+ });
3285
+ var CustomProvider;
3286
+ var init_custom = __esm({
3287
+ "src/llm/providers/custom.ts"() {
3288
+ CustomProvider = class {
3289
+ modelName;
3290
+ baseUrl;
3291
+ apiKey;
3292
+ constructor(baseUrl, model, apiKey = "") {
3293
+ this.baseUrl = baseUrl.replace(/\/$/, "");
3294
+ this.modelName = model;
3295
+ this.apiKey = apiKey;
3296
+ }
3297
+ async summarize(prompt2) {
3298
+ const url = `${this.baseUrl}/chat/completions`;
3299
+ const headers = {
3300
+ "Content-Type": "application/json"
3301
+ };
3302
+ if (this.apiKey) {
3303
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
3304
+ }
3305
+ const body = JSON.stringify({
3306
+ model: this.modelName,
3307
+ messages: [{ role: "user", content: prompt2 }],
3308
+ max_tokens: 200,
3309
+ temperature: 0.3
3310
+ });
3311
+ const res = await fetch(url, { method: "POST", headers, body });
3312
+ if (!res.ok) {
3313
+ const text = await res.text().catch(() => res.statusText);
3314
+ throw new Error(`Custom LLM API error ${res.status}: ${text}`);
3315
+ }
3316
+ const data = await res.json();
3317
+ return data.choices?.[0]?.message?.content?.trim() ?? "";
3318
+ }
3319
+ };
3320
+ }
3321
+ });
3322
+
3275
3323
  // src/llm/providers/ollama.ts
3276
3324
  var ollama_exports = {};
3277
3325
  __export(ollama_exports, {
@@ -3315,7 +3363,7 @@ __export(factory_exports, {
3315
3363
  createLLMProvider: () => createLLMProvider
3316
3364
  });
3317
3365
  async function createLLMProvider(config = {}) {
3318
- const { provider = "ollama", model } = config;
3366
+ const { provider = "ollama", model, baseUrl, apiKey } = config;
3319
3367
  switch (provider) {
3320
3368
  case "openai": {
3321
3369
  const { OpenAIProvider: OpenAIProvider2 } = await Promise.resolve().then(() => (init_openai(), openai_exports));
@@ -3325,6 +3373,13 @@ async function createLLMProvider(config = {}) {
3325
3373
  const { AnthropicProvider: AnthropicProvider2 } = await Promise.resolve().then(() => (init_anthropic(), anthropic_exports));
3326
3374
  return new AnthropicProvider2(model);
3327
3375
  }
3376
+ case "custom": {
3377
+ const { CustomProvider: CustomProvider2 } = await Promise.resolve().then(() => (init_custom(), custom_exports));
3378
+ const url = baseUrl ?? "http://localhost:1234/v1";
3379
+ const mdl = model ?? "default";
3380
+ const key = apiKey ?? "";
3381
+ return new CustomProvider2(url, mdl, key);
3382
+ }
3328
3383
  case "ollama":
3329
3384
  default: {
3330
3385
  const { OllamaProvider: OllamaProvider2 } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
@@ -3478,7 +3533,7 @@ var init_worker_pool = __esm({
3478
3533
  constructor(opts) {
3479
3534
  super();
3480
3535
  this.workerScript = opts.workerScript;
3481
- this.workerCount = opts.workerCount ?? Math.max(1, os13.cpus().length - 1);
3536
+ this.workerCount = opts.workerCount ?? Math.max(1, os18.cpus().length - 1);
3482
3537
  this.maxQueueSize = opts.maxQueueSize ?? 200;
3483
3538
  this.maxTaskRetries = opts.maxTaskRetries ?? 2;
3484
3539
  }
@@ -3606,20 +3661,20 @@ var init_parse_phase_parallel = __esm({
3606
3661
  if (!context2.fileCache) context2.fileCache = /* @__PURE__ */ new Map();
3607
3662
  if (!context2.fileFunctionIndex) context2.fileFunctionIndex = /* @__PURE__ */ new Map();
3608
3663
  const filePaths = context2.filePaths;
3609
- const workerCount = parseInt(process.env["PARSE_WORKERS"] ?? "", 10) || Math.max(1, os13.cpus().length - 1);
3664
+ const workerCount = parseInt(process.env["PARSE_WORKERS"] ?? "", 10) || Math.max(1, os18.cpus().length - 1);
3610
3665
  const CONCURRENCY = 64;
3611
3666
  for (let i = 0; i < filePaths.length; i += CONCURRENCY) {
3612
3667
  const batch = filePaths.slice(i, i + CONCURRENCY);
3613
3668
  await Promise.all(batch.map(async (filePath) => {
3614
3669
  try {
3615
- const source = await fs38.promises.readFile(filePath, "utf-8");
3670
+ const source = await fs39.promises.readFile(filePath, "utf-8");
3616
3671
  context2.fileCache.set(filePath, source);
3617
3672
  } catch {
3618
3673
  }
3619
3674
  }));
3620
3675
  }
3621
3676
  const workerScript = workerScriptPath();
3622
- const workerScriptExists = fs38.existsSync(workerScript);
3677
+ const workerScriptExists = fs39.existsSync(workerScript);
3623
3678
  if (!workerScriptExists || workerCount === 1) {
3624
3679
  logger_default.info(`[parse-parallel] falling back to sequential (workerCount=${workerCount}, scriptExists=${workerScriptExists})`);
3625
3680
  const { parsePhase: parsePhase2 } = await Promise.resolve().then(() => (init_parse_phase(), parse_phase_exports));
@@ -3734,8 +3789,8 @@ var init_resolve_phase_parallel = __esm({
3734
3789
  }
3735
3790
  const snapshot = { symbolIndex, fileSymbolIndex, fileIndex, workspaceRoot };
3736
3791
  const workerScript = workerScriptPath2();
3737
- const workerScriptExists = fs38.existsSync(workerScript);
3738
- const workerCount = parseInt(process.env["PARSE_WORKERS"] ?? "", 10) || Math.max(1, os13.cpus().length - 1);
3792
+ const workerScriptExists = fs39.existsSync(workerScript);
3793
+ const workerCount = parseInt(process.env["PARSE_WORKERS"] ?? "", 10) || Math.max(1, os18.cpus().length - 1);
3739
3794
  if (!workerScriptExists || workerCount === 1) {
3740
3795
  logger_default.info(`[resolve-parallel] falling back to sequential`);
3741
3796
  const { resolvePhase: resolvePhase2 } = await Promise.resolve().then(() => (init_resolve_phase(), resolve_phase_exports));
@@ -4071,6 +4126,41 @@ function nodeToDoc(node) {
4071
4126
  (node.content ?? "").slice(0, 1e3)
4072
4127
  ].join(" ");
4073
4128
  }
4129
+ function heapTopK(scores, k) {
4130
+ if (k <= 0) return [];
4131
+ const heap = [];
4132
+ function heapifyUp(i) {
4133
+ while (i > 0) {
4134
+ const parent = i - 1 >> 1;
4135
+ if (heap[parent][1] > heap[i][1]) {
4136
+ [heap[parent], heap[i]] = [heap[i], heap[parent]];
4137
+ i = parent;
4138
+ } else break;
4139
+ }
4140
+ }
4141
+ function heapifyDown(i) {
4142
+ const n = heap.length;
4143
+ while (true) {
4144
+ let smallest = i;
4145
+ const l = 2 * i + 1, r = 2 * i + 2;
4146
+ if (l < n && heap[l][1] < heap[smallest][1]) smallest = l;
4147
+ if (r < n && heap[r][1] < heap[smallest][1]) smallest = r;
4148
+ if (smallest === i) break;
4149
+ [heap[smallest], heap[i]] = [heap[i], heap[smallest]];
4150
+ i = smallest;
4151
+ }
4152
+ }
4153
+ for (const [nodeId, score] of scores) {
4154
+ if (heap.length < k) {
4155
+ heap.push([nodeId, score]);
4156
+ heapifyUp(heap.length - 1);
4157
+ } else if (score > heap[0][1]) {
4158
+ heap[0] = [nodeId, score];
4159
+ heapifyDown(0);
4160
+ }
4161
+ }
4162
+ return heap.sort((a, b) => b[1] - a[1]);
4163
+ }
4074
4164
  function getBm25DbPath(workspaceRoot) {
4075
4165
  return path39.join(workspaceRoot, ".code-intel", "bm25.db");
4076
4166
  }
@@ -4132,10 +4222,10 @@ var init_bm25_index = __esm({
4132
4222
  postings.push({ nodeId, tf: count });
4133
4223
  }
4134
4224
  }
4135
- fs38.mkdirSync(path39.dirname(this.dbPath), { recursive: true });
4225
+ fs39.mkdirSync(path39.dirname(this.dbPath), { recursive: true });
4136
4226
  for (const f of [this.dbPath, `${this.dbPath}-shm`, `${this.dbPath}-wal`]) {
4137
4227
  try {
4138
- if (fs38.existsSync(f)) fs38.unlinkSync(f);
4228
+ if (fs39.existsSync(f)) fs39.unlinkSync(f);
4139
4229
  } catch {
4140
4230
  }
4141
4231
  }
@@ -4173,7 +4263,7 @@ var init_bm25_index = __esm({
4173
4263
  * Called once on `serve` startup.
4174
4264
  */
4175
4265
  load() {
4176
- if (!fs38.existsSync(this.dbPath)) return;
4266
+ if (!fs39.existsSync(this.dbPath)) return;
4177
4267
  const db = new Database2(this.dbPath, { readonly: true });
4178
4268
  try {
4179
4269
  const getMeta = db.prepare("SELECT value FROM bm25_meta WHERE key = ?");
@@ -4207,8 +4297,13 @@ var init_bm25_index = __esm({
4207
4297
  }
4208
4298
  // ── Search ──────────────────────────────────────────────────────────────────
4209
4299
  /**
4210
- * BM25 search. LIMIT pushdown: scores only the posting lists for query terms,
4211
- * then partial-sorts to return only the top `limit` results.
4300
+ * BM25 search.
4301
+ *
4302
+ * Performance strategy:
4303
+ * 1. Skip ultra-high-df terms (df/N > 0.6) — near-zero IDF, dominate posting
4304
+ * lists for common words like "function", "return", "export" in large repos.
4305
+ * 2. Min-heap top-K selection — O(n log k) instead of full O(n log n) sort.
4306
+ * For k=10 and n=30,000 candidates this is ~10× faster than Array.sort.
4212
4307
  */
4213
4308
  search(query, limit) {
4214
4309
  if (!this._loaded || this.invertedIndex.size === 0) return [];
@@ -4221,6 +4316,7 @@ var init_bm25_index = __esm({
4221
4316
  const postings = this.invertedIndex.get(term);
4222
4317
  if (!postings) continue;
4223
4318
  const df = postings.length;
4319
+ if (N > 100 && df / N > 0.6) continue;
4224
4320
  const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);
4225
4321
  for (const { nodeId, tf } of postings) {
4226
4322
  const dl = this.docLengths.get(nodeId) ?? avgdl;
@@ -4228,10 +4324,9 @@ var init_bm25_index = __esm({
4228
4324
  scores.set(nodeId, (scores.get(nodeId) ?? 0) + score);
4229
4325
  }
4230
4326
  }
4231
- const entries = [...scores.entries()];
4232
- entries.sort((a, b) => b[1] - a[1]);
4233
- const topK = entries.slice(0, limit);
4234
- return topK.map(([nodeId, score]) => {
4327
+ if (scores.size === 0) return [];
4328
+ const topEntries = heapTopK(scores, limit);
4329
+ return topEntries.map(([nodeId, score]) => {
4235
4330
  const meta = this.nodeMeta.get(nodeId);
4236
4331
  return {
4237
4332
  nodeId,
@@ -4250,7 +4345,7 @@ var init_bm25_index = __esm({
4250
4345
  * Works even if `load()` was not called (reads affected terms directly from DB).
4251
4346
  */
4252
4347
  updateNodes(nodes) {
4253
- if (!fs38.existsSync(this.dbPath)) return;
4348
+ if (!fs39.existsSync(this.dbPath)) return;
4254
4349
  if (nodes.length === 0) return;
4255
4350
  const changedIds = new Set(nodes.map((n) => n.id));
4256
4351
  const newTermFreqs = /* @__PURE__ */ new Map();
@@ -4325,6 +4420,12 @@ var init_bm25_index = __esm({
4325
4420
  };
4326
4421
  }
4327
4422
  });
4423
+
4424
+ // src/storage/db-manager.ts
4425
+ var db_manager_exports = {};
4426
+ __export(db_manager_exports, {
4427
+ DbManager: () => DbManager
4428
+ });
4328
4429
  var DbManager;
4329
4430
  var init_db_manager = __esm({
4330
4431
  "src/storage/db-manager.ts"() {
@@ -4339,7 +4440,7 @@ var init_db_manager = __esm({
4339
4440
  }
4340
4441
  async init() {
4341
4442
  if (!this.readOnly) {
4342
- fs38.mkdirSync(path39.dirname(this.dbPath), { recursive: true });
4443
+ fs39.mkdirSync(path39.dirname(this.dbPath), { recursive: true });
4343
4444
  }
4344
4445
  this.db = new Database(this.dbPath, 0, true, this.readOnly);
4345
4446
  await this.db.init();
@@ -4379,7 +4480,7 @@ var init_db_manager = __esm({
4379
4480
  }
4380
4481
  });
4381
4482
  function writeNodeCSVs(graph, outputDir) {
4382
- fs38.mkdirSync(outputDir, { recursive: true });
4483
+ fs39.mkdirSync(outputDir, { recursive: true });
4383
4484
  const header = "id,name,file_path,start_line,end_line,exported,content,metadata\n";
4384
4485
  const tableBuffers = /* @__PURE__ */ new Map();
4385
4486
  const tableFilePaths = /* @__PURE__ */ new Map();
@@ -4407,12 +4508,12 @@ function writeNodeCSVs(graph, outputDir) {
4407
4508
  );
4408
4509
  }
4409
4510
  for (const [table, lines] of tableBuffers) {
4410
- fs38.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
4511
+ fs39.writeFileSync(tableFilePaths.get(table), lines.join(""), "utf-8");
4411
4512
  }
4412
4513
  return tableFilePaths;
4413
4514
  }
4414
4515
  function writeEdgeCSV(graph, outputDir) {
4415
- fs38.mkdirSync(outputDir, { recursive: true });
4516
+ fs39.mkdirSync(outputDir, { recursive: true });
4416
4517
  const header = "from_id,to_id,kind,weight,label\n";
4417
4518
  const groups = /* @__PURE__ */ new Map();
4418
4519
  for (const edge of graph.allEdges()) {
@@ -4438,7 +4539,7 @@ function writeEdgeCSV(graph, outputDir) {
4438
4539
  }
4439
4540
  const result = [];
4440
4541
  for (const group of groups.values()) {
4441
- fs38.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
4542
+ fs39.writeFileSync(group.filePath, group.lines.join(""), "utf-8");
4442
4543
  result.push({ fromTable: group.from, toTable: group.to, filePath: group.filePath });
4443
4544
  }
4444
4545
  return result;
@@ -4481,7 +4582,7 @@ async function loadGraphToDB(graph, dbManager) {
4481
4582
  } catch {
4482
4583
  }
4483
4584
  }
4484
- const tmpDir = fs38.mkdtempSync(path39.join(os13.tmpdir(), "code-intel-csv-"));
4585
+ const tmpDir = fs39.mkdtempSync(path39.join(os18.tmpdir(), "code-intel-csv-"));
4485
4586
  try {
4486
4587
  const nodeTableFiles = writeNodeCSVs(graph, tmpDir);
4487
4588
  const edgeGroups = writeEdgeCSV(graph, tmpDir);
@@ -4500,8 +4601,8 @@ async function loadGraphToDB(graph, dbManager) {
4500
4601
  }
4501
4602
  let nodeCount = 0;
4502
4603
  for (const [table, csvPath] of nodeTableFiles) {
4503
- if (!fs38.existsSync(csvPath)) continue;
4504
- const stat = fs38.statSync(csvPath);
4604
+ if (!fs39.existsSync(csvPath)) continue;
4605
+ const stat = fs39.statSync(csvPath);
4505
4606
  if (stat.size < 50) continue;
4506
4607
  try {
4507
4608
  await dbManager.execute(
@@ -4514,8 +4615,8 @@ async function loadGraphToDB(graph, dbManager) {
4514
4615
  }
4515
4616
  let edgeCount = 0;
4516
4617
  for (const group of edgeGroups) {
4517
- if (!fs38.existsSync(group.filePath)) continue;
4518
- const stat = fs38.statSync(group.filePath);
4618
+ if (!fs39.existsSync(group.filePath)) continue;
4619
+ const stat = fs39.statSync(group.filePath);
4519
4620
  if (stat.size < 50) continue;
4520
4621
  try {
4521
4622
  await dbManager.execute(
@@ -4529,7 +4630,7 @@ async function loadGraphToDB(graph, dbManager) {
4529
4630
  return { nodeCount, edgeCount };
4530
4631
  } finally {
4531
4632
  try {
4532
- fs38.rmSync(tmpDir, { recursive: true, force: true });
4633
+ fs39.rmSync(tmpDir, { recursive: true, force: true });
4533
4634
  } catch {
4534
4635
  }
4535
4636
  }
@@ -4640,15 +4741,15 @@ __export(repo_registry_exports, {
4640
4741
  });
4641
4742
  function loadRegistry() {
4642
4743
  try {
4643
- const data = fs38.readFileSync(REPOS_FILE, "utf-8");
4744
+ const data = fs39.readFileSync(REPOS_FILE, "utf-8");
4644
4745
  return JSON.parse(data);
4645
4746
  } catch {
4646
4747
  return [];
4647
4748
  }
4648
4749
  }
4649
4750
  function saveRegistry(entries) {
4650
- fs38.mkdirSync(GLOBAL_DIR, { recursive: true });
4651
- fs38.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
4751
+ fs39.mkdirSync(GLOBAL_DIR, { recursive: true });
4752
+ fs39.writeFileSync(REPOS_FILE, JSON.stringify(entries, null, 2));
4652
4753
  }
4653
4754
  function upsertRepo(entry) {
4654
4755
  const entries = loadRegistry();
@@ -4667,7 +4768,7 @@ function removeRepo(repoPath) {
4667
4768
  var GLOBAL_DIR, REPOS_FILE;
4668
4769
  var init_repo_registry = __esm({
4669
4770
  "src/storage/repo-registry.ts"() {
4670
- GLOBAL_DIR = path39.join(os13.homedir(), ".code-intel");
4771
+ GLOBAL_DIR = path39.join(os18.homedir(), ".code-intel");
4671
4772
  REPOS_FILE = path39.join(GLOBAL_DIR, "repos.json");
4672
4773
  }
4673
4774
  });
@@ -4682,12 +4783,12 @@ __export(metadata_exports, {
4682
4783
  });
4683
4784
  function saveMetadata(repoDir, metadata) {
4684
4785
  const metaDir = path39.join(repoDir, ".code-intel");
4685
- fs38.mkdirSync(metaDir, { recursive: true });
4686
- fs38.writeFileSync(path39.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
4786
+ fs39.mkdirSync(metaDir, { recursive: true });
4787
+ fs39.writeFileSync(path39.join(metaDir, "meta.json"), JSON.stringify(metadata, null, 2));
4687
4788
  }
4688
4789
  function loadMetadata(repoDir) {
4689
4790
  try {
4690
- const data = fs38.readFileSync(path39.join(repoDir, ".code-intel", "meta.json"), "utf-8");
4791
+ const data = fs39.readFileSync(path39.join(repoDir, ".code-intel", "meta.json"), "utf-8");
4691
4792
  return JSON.parse(data);
4692
4793
  } catch {
4693
4794
  return null;
@@ -4757,23 +4858,23 @@ function groupFile(name) {
4757
4858
  }
4758
4859
  function loadGroup(name) {
4759
4860
  try {
4760
- return JSON.parse(fs38.readFileSync(groupFile(name), "utf-8"));
4861
+ return JSON.parse(fs39.readFileSync(groupFile(name), "utf-8"));
4761
4862
  } catch {
4762
4863
  return null;
4763
4864
  }
4764
4865
  }
4765
4866
  function saveGroup(group) {
4766
- fs38.mkdirSync(GROUPS_DIR, { recursive: true });
4767
- fs38.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
4867
+ fs39.mkdirSync(GROUPS_DIR, { recursive: true });
4868
+ fs39.writeFileSync(groupFile(group.name), JSON.stringify(group, null, 2) + "\n");
4768
4869
  }
4769
4870
  function listGroups() {
4770
4871
  const groups = [];
4771
4872
  try {
4772
- for (const file of fs38.readdirSync(GROUPS_DIR)) {
4873
+ for (const file of fs39.readdirSync(GROUPS_DIR)) {
4773
4874
  if (!file.endsWith(".json") || file.endsWith(".sync.json")) continue;
4774
4875
  try {
4775
4876
  const g = JSON.parse(
4776
- fs38.readFileSync(path39.join(GROUPS_DIR, file), "utf-8")
4877
+ fs39.readFileSync(path39.join(GROUPS_DIR, file), "utf-8")
4777
4878
  );
4778
4879
  groups.push(g);
4779
4880
  } catch {
@@ -4785,16 +4886,16 @@ function listGroups() {
4785
4886
  }
4786
4887
  function deleteGroup(name) {
4787
4888
  try {
4788
- fs38.unlinkSync(groupFile(name));
4889
+ fs39.unlinkSync(groupFile(name));
4789
4890
  } catch {
4790
4891
  }
4791
4892
  try {
4792
- fs38.unlinkSync(path39.join(GROUPS_DIR, `${name}.sync.json`));
4893
+ fs39.unlinkSync(path39.join(GROUPS_DIR, `${name}.sync.json`));
4793
4894
  } catch {
4794
4895
  }
4795
4896
  }
4796
4897
  function groupExists(name) {
4797
- return fs38.existsSync(groupFile(name));
4898
+ return fs39.existsSync(groupFile(name));
4798
4899
  }
4799
4900
  function addMember(groupName, member) {
4800
4901
  const group = loadGroup(groupName);
@@ -4820,8 +4921,8 @@ function removeMember(groupName, groupPath) {
4820
4921
  return group;
4821
4922
  }
4822
4923
  function saveSyncResult(result) {
4823
- fs38.mkdirSync(GROUPS_DIR, { recursive: true });
4824
- fs38.writeFileSync(
4924
+ fs39.mkdirSync(GROUPS_DIR, { recursive: true });
4925
+ fs39.writeFileSync(
4825
4926
  path39.join(GROUPS_DIR, `${result.groupName}.sync.json`),
4826
4927
  JSON.stringify(result, null, 2) + "\n"
4827
4928
  );
@@ -4829,7 +4930,7 @@ function saveSyncResult(result) {
4829
4930
  function loadSyncResult(groupName) {
4830
4931
  try {
4831
4932
  return JSON.parse(
4832
- fs38.readFileSync(path39.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
4933
+ fs39.readFileSync(path39.join(GROUPS_DIR, `${groupName}.sync.json`), "utf-8")
4833
4934
  );
4834
4935
  } catch {
4835
4936
  return null;
@@ -4838,11 +4939,15 @@ function loadSyncResult(groupName) {
4838
4939
  var GROUPS_DIR;
4839
4940
  var init_group_registry = __esm({
4840
4941
  "src/multi-repo/group-registry.ts"() {
4841
- GROUPS_DIR = path39.join(os13.homedir(), ".code-intel", "groups");
4942
+ GROUPS_DIR = path39.join(os18.homedir(), ".code-intel", "groups");
4842
4943
  }
4843
4944
  });
4844
4945
 
4845
4946
  // src/multi-repo/graph-from-db.ts
4947
+ var graph_from_db_exports = {};
4948
+ __export(graph_from_db_exports, {
4949
+ loadGraphFromDB: () => loadGraphFromDB
4950
+ });
4846
4951
  function parseRow(row, kind) {
4847
4952
  return {
4848
4953
  id: String(row["id"] ?? ""),
@@ -4923,7 +5028,7 @@ function scanForFiles(root, matcher, maxDepth = 2) {
4923
5028
  if (depth > maxDepth) return;
4924
5029
  let entries;
4925
5030
  try {
4926
- entries = fs38.readdirSync(dir, { withFileTypes: true });
5031
+ entries = fs39.readdirSync(dir, { withFileTypes: true });
4927
5032
  } catch {
4928
5033
  return;
4929
5034
  }
@@ -4945,7 +5050,7 @@ var init_file_scanner = __esm({
4945
5050
  });
4946
5051
  function tryParseFile(filePath) {
4947
5052
  const ext = path39.extname(filePath).toLowerCase();
4948
- const content = fs38.readFileSync(filePath, "utf-8");
5053
+ const content = fs39.readFileSync(filePath, "utf-8");
4949
5054
  if (ext === ".json") {
4950
5055
  try {
4951
5056
  return JSON.parse(content);
@@ -5014,7 +5119,7 @@ async function parseGraphQLContracts(repoRoot) {
5014
5119
  const files = scanForFiles(repoRoot, (name) => name.endsWith(".graphql") || name.endsWith(".gql"));
5015
5120
  const contracts = [];
5016
5121
  for (const filePath of files) {
5017
- const content = fs38.readFileSync(filePath, "utf-8");
5122
+ const content = fs39.readFileSync(filePath, "utf-8");
5018
5123
  const typeRegex = /type\s+(\w+)\s*\{([^}]+)\}/g;
5019
5124
  let match;
5020
5125
  while ((match = typeRegex.exec(content)) !== null) {
@@ -5050,7 +5155,7 @@ async function parseProtoContracts(repoRoot) {
5050
5155
  const files = scanForFiles(repoRoot, (name) => name.endsWith(".proto"));
5051
5156
  const contracts = [];
5052
5157
  for (const filePath of files) {
5053
- const content = fs38.readFileSync(filePath, "utf-8");
5158
+ const content = fs39.readFileSync(filePath, "utf-8");
5054
5159
  const serviceRegex = /service\s+(\w+)\s*\{([^}]+)\}/g;
5055
5160
  let serviceMatch;
5056
5161
  while ((serviceMatch = serviceRegex.exec(content)) !== null) {
@@ -5269,7 +5374,7 @@ async function syncGroup(group) {
5269
5374
  continue;
5270
5375
  }
5271
5376
  const dbPath = path39.join(regEntry.path, ".code-intel", "graph.db");
5272
- if (!fs38.existsSync(dbPath)) {
5377
+ if (!fs39.existsSync(dbPath)) {
5273
5378
  logger_default.warn(` \u26A0 No index at ${dbPath} \u2014 run \`code-intel analyze ${regEntry.path}\` first`);
5274
5379
  continue;
5275
5380
  }
@@ -5393,14 +5498,14 @@ function runPrerequisiteChecks() {
5393
5498
  });
5394
5499
  }
5395
5500
  try {
5396
- const out = execSync(`df -BM "${os13.homedir()}" 2>/dev/null | tail -1 | awk '{print $4}'`, { encoding: "utf8" });
5501
+ const out = execSync(`df -BM "${os18.homedir()}" 2>/dev/null | tail -1 | awk '{print $4}'`, { encoding: "utf8" });
5397
5502
  const availMB = parseInt(out.trim().replace("M", ""), 10);
5398
5503
  if (Number.isFinite(availMB) && availMB < 500) {
5399
5504
  results.push({
5400
5505
  name: "Disk space",
5401
5506
  ok: false,
5402
5507
  level: "warn",
5403
- message: `Low disk space: ${availMB} MB available in ${os13.homedir()} (500 MB recommended)`
5508
+ message: `Low disk space: ${availMB} MB available in ${os18.homedir()} (500 MB recommended)`
5404
5509
  });
5405
5510
  }
5406
5511
  } catch {
@@ -5421,6 +5526,7 @@ var init_codes = __esm({
5421
5526
  RATE_LIMIT_EXCEEDED: "CI-1100",
5422
5527
  PAYLOAD_TOO_LARGE: "CI-1101",
5423
5528
  INVALID_REQUEST: "CI-1200",
5529
+ CONFLICT: "CI-1409",
5424
5530
  // Config (CI-2xxx)
5425
5531
  CONFIG_INVALID: "CI-2000",
5426
5532
  CONFIG_NOT_FOUND: "CI-2001",
@@ -5450,10 +5556,10 @@ var init_codes = __esm({
5450
5556
  }
5451
5557
  });
5452
5558
  function secureMkdir(dir) {
5453
- fs38.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
5559
+ fs39.mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
5454
5560
  if (process.platform !== "win32") {
5455
5561
  try {
5456
- fs38.chmodSync(dir, SECURE_DIR_MODE);
5562
+ fs39.chmodSync(dir, SECURE_DIR_MODE);
5457
5563
  } catch {
5458
5564
  }
5459
5565
  }
@@ -5461,22 +5567,22 @@ function secureMkdir(dir) {
5461
5567
  function secureChmodFile(file) {
5462
5568
  if (process.platform === "win32") return;
5463
5569
  try {
5464
- fs38.chmodSync(file, SECURE_FILE_MODE);
5570
+ fs39.chmodSync(file, SECURE_FILE_MODE);
5465
5571
  } catch {
5466
5572
  }
5467
5573
  }
5468
5574
  function secureWriteFile(file, data) {
5469
5575
  secureMkdir(path39.dirname(file));
5470
- fs38.writeFileSync(file, data, { mode: SECURE_FILE_MODE });
5576
+ fs39.writeFileSync(file, data, { mode: SECURE_FILE_MODE });
5471
5577
  secureChmodFile(file);
5472
5578
  }
5473
5579
  function tightenDbFiles(dir) {
5474
5580
  if (process.platform === "win32") return;
5475
- if (!fs38.existsSync(dir)) return;
5476
- for (const name of fs38.readdirSync(dir)) {
5581
+ if (!fs39.existsSync(dir)) return;
5582
+ for (const name of fs39.readdirSync(dir)) {
5477
5583
  if (name.endsWith(".db") || name.endsWith(".db-wal") || name.endsWith(".db-shm")) {
5478
5584
  try {
5479
- fs38.chmodSync(path39.join(dir, name), SECURE_FILE_MODE);
5585
+ fs39.chmodSync(path39.join(dir, name), SECURE_FILE_MODE);
5480
5586
  } catch {
5481
5587
  }
5482
5588
  }
@@ -5490,7 +5596,7 @@ var init_fs_secure = __esm({
5490
5596
  }
5491
5597
  });
5492
5598
  function getUsersDBPath() {
5493
- return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path39.join(os13.homedir(), ".code-intel", "users.db");
5599
+ return process.env["CODE_INTEL_USERS_DB_PATH"] ?? path39.join(os18.homedir(), ".code-intel", "users.db");
5494
5600
  }
5495
5601
  function getOrCreateUsersDB() {
5496
5602
  if (!_usersDB) {
@@ -5783,17 +5889,17 @@ function getScryptN() {
5783
5889
  return Number.isInteger(v) && v >= 1024 ? v : 1 << 14;
5784
5890
  }
5785
5891
  function getSecretsPath() {
5786
- return process.env["CODE_INTEL_SECRETS_PATH"] ?? path39.join(os13.homedir(), ".code-intel", ".secrets");
5892
+ return process.env["CODE_INTEL_SECRETS_PATH"] ?? path39.join(os18.homedir(), ".code-intel", ".secrets");
5787
5893
  }
5788
5894
  function getMasterPassword() {
5789
5895
  const fromEnv = process.env["CODE_INTEL_SECRET_KEY"];
5790
5896
  if (fromEnv && fromEnv.length >= 16) return fromEnv;
5791
5897
  let username = "unknown";
5792
5898
  try {
5793
- username = os13.userInfo().username;
5899
+ username = os18.userInfo().username;
5794
5900
  } catch {
5795
5901
  }
5796
- return `code-intel-machine:${os13.hostname()}:${username}:${os13.platform()}`;
5902
+ return `code-intel-machine:${os18.hostname()}:${username}:${os18.platform()}`;
5797
5903
  }
5798
5904
  function deriveKey(password, salt) {
5799
5905
  return crypto5.scryptSync(password, salt, KEY_LEN, { N: getScryptN(), r: 8, p: 1 });
@@ -5825,8 +5931,8 @@ function decryptSecrets(encrypted) {
5825
5931
  return JSON.parse(plaintext.toString("utf8"));
5826
5932
  }
5827
5933
  function loadSecrets(secretsPath = getSecretsPath()) {
5828
- if (!fs38.existsSync(secretsPath)) return {};
5829
- const blob = fs38.readFileSync(secretsPath);
5934
+ if (!fs39.existsSync(secretsPath)) return {};
5935
+ const blob = fs39.readFileSync(secretsPath);
5830
5936
  return decryptSecrets(blob);
5831
5937
  }
5832
5938
  function saveSecrets(blob, secretsPath = getSecretsPath()) {
@@ -5866,11 +5972,18 @@ function getSessionTtlMs() {
5866
5972
  const hours = parseInt(process.env["CODE_INTEL_SESSION_TTL_HOURS"] ?? "8", 10);
5867
5973
  return (isNaN(hours) ? 8 : hours) * 60 * 60 * 1e3;
5868
5974
  }
5869
- function createSession(user) {
5975
+ function createSession(user, rememberMe = false) {
5870
5976
  const sessionId = v4();
5871
- const expiresAt = Date.now() + getSessionTtlMs();
5872
- sessionStore.set(sessionId, { userId: user.id, username: user.username, role: user.role, expiresAt });
5873
- return sessionId;
5977
+ const ttlMs = rememberMe ? REMEMBER_ME_TTL_MS : getSessionTtlMs();
5978
+ const expiresAt = Date.now() + ttlMs;
5979
+ sessionStore.set(sessionId, {
5980
+ userId: user.id,
5981
+ username: user.username,
5982
+ role: user.role,
5983
+ expiresAt,
5984
+ ttlMs
5985
+ });
5986
+ return { sessionId, ttlMs };
5874
5987
  }
5875
5988
  function getSession(sessionId) {
5876
5989
  const entry = sessionStore.get(sessionId);
@@ -5879,10 +5992,9 @@ function getSession(sessionId) {
5879
5992
  sessionStore.delete(sessionId);
5880
5993
  return null;
5881
5994
  }
5882
- const ttlMs = getSessionTtlMs();
5883
5995
  const remaining = entry.expiresAt - Date.now();
5884
- if (remaining < ttlMs * 0.75) {
5885
- entry.expiresAt = Date.now() + ttlMs;
5996
+ if (remaining < entry.ttlMs * 0.75) {
5997
+ entry.expiresAt = Date.now() + entry.ttlMs;
5886
5998
  }
5887
5999
  return entry;
5888
6000
  }
@@ -5902,7 +6014,7 @@ function authMiddleware(req, res, next) {
5902
6014
  const session = getSession(sessionId);
5903
6015
  if (session) {
5904
6016
  req.user = { id: session.userId, username: session.username, role: session.role, authMethod: "session" };
5905
- res.setHeader("Set-Cookie", buildSessionCookie(sessionId));
6017
+ res.setHeader("Set-Cookie", buildSessionCookie(sessionId, session.ttlMs));
5906
6018
  next();
5907
6019
  return;
5908
6020
  }
@@ -6072,9 +6184,9 @@ function parseCookies(cookieHeader) {
6072
6184
  }
6073
6185
  return result;
6074
6186
  }
6075
- function buildSessionCookie(sessionId) {
6187
+ function buildSessionCookie(sessionId, ttlMs) {
6076
6188
  const isProduction = process.env["NODE_ENV"] === "production";
6077
- const maxAge = Math.floor(getSessionTtlMs() / 1e3);
6189
+ const maxAge = Math.floor((ttlMs ?? getSessionTtlMs()) / 1e3);
6078
6190
  const parts = [
6079
6191
  `${SESSION_COOKIE_NAME}=${encodeURIComponent(sessionId)}`,
6080
6192
  `HttpOnly`,
@@ -6091,7 +6203,7 @@ function clearSessionCookie() {
6091
6203
  async function verifyPassword(plain, hash) {
6092
6204
  return bcrypt.compare(plain, hash);
6093
6205
  }
6094
- var sessionStore, SESSION_COOKIE_NAME, ROLE_RANK;
6206
+ var sessionStore, SESSION_COOKIE_NAME, REMEMBER_ME_TTL_MS, ROLE_RANK;
6095
6207
  var init_middleware = __esm({
6096
6208
  "src/auth/middleware.ts"() {
6097
6209
  init_users_db();
@@ -6099,6 +6211,7 @@ var init_middleware = __esm({
6099
6211
  init_secret_store();
6100
6212
  sessionStore = /* @__PURE__ */ new Map();
6101
6213
  SESSION_COOKIE_NAME = "code_intel_session";
6214
+ REMEMBER_ME_TTL_MS = 12 * 60 * 60 * 1e3;
6102
6215
  ROLE_RANK = {
6103
6216
  viewer: 1,
6104
6217
  "repo-owner": 2,
@@ -7871,7 +7984,7 @@ var init_secret_scanner = __esm({
7871
7984
  const ignorePatterns = [...options?.ignorePatterns ?? []];
7872
7985
  if (options?.workspaceRoot) {
7873
7986
  try {
7874
- const raw = fs38.readFileSync(path39.join(options.workspaceRoot, ".codeintelignore"), "utf-8");
7987
+ const raw = fs39.readFileSync(path39.join(options.workspaceRoot, ".codeintelignore"), "utf-8");
7875
7988
  for (const line of raw.split("\n")) {
7876
7989
  const trimmed = line.trim();
7877
7990
  if (trimmed && !trimmed.startsWith("#")) ignorePatterns.push(trimmed);
@@ -8129,22 +8242,22 @@ __export(init_wizard_exports, {
8129
8242
  wipeConfig: () => wipeConfig
8130
8243
  });
8131
8244
  function configExists() {
8132
- return fs38.existsSync(CONFIG_PATH);
8245
+ return fs39.existsSync(CONFIG_PATH);
8133
8246
  }
8134
8247
  function loadConfig() {
8135
8248
  if (!configExists()) return null;
8136
8249
  try {
8137
- return JSON.parse(fs38.readFileSync(CONFIG_PATH, "utf-8"));
8250
+ return JSON.parse(fs39.readFileSync(CONFIG_PATH, "utf-8"));
8138
8251
  } catch {
8139
8252
  return null;
8140
8253
  }
8141
8254
  }
8142
8255
  function saveConfig(cfg) {
8143
- fs38.mkdirSync(GLOBAL_DIR2, { recursive: true });
8144
- fs38.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
8256
+ fs39.mkdirSync(GLOBAL_DIR2, { recursive: true });
8257
+ fs39.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
8145
8258
  }
8146
8259
  function wipeConfig() {
8147
- if (fs38.existsSync(CONFIG_PATH)) fs38.unlinkSync(CONFIG_PATH);
8260
+ if (fs39.existsSync(CONFIG_PATH)) fs39.unlinkSync(CONFIG_PATH);
8148
8261
  }
8149
8262
  function commandExists(bin) {
8150
8263
  try {
@@ -8224,8 +8337,8 @@ async function runInitWizard(opts = {}) {
8224
8337
  const mcpFile = path39.resolve(editor.mcpConfigKey);
8225
8338
  try {
8226
8339
  let existing = {};
8227
- if (fs38.existsSync(mcpFile)) {
8228
- existing = JSON.parse(fs38.readFileSync(mcpFile, "utf-8"));
8340
+ if (fs39.existsSync(mcpFile)) {
8341
+ existing = JSON.parse(fs39.readFileSync(mcpFile, "utf-8"));
8229
8342
  }
8230
8343
  const merged = {
8231
8344
  ...existing,
@@ -8234,8 +8347,8 @@ async function runInitWizard(opts = {}) {
8234
8347
  ...mcpConfig.servers
8235
8348
  }
8236
8349
  };
8237
- fs38.mkdirSync(path39.dirname(mcpFile), { recursive: true });
8238
- fs38.writeFileSync(mcpFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
8350
+ fs39.mkdirSync(path39.dirname(mcpFile), { recursive: true });
8351
+ fs39.writeFileSync(mcpFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
8239
8352
  console.log(` \u2705 MCP registered for ${name} \u2192 ${editor.mcpConfigKey}`);
8240
8353
  } catch {
8241
8354
  console.log(` \u26A0 Could not write MCP config for ${name} \u2014 do it manually.`);
@@ -8252,6 +8365,7 @@ async function runInitWizard(opts = {}) {
8252
8365
  { label: "Ollama (local, free, requires Ollama running)", value: "ollama" },
8253
8366
  { label: "OpenAI (requires OPENAI_API_KEY env var)", value: "openai" },
8254
8367
  { label: "Anthropic (requires ANTHROPIC_API_KEY env var)", value: "anthropic" },
8368
+ { label: "Custom (OpenAI-compatible API \u2014 enter URL, token & model)", value: "custom" },
8255
8369
  { label: "Skip (configure later)", value: "none" }
8256
8370
  ], 0);
8257
8371
  cfg.llm.provider = llmProvider;
@@ -8267,6 +8381,19 @@ async function runInitWizard(opts = {}) {
8267
8381
  cfg.llm.model = "llama3";
8268
8382
  cfg.llm.apiKey = "";
8269
8383
  console.log(" Make sure Ollama is running: https://ollama.com");
8384
+ } else if (llmProvider === "custom") {
8385
+ console.log(" Configure your OpenAI-compatible provider (e.g. LM Studio, vLLM, Together, Groq, Azure).\n");
8386
+ const baseUrl = (await prompt(rl, " API Base URL (e.g. http://localhost:1234/v1): ")).trim();
8387
+ cfg.llm.baseUrl = baseUrl || "http://localhost:1234/v1";
8388
+ const apiKey = (await prompt(rl, " API Token/Key (leave blank if not required): ")).trim();
8389
+ cfg.llm.apiKey = apiKey || "";
8390
+ const model = (await prompt(rl, " Model name (e.g. mistral-7b-instruct): ")).trim();
8391
+ cfg.llm.model = model || "default";
8392
+ console.log(`
8393
+ \u2705 Custom provider configured:`);
8394
+ console.log(` URL: ${cfg.llm.baseUrl}`);
8395
+ console.log(` Model: ${cfg.llm.model}`);
8396
+ console.log(` Token: ${cfg.llm.apiKey ? "(set)" : "(none)"}`);
8270
8397
  } else {
8271
8398
  cfg.llm.apiKey = "";
8272
8399
  console.log(" Skipped. Run `code-intel config set llm.provider openai` later.");
@@ -8331,7 +8458,7 @@ function printNextSteps() {
8331
8458
  var GLOBAL_DIR2, CONFIG_PATH, DEFAULT_CONFIG, EDITORS;
8332
8459
  var init_init_wizard = __esm({
8333
8460
  "src/cli/init-wizard.ts"() {
8334
- GLOBAL_DIR2 = path39.join(os13.homedir(), ".code-intel");
8461
+ GLOBAL_DIR2 = path39.join(os18.homedir(), ".code-intel");
8335
8462
  CONFIG_PATH = path39.join(GLOBAL_DIR2, "config.json");
8336
8463
  DEFAULT_CONFIG = {
8337
8464
  $schema: "https://code-intel.dev/config-schema.json",
@@ -8644,9 +8771,10 @@ var init_config_manager = __esm({
8644
8771
  /access[_-]?token/i
8645
8772
  ];
8646
8773
  CONFIG_SCHEMA = {
8647
- "llm.provider": { type: "string", enum: ["openai", "anthropic", "ollama", "none"], default: "ollama", description: "LLM provider for AI summaries" },
8774
+ "llm.provider": { type: "string", enum: ["openai", "anthropic", "ollama", "custom", "none"], default: "ollama", description: "LLM provider for AI summaries" },
8648
8775
  "llm.model": { type: "string", default: "llama3", description: "LLM model name" },
8649
8776
  "llm.apiKey": { type: "string", default: "", description: "API key \u2014 use $ENV_VAR syntax, e.g. $OPENAI_API_KEY" },
8777
+ "llm.baseUrl": { type: "string", default: "", description: "Base URL for custom OpenAI-compatible API (e.g. http://localhost:1234/v1)" },
8650
8778
  "llm.batchSize": { type: "number", minimum: 1, maximum: 100, default: 20, description: "Concurrent LLM calls per batch" },
8651
8779
  "llm.maxTokensPerSummary": { type: "number", minimum: 10, maximum: 2e3, default: 100, description: "Max tokens per AI summary" },
8652
8780
  "embeddings.model": { type: "string", default: "all-MiniLM-L6-v2", description: "Embedding model name" },
@@ -9005,8 +9133,8 @@ var init_file_watcher = __esm({
9005
9133
  readCodeIntelIgnore() {
9006
9134
  const ignoreFile = path39.join(this.workspaceRoot, ".codeintelignore");
9007
9135
  try {
9008
- if (!fs38.existsSync(ignoreFile)) return [];
9009
- return fs38.readFileSync(ignoreFile, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
9136
+ if (!fs39.existsSync(ignoreFile)) return [];
9137
+ return fs39.readFileSync(ignoreFile, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
9010
9138
  } catch {
9011
9139
  return [];
9012
9140
  }
@@ -9067,7 +9195,7 @@ var init_incremental_indexer = __esm({
9067
9195
  graph.removeNodeCascade(id);
9068
9196
  nodesRemoved++;
9069
9197
  }
9070
- if (fs38.existsSync(dbPath)) {
9198
+ if (fs39.existsSync(dbPath)) {
9071
9199
  try {
9072
9200
  const db = new DbManager(dbPath);
9073
9201
  await db.init();
@@ -9082,7 +9210,7 @@ var init_incremental_indexer = __esm({
9082
9210
  }
9083
9211
  const existingFiles = changedFiles.filter((f) => {
9084
9212
  try {
9085
- return fs38.statSync(f).isFile();
9213
+ return fs39.statSync(f).isFile();
9086
9214
  } catch {
9087
9215
  return false;
9088
9216
  }
@@ -9104,7 +9232,7 @@ var init_incremental_indexer = __esm({
9104
9232
  await runPipeline([noopScan, parsePhase, resolvePhase], context2);
9105
9233
  }
9106
9234
  const nodesAdded = Math.max(0, graph.size.nodes - (nodesBeforeParse - nodesRemoved));
9107
- if (fs38.existsSync(dbPath) && existingFiles.length > 0) {
9235
+ if (fs39.existsSync(dbPath) && existingFiles.length > 0) {
9108
9236
  try {
9109
9237
  const db = new DbManager(dbPath);
9110
9238
  await db.init();
@@ -9116,7 +9244,7 @@ var init_incremental_indexer = __esm({
9116
9244
  db.close();
9117
9245
  try {
9118
9246
  const bm25DbPath = getBm25DbPath(workspaceRoot);
9119
- if (fs38.existsSync(bm25DbPath)) {
9247
+ if (fs39.existsSync(bm25DbPath)) {
9120
9248
  const bm25 = new Bm25Index(bm25DbPath);
9121
9249
  bm25.updateNodes(nodesToUpsert);
9122
9250
  logger_default.info(`[incremental] BM25 index updated: ${nodesToUpsert.length} nodes`);
@@ -9155,31 +9283,31 @@ function getQueriesDir(workspaceRoot) {
9155
9283
  }
9156
9284
  function ensureQueriesDir(workspaceRoot) {
9157
9285
  const dir = getQueriesDir(workspaceRoot);
9158
- if (!fs38.existsSync(dir)) {
9159
- fs38.mkdirSync(dir, { recursive: true });
9286
+ if (!fs39.existsSync(dir)) {
9287
+ fs39.mkdirSync(dir, { recursive: true });
9160
9288
  }
9161
9289
  return dir;
9162
9290
  }
9163
9291
  function saveQuery(workspaceRoot, name, gql) {
9164
9292
  const dir = ensureQueriesDir(workspaceRoot);
9165
9293
  const filePath = path39.join(dir, `${name}.gql`);
9166
- fs38.writeFileSync(filePath, gql, "utf-8");
9294
+ fs39.writeFileSync(filePath, gql, "utf-8");
9167
9295
  }
9168
9296
  function loadQuery(workspaceRoot, name) {
9169
9297
  const dir = getQueriesDir(workspaceRoot);
9170
9298
  const filePath = path39.join(dir, `${name}.gql`);
9171
- if (!fs38.existsSync(filePath)) return null;
9172
- return fs38.readFileSync(filePath, "utf-8");
9299
+ if (!fs39.existsSync(filePath)) return null;
9300
+ return fs39.readFileSync(filePath, "utf-8");
9173
9301
  }
9174
9302
  function listQueries(workspaceRoot) {
9175
9303
  const dir = getQueriesDir(workspaceRoot);
9176
- if (!fs38.existsSync(dir)) return [];
9177
- const files = fs38.readdirSync(dir).filter((f) => f.endsWith(".gql"));
9304
+ if (!fs39.existsSync(dir)) return [];
9305
+ const files = fs39.readdirSync(dir).filter((f) => f.endsWith(".gql"));
9178
9306
  return files.map((f) => {
9179
9307
  const filePath = path39.join(dir, f);
9180
9308
  const name = f.replace(/\.gql$/, "");
9181
- const content = fs38.readFileSync(filePath, "utf-8");
9182
- const stat = fs38.statSync(filePath);
9309
+ const content = fs39.readFileSync(filePath, "utf-8");
9310
+ const stat = fs39.statSync(filePath);
9183
9311
  return {
9184
9312
  name,
9185
9313
  content,
@@ -9191,14 +9319,14 @@ function listQueries(workspaceRoot) {
9191
9319
  function deleteQuery(workspaceRoot, name) {
9192
9320
  const dir = getQueriesDir(workspaceRoot);
9193
9321
  const filePath = path39.join(dir, `${name}.gql`);
9194
- if (!fs38.existsSync(filePath)) return false;
9195
- fs38.unlinkSync(filePath);
9322
+ if (!fs39.existsSync(filePath)) return false;
9323
+ fs39.unlinkSync(filePath);
9196
9324
  return true;
9197
9325
  }
9198
9326
  function queryExists(workspaceRoot, name) {
9199
9327
  const dir = getQueriesDir(workspaceRoot);
9200
9328
  const filePath = path39.join(dir, `${name}.gql`);
9201
- return fs38.existsSync(filePath);
9329
+ return fs39.existsSync(filePath);
9202
9330
  }
9203
9331
  var init_saved_queries = __esm({
9204
9332
  "src/query/saved-queries.ts"() {
@@ -9266,6 +9394,324 @@ var init_sarif_builder = __esm({
9266
9394
  }
9267
9395
  });
9268
9396
 
9397
+ // src/context/token-counter.ts
9398
+ var token_counter_exports = {};
9399
+ __export(token_counter_exports, {
9400
+ estimateTokens: () => estimateTokens,
9401
+ measureBlocks: () => measureBlocks
9402
+ });
9403
+ function estimateTokens(text) {
9404
+ if (!text) return 0;
9405
+ const words = text.split(/\s+/).filter(Boolean).length;
9406
+ const chars = text.length;
9407
+ return Math.ceil((words * 1.3 + chars * 0.25) / 2);
9408
+ }
9409
+ function measureBlocks(doc) {
9410
+ const summary = estimateTokens(doc.summary);
9411
+ const logic = estimateTokens(doc.logic);
9412
+ const relation = estimateTokens(doc.relation);
9413
+ const focusCode = estimateTokens(doc.focusCode);
9414
+ return { summary, logic, relation, focusCode, total: summary + logic + relation + focusCode };
9415
+ }
9416
+ var init_token_counter = __esm({
9417
+ "src/context/token-counter.ts"() {
9418
+ }
9419
+ });
9420
+
9421
+ // src/context/builder.ts
9422
+ var builder_exports = {};
9423
+ __export(builder_exports, {
9424
+ build: () => build,
9425
+ detectQueryIntent: () => detectQueryIntent
9426
+ });
9427
+ function detectQueryIntent(question) {
9428
+ const q = question.toLowerCase();
9429
+ if (/\b(show|code|implement|source|how is written|function body|method body)\b/.test(q)) return "code";
9430
+ if (/\b(who calls|callers?|depends on|blast radius|impact|upstream)\b/.test(q)) return "callers";
9431
+ if (/\b(architecture|overview|structure|design|how is built|system)\b/.test(q)) return "architecture";
9432
+ return "auto";
9433
+ }
9434
+ function last2Segments(filePath) {
9435
+ const parts = filePath.replace(/\\/g, "/").split("/").filter(Boolean);
9436
+ return parts.slice(-2).join("/");
9437
+ }
9438
+ function firstSentence(text) {
9439
+ if (!text) return "";
9440
+ const sentence = text.split(/[.!?]/)[0]?.trim() ?? "";
9441
+ const words = sentence.split(/\s+/);
9442
+ return words.slice(0, 15).join(" ");
9443
+ }
9444
+ function getCluster(graph, nodeId) {
9445
+ for (const edge of graph.findEdgesFrom(nodeId)) {
9446
+ if (edge.kind === "belongs_to") return graph.getNode(edge.target)?.name;
9447
+ }
9448
+ return void 0;
9449
+ }
9450
+ function dirOf(filePath) {
9451
+ const parts = filePath.replace(/\\/g, "/").split("/").filter(Boolean);
9452
+ return parts.slice(0, -1).join("/") || ".";
9453
+ }
9454
+ function meaningfulLines(content) {
9455
+ return content.split("\n").filter((l) => {
9456
+ const t = l.trim();
9457
+ return t.length > 0 && !t.startsWith("//") && !t.startsWith("*") && !t.startsWith("#");
9458
+ });
9459
+ }
9460
+ function adaptiveSnippet(content) {
9461
+ if (!content) return { lines: "", truncated: false };
9462
+ const stripped = content.replace(/^\n+|\n+$/g, "");
9463
+ const rawLines = stripped.split("\n");
9464
+ const ml = meaningfulLines(stripped).length;
9465
+ if (ml <= 10) return { lines: stripped, truncated: false };
9466
+ if (ml <= 25) {
9467
+ const out2 = rawLines.slice(0, 25);
9468
+ const truncated = rawLines.length > 25;
9469
+ return { lines: out2.join("\n") + (truncated ? "\n// ..." : ""), truncated };
9470
+ }
9471
+ const out = rawLines.slice(0, 40);
9472
+ const remaining = rawLines.length - 40;
9473
+ return {
9474
+ lines: out.join("\n") + (remaining > 0 ? `
9475
+ // ... (${remaining} more lines)` : ""),
9476
+ truncated: remaining > 0
9477
+ };
9478
+ }
9479
+ function buildSummaryBlock(nodes, graph, dedup) {
9480
+ if (nodes.length === 0) return "";
9481
+ const byDir = /* @__PURE__ */ new Map();
9482
+ for (const node of nodes) {
9483
+ const dir = dirOf(node.filePath);
9484
+ if (!byDir.has(dir)) byDir.set(dir, []);
9485
+ byDir.get(dir).push(node);
9486
+ }
9487
+ const lines = ["[SUMMARY]"];
9488
+ for (const [dir, group] of byDir) {
9489
+ const useHeader = group.length >= 3;
9490
+ if (useHeader) lines.push(`${dir}/:`);
9491
+ for (const node of group) {
9492
+ const summary = firstSentence(node.metadata?.["summary"]);
9493
+ const callerCount = [...graph.findEdgesTo(node.id)].filter((e) => e.kind === "calls").length;
9494
+ getCluster(graph, node.id);
9495
+ const badges = [];
9496
+ if (callerCount >= 10) badges.push("\u26A0");
9497
+ if (callerCount === 0) badges.push("\u{1F47B}");
9498
+ const badgeStr = badges.join("");
9499
+ const path210 = last2Segments(node.filePath);
9500
+ const line = node.startLine ? `:${node.startLine}` : "";
9501
+ const fullFmt = `${node.name} [${node.kind}] ${path210}${line}${badgeStr ? " " + badgeStr : ""}${summary ? " \u2014 " + summary : ""}`;
9502
+ const formatted = dedup.formatSymbol(node.name, node.filePath, fullFmt);
9503
+ lines.push(useHeader ? ` ${formatted}` : formatted);
9504
+ }
9505
+ }
9506
+ return lines.join("\n");
9507
+ }
9508
+ function buildLogicBlock(nodes, graph, dedup) {
9509
+ if (nodes.length === 0) return "";
9510
+ const lines = ["[LOGIC]"];
9511
+ const nodeCallees = /* @__PURE__ */ new Map();
9512
+ const calleeUsage = /* @__PURE__ */ new Map();
9513
+ for (const node of nodes) {
9514
+ const callees = [];
9515
+ for (const edge of graph.findEdgesFrom(node.id)) {
9516
+ if (edge.kind === "calls") {
9517
+ const callee = graph.getNode(edge.target);
9518
+ if (callee && callee.name !== node.name) {
9519
+ callees.push(callee.name);
9520
+ calleeUsage.set(callee.name, (calleeUsage.get(callee.name) ?? 0) + 1);
9521
+ }
9522
+ }
9523
+ }
9524
+ nodeCallees.set(node.id, [...new Set(callees)]);
9525
+ }
9526
+ const sharedCallees = new Set(
9527
+ [...calleeUsage.entries()].filter(([, cnt]) => cnt >= 3).map(([name]) => name)
9528
+ );
9529
+ if (sharedCallees.size > 0) {
9530
+ lines.push(`(all above \u2192 ${[...sharedCallees].join(", ")})`);
9531
+ }
9532
+ for (const node of nodes) {
9533
+ const callees = (nodeCallees.get(node.id) ?? []).filter((c) => !sharedCallees.has(c));
9534
+ for (const callee of callees) {
9535
+ dedup.markCallPair(node.name, callee);
9536
+ }
9537
+ if (callees.length === 0) continue;
9538
+ if (callees.length <= 5) {
9539
+ for (const callee of callees) dedup.markInLogic(callee);
9540
+ lines.push(`${node.name} \u2192 ${callees.join(", ")}`);
9541
+ } else {
9542
+ lines.push(`${node.name} \u2192`);
9543
+ for (const callee of callees) {
9544
+ dedup.markInLogic(callee);
9545
+ if (dedup.hasSymbol(callee)) {
9546
+ lines.push(` ${callee}`);
9547
+ } else {
9548
+ const calleeNode = [...graph.allNodes()].find((n) => n.name === callee);
9549
+ const path40 = calleeNode ? ` (${last2Segments(calleeNode.filePath)})` : "";
9550
+ lines.push(` ${callee}${path40}`);
9551
+ }
9552
+ }
9553
+ }
9554
+ }
9555
+ return lines.length > 1 ? lines.join("\n") : "";
9556
+ }
9557
+ function buildRelationBlock(nodes, graph, dedup) {
9558
+ if (nodes.length === 0) return "";
9559
+ const lines = ["[RELATION]"];
9560
+ for (const node of nodes) {
9561
+ const callers = [...graph.findEdgesTo(node.id)].filter((e) => e.kind === "calls").map((e) => graph.getNode(e.source)?.name).filter((n) => Boolean(n));
9562
+ const extendsNodes = [...graph.findEdgesFrom(node.id)].filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name).filter((n) => Boolean(n));
9563
+ const implementsNodes = [...graph.findEdgesFrom(node.id)].filter((e) => e.kind === "implements").map((e) => graph.getNode(e.target)?.name).filter((n) => Boolean(n));
9564
+ const highBlast = callers.length >= 5;
9565
+ const prefix = highBlast ? "\u26A1 " : "";
9566
+ if (callers.length > 0) {
9567
+ const nonDupCallers = callers.filter(
9568
+ (c) => highBlast || !dedup.hasCallPair(c, node.name)
9569
+ );
9570
+ if (nonDupCallers.length > 0) {
9571
+ const top3 = nonDupCallers.slice(0, 3);
9572
+ const rest = nonDupCallers.length - 3;
9573
+ const callerStr = top3.join(", ") + (rest > 0 ? ` (+${rest} more \u2014 use blast_radius for full list)` : "");
9574
+ lines.push(`${prefix}${node.name} \u2190 ${callerStr}`);
9575
+ }
9576
+ }
9577
+ const heritage = [];
9578
+ if (extendsNodes.length > 0) heritage.push(`extends ${extendsNodes.join(", ")}`);
9579
+ if (implementsNodes.length > 0) heritage.push(`implements ${implementsNodes.join(" \xB7 ")}`);
9580
+ if (heritage.length > 0) lines.push(`${node.name}: ${heritage.join(" \xB7 ")}`);
9581
+ }
9582
+ return lines.length > 1 ? lines.join("\n") : "";
9583
+ }
9584
+ function buildFocusCodeBlock(seeds, nodes, dedup, signatureOnlyThreshold, tokenBudget) {
9585
+ if (nodes.length === 0) return { text: "", truncated: false };
9586
+ const lines = ["[FOCUS CODE]"];
9587
+ let usedTokens = estimateTokens("[FOCUS CODE]");
9588
+ let truncated = false;
9589
+ for (let i = 0; i < nodes.length; i++) {
9590
+ const node = nodes[i];
9591
+ const seed = seeds.find((s) => s.nodeId === node.id);
9592
+ const score = seed?.refinedScore ?? 1;
9593
+ const content = node.content;
9594
+ const ml = content ? meaningfulLines(content).length : 0;
9595
+ if (ml <= 5 && dedup.isInLogic(node.name)) continue;
9596
+ const header = `// ${node.name} \u2014 ${last2Segments(node.filePath)}${node.startLine ? ":" + node.startLine : ""}`;
9597
+ if (score < signatureOnlyThreshold) {
9598
+ const sig = content?.split("\n").find((l) => l.trim().length > 0) ?? "";
9599
+ const sigLine = sig ? sig.trimEnd() + (sig.includes("{") ? " ... }" : "") : "";
9600
+ const entry2 = `${header}
9601
+ // (low relevance)
9602
+ ${sigLine}`;
9603
+ const toks2 = estimateTokens(entry2);
9604
+ if (usedTokens + toks2 > tokenBudget) {
9605
+ truncated = true;
9606
+ break;
9607
+ }
9608
+ lines.push(entry2);
9609
+ usedTokens += toks2;
9610
+ continue;
9611
+ }
9612
+ const { lines: snippet, truncated: snipTruncated } = adaptiveSnippet(content);
9613
+ const entry = `${header}
9614
+ \`\`\`
9615
+ ${snippet}
9616
+ \`\`\``;
9617
+ const toks = estimateTokens(entry);
9618
+ if (usedTokens + toks > tokenBudget) {
9619
+ truncated = true;
9620
+ break;
9621
+ }
9622
+ lines.push(entry);
9623
+ usedTokens += toks;
9624
+ if (snipTruncated) truncated = true;
9625
+ }
9626
+ return { text: lines.length > 1 ? lines.join("\n\n") : "", truncated };
9627
+ }
9628
+ function build(seeds, graph, options = {}) {
9629
+ const maxTokens = options.maxTokens ?? 6e3;
9630
+ const signatureOnlyThreshold = options.signatureOnlyThreshold ?? 0.3;
9631
+ const intent = options.queryIntent ?? "auto";
9632
+ const budgets = BUDGET_PRESETS[intent];
9633
+ const nodes = seeds.map((s) => graph.getNode(s.nodeId)).filter((n) => n !== void 0);
9634
+ const dedup = new DedupeRegistry();
9635
+ let available = maxTokens;
9636
+ const summaryText = buildSummaryBlock(nodes, graph, dedup);
9637
+ const summaryToks = estimateTokens(summaryText);
9638
+ const summaryUsed = Math.min(summaryToks, Math.min(budgets.summary, available));
9639
+ available -= summaryUsed;
9640
+ const logicText = buildLogicBlock(nodes, graph, dedup);
9641
+ const logicToks = estimateTokens(logicText);
9642
+ const logicBudget = Math.min(budgets.logic, Math.floor(available * 0.35));
9643
+ const logicUsed = Math.min(logicToks, logicBudget);
9644
+ available -= logicUsed;
9645
+ const relationText = buildRelationBlock(nodes, graph, dedup);
9646
+ const relationToks = estimateTokens(relationText);
9647
+ const relationBudget = Math.min(budgets.relation, Math.floor(available * 0.35));
9648
+ const relationUsed = Math.min(relationToks, relationBudget);
9649
+ available -= relationUsed;
9650
+ const focusBudget = available;
9651
+ const { text: focusText, truncated } = buildFocusCodeBlock(
9652
+ seeds,
9653
+ nodes,
9654
+ dedup,
9655
+ signatureOnlyThreshold,
9656
+ focusBudget
9657
+ );
9658
+ return {
9659
+ summary: summaryText,
9660
+ logic: logicText,
9661
+ relation: relationText,
9662
+ focusCode: focusText,
9663
+ truncated,
9664
+ intent
9665
+ };
9666
+ }
9667
+ var BUDGET_PRESETS, DedupeRegistry;
9668
+ var init_builder = __esm({
9669
+ "src/context/builder.ts"() {
9670
+ init_token_counter();
9671
+ BUDGET_PRESETS = {
9672
+ code: { summary: 300, logic: 400, relation: 300, focusCode: 5e3 },
9673
+ callers: { summary: 500, logic: 300, relation: 2500, focusCode: 700 },
9674
+ architecture: { summary: 1200, logic: 800, relation: 800, focusCode: 1200 },
9675
+ auto: { summary: 800, logic: 600, relation: 500, focusCode: 1500 }
9676
+ };
9677
+ DedupeRegistry = class {
9678
+ seenSymbols = /* @__PURE__ */ new Set();
9679
+ seenFilePaths = /* @__PURE__ */ new Set();
9680
+ seenCallPairs = /* @__PURE__ */ new Set();
9681
+ logicSymbols = /* @__PURE__ */ new Set();
9682
+ // B.4.2: symbols referenced in LOGIC
9683
+ /** Returns full format on first mention, name-only on repeats. */
9684
+ formatSymbol(name, filePath, extra) {
9685
+ const key = name;
9686
+ if (this.seenSymbols.has(key)) return name;
9687
+ this.seenSymbols.add(key);
9688
+ this.seenFilePaths.add(filePath);
9689
+ return extra;
9690
+ }
9691
+ hasSymbol(name) {
9692
+ return this.seenSymbols.has(name);
9693
+ }
9694
+ markCallPair(caller, callee) {
9695
+ this.seenCallPairs.add(`${caller}\u2192${callee}`);
9696
+ }
9697
+ hasCallPair(caller, callee) {
9698
+ return this.seenCallPairs.has(`${caller}\u2192${callee}`);
9699
+ }
9700
+ hasFilePath(fp) {
9701
+ return this.seenFilePaths.has(fp);
9702
+ }
9703
+ /** Mark a symbol as referenced in the LOGIC block (B.4.2). */
9704
+ markInLogic(name) {
9705
+ this.logicSymbols.add(name);
9706
+ }
9707
+ /** Returns true only if symbol was referenced in LOGIC (B.4.2). */
9708
+ isInLogic(name) {
9709
+ return this.logicSymbols.has(name);
9710
+ }
9711
+ };
9712
+ }
9713
+ });
9714
+
9269
9715
  // src/cli/main.ts
9270
9716
  init_logger();
9271
9717
  init_knowledge_graph();
@@ -9763,7 +10209,7 @@ init_embedder();
9763
10209
  async function hybridSearch(graph, query, limit, options = {}) {
9764
10210
  const { vectorDbPath, bm25Limit = 50, vectorLimit = 50, bm25Results: precomputedBm25 } = options;
9765
10211
  const bm25Promise = precomputedBm25 ? Promise.resolve(precomputedBm25) : Promise.resolve(textSearch(graph, query, bm25Limit));
9766
- const hasVectorDb = Boolean(vectorDbPath && fs38.existsSync(vectorDbPath));
10212
+ const hasVectorDb = Boolean(vectorDbPath && fs39.existsSync(vectorDbPath));
9767
10213
  if (!hasVectorDb) {
9768
10214
  const bm25Results2 = await bm25Promise;
9769
10215
  return {
@@ -9831,7 +10277,7 @@ async function queryGroup(group, query, limit = 20) {
9831
10277
  const regEntry = registry.find((r) => r.name === member.registryName);
9832
10278
  if (!regEntry) continue;
9833
10279
  const dbPath = path39.join(regEntry.path, ".code-intel", "graph.db");
9834
- if (!fs38.existsSync(dbPath)) continue;
10280
+ if (!fs39.existsSync(dbPath)) continue;
9835
10281
  const graph = createKnowledgeGraph();
9836
10282
  const db = new DbManager(dbPath, true);
9837
10283
  try {
@@ -9873,7 +10319,7 @@ var STUCK_THRESHOLD_MINUTES = 30;
9873
10319
  var JobsDB = class {
9874
10320
  db;
9875
10321
  constructor(dbPath) {
9876
- fs38.mkdirSync(path39.dirname(dbPath), { recursive: true });
10322
+ fs39.mkdirSync(path39.dirname(dbPath), { recursive: true });
9877
10323
  this.db = new Database2(dbPath);
9878
10324
  this.db.pragma("journal_mode = WAL");
9879
10325
  this.db.pragma("foreign_keys = ON");
@@ -10015,7 +10461,7 @@ var JobsDB = class {
10015
10461
  }
10016
10462
  };
10017
10463
  function getJobsDBPath() {
10018
- return path39.join(os13.homedir(), ".code-intel", "jobs.db");
10464
+ return path39.join(os18.homedir(), ".code-intel", "jobs.db");
10019
10465
  }
10020
10466
  var _jobsDB = null;
10021
10467
  function getOrCreateJobsDB() {
@@ -10110,14 +10556,14 @@ var BACKUP_VERSION = "1.0";
10110
10556
  var ALGORITHM = "aes-256-gcm";
10111
10557
  var IV_LENGTH = 16;
10112
10558
  function getBackupDir() {
10113
- return path39.join(os13.homedir(), ".code-intel", "backups");
10559
+ return path39.join(os18.homedir(), ".code-intel", "backups");
10114
10560
  }
10115
10561
  function getBackupKey() {
10116
10562
  const keyHex = process.env["CODE_INTEL_BACKUP_KEY"];
10117
10563
  if (keyHex && keyHex.length >= 64) {
10118
10564
  return Buffer.from(keyHex.slice(0, 64), "hex");
10119
10565
  }
10120
- const seed = `code-intel-backup-${os13.hostname()}-${os13.homedir()}`;
10566
+ const seed = `code-intel-backup-${os18.hostname()}-${os18.homedir()}`;
10121
10567
  return crypto5.createHash("sha256").update(seed).digest();
10122
10568
  }
10123
10569
  function encryptBuffer(data, key) {
@@ -10141,7 +10587,7 @@ var BackupService = class {
10141
10587
  constructor(backupDir) {
10142
10588
  this.backupDir = backupDir ?? getBackupDir();
10143
10589
  this.key = getBackupKey();
10144
- fs38.mkdirSync(this.backupDir, { recursive: true });
10590
+ fs39.mkdirSync(this.backupDir, { recursive: true });
10145
10591
  }
10146
10592
  /**
10147
10593
  * Create a backup for a repository.
@@ -10155,16 +10601,16 @@ var BackupService = class {
10155
10601
  const candidates = ["graph.db", "vector.db", "meta.json"];
10156
10602
  for (const f of candidates) {
10157
10603
  const fp = path39.join(codeIntelDir, f);
10158
- if (fs38.existsSync(fp)) {
10604
+ if (fs39.existsSync(fp)) {
10159
10605
  filesToBackup.push({ name: f, localPath: fp });
10160
10606
  }
10161
10607
  }
10162
- const registryPath = path39.join(os13.homedir(), ".code-intel", "registry.json");
10163
- if (fs38.existsSync(registryPath)) {
10608
+ const registryPath = path39.join(os18.homedir(), ".code-intel", "registry.json");
10609
+ if (fs39.existsSync(registryPath)) {
10164
10610
  filesToBackup.push({ name: "registry.json", localPath: registryPath });
10165
10611
  }
10166
- const usersDbPath = path39.join(os13.homedir(), ".code-intel", "users.db");
10167
- if (fs38.existsSync(usersDbPath)) {
10612
+ const usersDbPath = path39.join(os18.homedir(), ".code-intel", "users.db");
10613
+ if (fs39.existsSync(usersDbPath)) {
10168
10614
  filesToBackup.push({ name: "users.db", localPath: usersDbPath });
10169
10615
  }
10170
10616
  if (filesToBackup.length === 0) {
@@ -10175,7 +10621,7 @@ var BackupService = class {
10175
10621
  createdAt,
10176
10622
  version: BACKUP_VERSION,
10177
10623
  files: filesToBackup.map((f) => {
10178
- const data = fs38.readFileSync(f.localPath);
10624
+ const data = fs39.readFileSync(f.localPath);
10179
10625
  return {
10180
10626
  name: f.name,
10181
10627
  sha256: crypto5.createHash("sha256").update(data).digest("hex"),
@@ -10189,7 +10635,7 @@ var BackupService = class {
10189
10635
  manifestLenBuf.writeUInt32BE(manifestBuf.length, 0);
10190
10636
  parts.push(manifestLenBuf, manifestBuf);
10191
10637
  for (const f of filesToBackup) {
10192
- const data = fs38.readFileSync(f.localPath);
10638
+ const data = fs39.readFileSync(f.localPath);
10193
10639
  const nameBuf = Buffer.from(f.name, "utf-8");
10194
10640
  const nameLenBuf = Buffer.alloc(2);
10195
10641
  nameLenBuf.writeUInt16BE(nameBuf.length, 0);
@@ -10201,7 +10647,7 @@ var BackupService = class {
10201
10647
  const encrypted = encryptBuffer(plaintext, this.key);
10202
10648
  const backupFileName = `backup-${id}.cib`;
10203
10649
  const backupPath = path39.join(this.backupDir, backupFileName);
10204
- fs38.writeFileSync(backupPath, encrypted);
10650
+ fs39.writeFileSync(backupPath, encrypted);
10205
10651
  const entry = {
10206
10652
  id,
10207
10653
  createdAt,
@@ -10230,7 +10676,7 @@ var BackupService = class {
10230
10676
  if (!cfg) throw new Error("S3 not configured. Set CODE_INTEL_BACKUP_S3_BUCKET, CODE_INTEL_BACKUP_S3_ACCESS_KEY_ID, CODE_INTEL_BACKUP_S3_SECRET_ACCESS_KEY.");
10231
10677
  const fileName = path39.basename(entry.path);
10232
10678
  const s3Key = `${cfg.prefix}${fileName}`;
10233
- const body = fs38.readFileSync(entry.path);
10679
+ const body = fs39.readFileSync(entry.path);
10234
10680
  const result = await s3Request({ method: "PUT", cfg, key: s3Key, body });
10235
10681
  if (result.statusCode < 200 || result.statusCode >= 300) {
10236
10682
  throw new Error(`S3 upload failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
@@ -10247,8 +10693,8 @@ var BackupService = class {
10247
10693
  if (result.statusCode < 200 || result.statusCode >= 300) {
10248
10694
  throw new Error(`S3 download failed (HTTP ${result.statusCode}): ${result.body.slice(0, 200)}`);
10249
10695
  }
10250
- fs38.mkdirSync(path39.dirname(destPath), { recursive: true });
10251
- fs38.writeFileSync(destPath, Buffer.from(result.body, "binary"));
10696
+ fs39.mkdirSync(path39.dirname(destPath), { recursive: true });
10697
+ fs39.writeFileSync(destPath, Buffer.from(result.body, "binary"));
10252
10698
  }
10253
10699
  /**
10254
10700
  * List backup objects in S3 with the configured prefix.
@@ -10294,10 +10740,10 @@ var BackupService = class {
10294
10740
  if (!entry) {
10295
10741
  throw new Error(`Backup "${backupId}" not found.`);
10296
10742
  }
10297
- if (!fs38.existsSync(entry.path)) {
10743
+ if (!fs39.existsSync(entry.path)) {
10298
10744
  throw new Error(`Backup file not found at: ${entry.path}`);
10299
10745
  }
10300
- const encrypted = fs38.readFileSync(entry.path);
10746
+ const encrypted = fs39.readFileSync(entry.path);
10301
10747
  let plaintext;
10302
10748
  try {
10303
10749
  plaintext = decryptBuffer(encrypted, this.key);
@@ -10312,7 +10758,7 @@ var BackupService = class {
10312
10758
  const manifest = JSON.parse(manifestStr);
10313
10759
  const restoreBase = targetRepoPath ?? entry.repoPath;
10314
10760
  const codeIntelDir = path39.join(restoreBase, ".code-intel");
10315
- fs38.mkdirSync(codeIntelDir, { recursive: true });
10761
+ fs39.mkdirSync(codeIntelDir, { recursive: true });
10316
10762
  for (const fileEntry of manifest.files) {
10317
10763
  const nameLen = plaintext.readUInt16BE(offset);
10318
10764
  offset += 2;
@@ -10329,18 +10775,18 @@ var BackupService = class {
10329
10775
  }
10330
10776
  let destPath;
10331
10777
  if (name === "registry.json" || name === "users.db") {
10332
- destPath = path39.join(os13.homedir(), ".code-intel", name);
10778
+ destPath = path39.join(os18.homedir(), ".code-intel", name);
10333
10779
  } else {
10334
10780
  destPath = path39.join(codeIntelDir, name);
10335
10781
  }
10336
- fs38.writeFileSync(destPath, data);
10782
+ fs39.writeFileSync(destPath, data);
10337
10783
  }
10338
10784
  }
10339
10785
  /**
10340
10786
  * Apply retention policy: keep N daily, M weekly, L monthly backups.
10341
10787
  */
10342
10788
  applyRetention(options = { daily: 7, weekly: 4, monthly: 12 }) {
10343
- const entries = this._loadIndex().filter((e) => fs38.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
10789
+ const entries = this._loadIndex().filter((e) => fs39.existsSync(e.path)).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
10344
10790
  const keep = /* @__PURE__ */ new Set();
10345
10791
  const now = /* @__PURE__ */ new Date();
10346
10792
  const dailyCutoff = new Date(now);
@@ -10370,7 +10816,7 @@ var BackupService = class {
10370
10816
  for (const e of entries) {
10371
10817
  if (!keep.has(e.id)) {
10372
10818
  try {
10373
- fs38.unlinkSync(e.path);
10819
+ fs39.unlinkSync(e.path);
10374
10820
  deleted++;
10375
10821
  } catch {
10376
10822
  }
@@ -10386,13 +10832,13 @@ var BackupService = class {
10386
10832
  }
10387
10833
  _loadIndex() {
10388
10834
  try {
10389
- return JSON.parse(fs38.readFileSync(this._indexPath(), "utf-8"));
10835
+ return JSON.parse(fs39.readFileSync(this._indexPath(), "utf-8"));
10390
10836
  } catch {
10391
10837
  return [];
10392
10838
  }
10393
10839
  }
10394
10840
  _saveIndex(entries) {
10395
- fs38.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
10841
+ fs39.writeFileSync(this._indexPath(), JSON.stringify(entries, null, 2));
10396
10842
  }
10397
10843
  _appendIndex(entry) {
10398
10844
  const entries = this._loadIndex();
@@ -11036,7 +11482,7 @@ var openApiSpec = {
11036
11482
  var __dirname$1 = path39.dirname(fileURLToPath(import.meta.url));
11037
11483
  var WEB_DIST = (() => {
11038
11484
  const bundled = path39.resolve(__dirname$1, "..", "web");
11039
- if (fs38.existsSync(bundled)) return bundled;
11485
+ if (fs39.existsSync(bundled)) return bundled;
11040
11486
  return path39.resolve(__dirname$1, "..", "..", "..", "web", "dist");
11041
11487
  })();
11042
11488
  function getAllowedOrigins() {
@@ -11121,8 +11567,8 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11121
11567
  const metaFilePath = path39.join(workspaceRoot, ".code-intel", "meta.json");
11122
11568
  let metaOk = false;
11123
11569
  try {
11124
- if (fs38.existsSync(metaFilePath)) {
11125
- const raw = fs38.readFileSync(metaFilePath, "utf-8");
11570
+ if (fs39.existsSync(metaFilePath)) {
11571
+ const raw = fs39.readFileSync(metaFilePath, "utf-8");
11126
11572
  const meta = JSON.parse(raw);
11127
11573
  if (meta?.indexVersion) res.setHeader("X-Index-Version", meta.indexVersion);
11128
11574
  }
@@ -11313,12 +11759,12 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11313
11759
  return;
11314
11760
  }
11315
11761
  const user = db.createUser(username, password, "admin");
11316
- const sessionId = createSession({ id: user.id, username: user.username, role: user.role });
11317
- res.setHeader("Set-Cookie", buildSessionCookie(sessionId));
11762
+ const { sessionId, ttlMs } = createSession({ id: user.id, username: user.username, role: user.role });
11763
+ res.setHeader("Set-Cookie", buildSessionCookie(sessionId, ttlMs));
11318
11764
  res.status(201).json({ user: { id: user.id, username: user.username, role: user.role } });
11319
11765
  });
11320
11766
  app.post("/auth/login", async (req, res) => {
11321
- const { username, password } = req.body;
11767
+ const { username, password, rememberMe } = req.body;
11322
11768
  if (!username || !password) {
11323
11769
  res.status(400).json({
11324
11770
  error: {
@@ -11362,10 +11808,10 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11362
11808
  });
11363
11809
  return;
11364
11810
  }
11365
- const sessionId = createSession({ id: user.id, username: user.username, role: user.role });
11811
+ const { sessionId, ttlMs } = createSession({ id: user.id, username: user.username, role: user.role }, rememberMe === true);
11366
11812
  db.logAccess(user.id, "/auth/login", "login", "allow", req.ip ?? "unknown");
11367
11813
  authAttemptsTotal.inc({ method: "local", outcome: "success" });
11368
- res.setHeader("Set-Cookie", buildSessionCookie(sessionId));
11814
+ res.setHeader("Set-Cookie", buildSessionCookie(sessionId, ttlMs));
11369
11815
  res.json({ user: { id: user.id, username: user.username, role: user.role } });
11370
11816
  });
11371
11817
  app.post("/auth/logout", (req, res) => {
@@ -11487,9 +11933,9 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11487
11933
  authAttemptsTotal.inc({ method: "oidc", outcome: "success" });
11488
11934
  logger_default.info(`[oidc] Auto-provisioned new user: ${finalUsername} (${cfg.defaultRole})`);
11489
11935
  }
11490
- const sessionId = createSession({ id: user.id, username: user.username, role: user.role });
11936
+ const { sessionId, ttlMs } = createSession({ id: user.id, username: user.username, role: user.role });
11491
11937
  db.logAccess(user.id, "/auth/callback", "oidc-login", "allow", req.ip ?? "unknown");
11492
- res.setHeader("Set-Cookie", buildSessionCookie(sessionId));
11938
+ res.setHeader("Set-Cookie", buildSessionCookie(sessionId, ttlMs));
11493
11939
  res.redirect(302, "/");
11494
11940
  } catch (err) {
11495
11941
  logger_default.warn("[oidc] Callback failed:", err instanceof Error ? err.message : err);
@@ -11607,7 +12053,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11607
12053
  const entry = registry.find((r) => r.name === requestedRepo || r.path === requestedRepo);
11608
12054
  if (!entry) return null;
11609
12055
  const dbPath = path39.join(entry.path, ".code-intel", "graph.db");
11610
- if (!fs38.existsSync(dbPath)) return null;
12056
+ if (!fs39.existsSync(dbPath)) return null;
11611
12057
  const repoGraph = createKnowledgeGraph();
11612
12058
  const db = new DbManager(dbPath, true);
11613
12059
  try {
@@ -11629,7 +12075,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11629
12075
  const regEntry = registry.find((r) => r.name === member.registryName);
11630
12076
  if (!regEntry) continue;
11631
12077
  const dbPath = path39.join(regEntry.path, ".code-intel", "graph.db");
11632
- if (!fs38.existsSync(dbPath)) continue;
12078
+ if (!fs39.existsSync(dbPath)) continue;
11633
12079
  const db = new DbManager(dbPath, true);
11634
12080
  try {
11635
12081
  await db.init();
@@ -11711,7 +12157,21 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11711
12157
  });
11712
12158
  });
11713
12159
  app.post("/api/v1/search", requireToolScope("search"), async (req, res) => {
11714
- const { query, limit, repo } = req.body;
12160
+ const { query, limit, repo, group } = req.body;
12161
+ if (group) {
12162
+ const grp = loadGroup(group);
12163
+ if (!grp) {
12164
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: `Group '${group}' not found`, hint: "Use /api/v1/groups to list available groups" } });
12165
+ return;
12166
+ }
12167
+ try {
12168
+ const { perRepo, merged } = await queryGroup(grp, query ?? "", limit ?? 20);
12169
+ res.json({ results: merged, perRepo, searchMode: "bm25", group });
12170
+ } catch (err) {
12171
+ res.status(500).json({ error: { code: ErrorCodes.INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err) } });
12172
+ }
12173
+ return;
12174
+ }
11715
12175
  const g = await getGraphForRepo(repo);
11716
12176
  const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
11717
12177
  const bm25 = !repo || repo === repoName ? ensureBm25Index() : null;
@@ -11720,7 +12180,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11720
12180
  vectorDbPath: vdbPath,
11721
12181
  bm25Results: bm25Results ?? void 0
11722
12182
  });
11723
- res.json({ results, searchMode });
12183
+ res.json({ results, searchMode, repo: repo ?? repoName });
11724
12184
  });
11725
12185
  app.post("/api/v1/vector-search", async (req, res) => {
11726
12186
  const { query, limit = 10 } = req.body;
@@ -11762,7 +12222,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11762
12222
  return;
11763
12223
  }
11764
12224
  try {
11765
- const content = fs38.readFileSync(file_path, "utf-8");
12225
+ const content = fs39.readFileSync(file_path, "utf-8");
11766
12226
  res.json({ content });
11767
12227
  } catch {
11768
12228
  res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "File not found" } });
@@ -11994,6 +12454,97 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
11994
12454
  }
11995
12455
  res.json(result);
11996
12456
  });
12457
+ app.post("/api/v1/groups", requireAuth, requireRole("analyst"), (req, res) => {
12458
+ const { name } = req.body;
12459
+ if (!name || !name.trim()) {
12460
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "Group name is required" } });
12461
+ return;
12462
+ }
12463
+ const trimmed = name.trim();
12464
+ if (groupExists(trimmed)) {
12465
+ res.status(409).json({ error: { code: ErrorCodes.CONFLICT, message: `Group "${trimmed}" already exists` } });
12466
+ return;
12467
+ }
12468
+ const group = { name: trimmed, createdAt: (/* @__PURE__ */ new Date()).toISOString(), members: [] };
12469
+ saveGroup(group);
12470
+ res.status(201).json(group);
12471
+ });
12472
+ app.delete("/api/v1/groups/:name", requireAuth, requireRole("analyst"), (req, res) => {
12473
+ const groupName = req.params["name"];
12474
+ if (!groupExists(groupName)) {
12475
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
12476
+ return;
12477
+ }
12478
+ deleteGroup(groupName);
12479
+ res.status(204).end();
12480
+ });
12481
+ app.patch("/api/v1/groups/:name", requireAuth, requireRole("analyst"), (req, res) => {
12482
+ const groupName = req.params["name"];
12483
+ const group = loadGroup(groupName);
12484
+ if (!group) {
12485
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
12486
+ return;
12487
+ }
12488
+ const { name } = req.body;
12489
+ if (!name || !name.trim()) {
12490
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "New name is required" } });
12491
+ return;
12492
+ }
12493
+ const newName = name.trim();
12494
+ if (newName !== group.name && groupExists(newName)) {
12495
+ res.status(409).json({ error: { code: ErrorCodes.CONFLICT, message: `Group "${newName}" already exists` } });
12496
+ return;
12497
+ }
12498
+ if (newName !== group.name) {
12499
+ deleteGroup(group.name);
12500
+ group.name = newName;
12501
+ }
12502
+ saveGroup(group);
12503
+ res.json(group);
12504
+ });
12505
+ app.post("/api/v1/groups/:name/members", requireAuth, requireRole("analyst"), (req, res) => {
12506
+ const groupName = req.params["name"];
12507
+ const group = loadGroup(groupName);
12508
+ if (!group) {
12509
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
12510
+ return;
12511
+ }
12512
+ const { groupPath, registryName } = req.body;
12513
+ if (!groupPath || !registryName) {
12514
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "groupPath and registryName are required" } });
12515
+ return;
12516
+ }
12517
+ const registry = loadRegistry();
12518
+ if (!registry.find((r) => r.name === registryName)) {
12519
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: `Repo "${registryName}" not found in registry. Run code-intel analyze first.` } });
12520
+ return;
12521
+ }
12522
+ try {
12523
+ const updated = addMember(groupName, { groupPath, registryName });
12524
+ res.json(updated);
12525
+ } catch (err) {
12526
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: err instanceof Error ? err.message : String(err) } });
12527
+ }
12528
+ });
12529
+ app.delete("/api/v1/groups/:name/members", requireAuth, requireRole("analyst"), (req, res) => {
12530
+ const groupName = req.params["name"];
12531
+ const group = loadGroup(groupName);
12532
+ if (!group) {
12533
+ res.status(404).json({ error: { code: ErrorCodes.NOT_FOUND, message: "Group not found" } });
12534
+ return;
12535
+ }
12536
+ const { groupPath } = req.body;
12537
+ if (!groupPath) {
12538
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: "groupPath is required" } });
12539
+ return;
12540
+ }
12541
+ try {
12542
+ const updated = removeMember(groupName, groupPath);
12543
+ res.json(updated);
12544
+ } catch (err) {
12545
+ res.status(400).json({ error: { code: ErrorCodes.INVALID_REQUEST, message: err instanceof Error ? err.message : String(err) } });
12546
+ }
12547
+ });
11997
12548
  app.post("/api/v1/groups/:name/sync", async (req, res) => {
11998
12549
  const group = loadGroup(req.params.name);
11999
12550
  if (!group) {
@@ -12004,8 +12555,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
12004
12555
  const result = await syncGroup(group);
12005
12556
  saveSyncResult(result);
12006
12557
  group.lastSync = result.syncedAt;
12007
- const { saveGroup: saveGroup2 } = await Promise.resolve().then(() => (init_group_registry(), group_registry_exports));
12008
- saveGroup2(group);
12558
+ saveGroup(group);
12009
12559
  res.json(result);
12010
12560
  } catch (err) {
12011
12561
  res.status(500).json({ error: { code: ErrorCodes.INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err) } });
@@ -12041,7 +12591,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
12041
12591
  const regEntry = registry.find((r) => r.name === member.registryName);
12042
12592
  if (!regEntry) continue;
12043
12593
  const dbPath = path39.join(regEntry.path, ".code-intel", "graph.db");
12044
- if (!fs38.existsSync(dbPath)) continue;
12594
+ if (!fs39.existsSync(dbPath)) continue;
12045
12595
  const db = new DbManager(dbPath, true);
12046
12596
  try {
12047
12597
  await db.init();
@@ -12068,7 +12618,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
12068
12618
  let edgeCount = 0;
12069
12619
  if (regEntry) {
12070
12620
  const dbPath = path39.join(regEntry.path, ".code-intel", "graph.db");
12071
- if (fs38.existsSync(dbPath)) {
12621
+ if (fs39.existsSync(dbPath)) {
12072
12622
  try {
12073
12623
  const db = new DbManager(dbPath, true);
12074
12624
  await db.init();
@@ -12131,7 +12681,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
12131
12681
  const regEntry = registry.find((r) => r.name === member.registryName);
12132
12682
  if (!regEntry) continue;
12133
12683
  const candidate = path39.resolve(path39.join(regEntry.path, normalizedFile));
12134
- if (fs38.existsSync(candidate)) {
12684
+ if (fs39.existsSync(candidate)) {
12135
12685
  baseDir = regEntry.path;
12136
12686
  break;
12137
12687
  }
@@ -12183,7 +12733,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
12183
12733
  }
12184
12734
  let fileContent;
12185
12735
  try {
12186
- fileContent = fs38.readFileSync(resolvedFile, "utf-8");
12736
+ fileContent = fs39.readFileSync(resolvedFile, "utf-8");
12187
12737
  } catch {
12188
12738
  res.status(404).json({
12189
12739
  error: {
@@ -12349,7 +12899,7 @@ function createApp(graph, repoName, workspaceRoot, watcherState) {
12349
12899
  res.status(500).json({ error: { code: ErrorCodes.INTERNAL_ERROR, message: err instanceof Error ? err.message : String(err), requestId: req.requestId, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
12350
12900
  }
12351
12901
  });
12352
- if (fs38.existsSync(WEB_DIST)) {
12902
+ if (fs39.existsSync(WEB_DIST)) {
12353
12903
  app.use(express.static(WEB_DIST));
12354
12904
  app.get("/{*path}", (_req, res) => {
12355
12905
  res.sendFile(path39.join(WEB_DIST, "index.html"));
@@ -12503,6 +13053,7 @@ async function startHttpServer(graph, repoName, port = 4747, workspaceRoot, watc
12503
13053
  });
12504
13054
  });
12505
13055
  }
13056
+ init_bm25_index();
12506
13057
  init_storage();
12507
13058
  init_repo_registry();
12508
13059
  init_metadata();
@@ -13029,11 +13580,30 @@ function summarizeCluster(graph, cluster) {
13029
13580
  }
13030
13581
 
13031
13582
  // src/mcp-server/server.ts
13583
+ function compact(obj) {
13584
+ return JSON.stringify(obj, (_key, value) => value === null || value === void 0 ? void 0 : value);
13585
+ }
13032
13586
  function createMcpServer(graph, repoName, workspaceRoot) {
13033
13587
  const server = new Server(
13034
13588
  { name: "code-intel", version: "0.1.0" },
13035
13589
  { capabilities: { tools: {}, resources: {} } }
13036
13590
  );
13591
+ let bm25Index = null;
13592
+ function ensureBm25Index() {
13593
+ if (bm25Index) return bm25Index;
13594
+ if (!workspaceRoot) return null;
13595
+ try {
13596
+ const idx = new Bm25Index(getBm25DbPath(workspaceRoot));
13597
+ idx.load();
13598
+ bm25Index = idx;
13599
+ return bm25Index;
13600
+ } catch {
13601
+ return null;
13602
+ }
13603
+ }
13604
+ if (workspaceRoot) {
13605
+ setImmediate(() => ensureBm25Index());
13606
+ }
13037
13607
  const _tokenProp = {
13038
13608
  _token: { type: "string", description: "Required if CODE_INTEL_TOKEN is configured" }
13039
13609
  };
@@ -13053,13 +13623,15 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13053
13623
  // ── Search & inspect ─────────────────────────────────────────────────
13054
13624
  {
13055
13625
  name: "search",
13056
- description: "BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc.",
13626
+ description: "BM25 keyword search across all indexed symbols \u2014 functions, classes, files, routes, etc. Optionally scope to a specific repo or group.",
13057
13627
  inputSchema: {
13058
13628
  type: "object",
13059
13629
  properties: {
13060
13630
  query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
13061
13631
  offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
13062
- limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
13632
+ limit: { type: "number", description: "Max results per page (default: 10, max: 500)" },
13633
+ repo: { type: "string", description: "Scope search to a specific indexed repo name (optional; defaults to current repo)" },
13634
+ group: { type: "string", description: "Scope search across all repos in a group via cross-repo RRF merge (optional; overrides repo)" },
13063
13635
  ..._tokenProp
13064
13636
  },
13065
13637
  required: ["query"]
@@ -13089,7 +13661,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13089
13661
  enum: ["callers", "callees", "both"],
13090
13662
  description: "Which direction to trace \u2014 callers (who depends on it), callees (what it depends on), or both (default: both)"
13091
13663
  },
13092
- max_hops: { type: "number", description: "Maximum traversal depth (default: 5)" },
13664
+ max_hops: { type: "number", description: "Maximum traversal depth (default: 2, max: 10)" },
13093
13665
  ..._tokenProp
13094
13666
  },
13095
13667
  required: ["target"]
@@ -13103,7 +13675,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13103
13675
  properties: {
13104
13676
  file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' },
13105
13677
  offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
13106
- limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
13678
+ limit: { type: "number", description: "Max results per page (default: 10, max: 500)" },
13107
13679
  ..._tokenProp
13108
13680
  },
13109
13681
  required: ["file_path"]
@@ -13134,7 +13706,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13134
13706
  description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
13135
13707
  },
13136
13708
  offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
13137
- limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
13709
+ limit: { type: "number", description: "Max results per page (default: 10, max: 500)" },
13138
13710
  ..._tokenProp
13139
13711
  }
13140
13712
  }
@@ -13152,7 +13724,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13152
13724
  type: "object",
13153
13725
  properties: {
13154
13726
  offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
13155
- limit: { type: "number", description: "Max clusters per page (default: 50, max: 500)" },
13727
+ limit: { type: "number", description: "Max clusters per page (default: 10, max: 500)" },
13156
13728
  ..._tokenProp
13157
13729
  }
13158
13730
  }
@@ -13164,7 +13736,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13164
13736
  type: "object",
13165
13737
  properties: {
13166
13738
  offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
13167
- limit: { type: "number", description: "Max flows per page (default: 50, max: 500)" },
13739
+ limit: { type: "number", description: "Max flows per page (default: 10, max: 500)" },
13168
13740
  ..._tokenProp
13169
13741
  }
13170
13742
  }
@@ -13318,7 +13890,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13318
13890
  },
13319
13891
  maxHops: {
13320
13892
  type: "number",
13321
- description: "Maximum BFS depth for blast radius (default: 5)"
13893
+ description: "Maximum BFS depth for blast radius (default: 2, max: 10)"
13322
13894
  },
13323
13895
  ..._tokenProp
13324
13896
  }
@@ -13446,13 +14018,13 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13446
14018
  const providedToken = a._token;
13447
14019
  if (providedToken !== expectedToken) {
13448
14020
  return {
13449
- content: [{ type: "text", text: JSON.stringify({ error: "Unauthorized: invalid or missing CODE_INTEL_TOKEN" }) }],
14021
+ content: [{ type: "text", text: compact({ error: "Unauthorized: invalid or missing CODE_INTEL_TOKEN" }) }],
13450
14022
  isError: true
13451
14023
  };
13452
14024
  }
13453
14025
  }
13454
14026
  const startMs = Date.now();
13455
- const dispatch = () => dispatchTool(name, a, graph, repoName, workspaceRoot);
14027
+ const dispatch = () => dispatchTool(name, a, graph, repoName, workspaceRoot, ensureBm25Index);
13456
14028
  const MCP_TIMEOUT_MS = parseInt(process.env["CODE_INTEL_MCP_TIMEOUT_MS"] ?? "30000", 10);
13457
14029
  let timeoutHandle = null;
13458
14030
  let timedOut = false;
@@ -13484,7 +14056,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13484
14056
  mcpToolDurationSeconds.observe({ tool: name }, (Date.now() - startMs) / 1e3);
13485
14057
  if (timedOut) {
13486
14058
  return {
13487
- content: [{ type: "text", text: JSON.stringify({ truncated: true, reason: `Tool '${name}' timed out after ${MCP_TIMEOUT_MS}ms`, partialResults: [] }) }],
14059
+ content: [{ type: "text", text: compact({ truncated: true, reason: `Tool '${name}' timed out after ${MCP_TIMEOUT_MS}ms`, partialResults: [] }) }],
13488
14060
  isError: false
13489
14061
  };
13490
14062
  }
@@ -13499,7 +14071,7 @@ function createMcpServer(graph, repoName, workspaceRoot) {
13499
14071
  registerResources(server, graph, repoName);
13500
14072
  return server;
13501
14073
  }
13502
- async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14074
+ async function dispatchTool(name, a, graph, repoName, workspaceRoot, bm25Resolver) {
13503
14075
  switch (name) {
13504
14076
  // ── repos ──────────────────────────────────────────────────────────────
13505
14077
  case "repos": {
@@ -13507,10 +14079,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13507
14079
  return {
13508
14080
  content: [{
13509
14081
  type: "text",
13510
- text: JSON.stringify(
13511
- registry.map((r) => ({ name: r.name, path: r.path, indexedAt: r.indexedAt, stats: r.stats })),
13512
- null,
13513
- 2
14082
+ text: compact(
14083
+ registry.map((r) => ({ name: r.name, path: r.path, indexedAt: r.indexedAt, stats: r.stats }))
13514
14084
  )
13515
14085
  }]
13516
14086
  };
@@ -13538,13 +14108,13 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13538
14108
  return {
13539
14109
  content: [{
13540
14110
  type: "text",
13541
- text: JSON.stringify({
14111
+ text: compact({
13542
14112
  repo: repoName,
13543
14113
  stats: graph.size,
13544
14114
  nodeCounts: kindCounts,
13545
14115
  edgeCounts,
13546
14116
  health
13547
- }, null, 2)
14117
+ })
13548
14118
  }]
13549
14119
  };
13550
14120
  }
@@ -13552,15 +14122,59 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13552
14122
  case "search": {
13553
14123
  const query = a.query;
13554
14124
  const offset = a.offset ?? 0;
13555
- const effectiveLimit = Math.min(a.limit ?? 50, 500);
14125
+ const effectiveLimit = Math.min(a.limit ?? 10, 500);
14126
+ if (a.group) {
14127
+ const grp = loadGroup(a.group);
14128
+ if (!grp) {
14129
+ return { content: [{ type: "text", text: `Group "${a.group}" not found. Use list_groups to see available groups.` }] };
14130
+ }
14131
+ const { perRepo, merged } = await queryGroup(grp, query, effectiveLimit + offset);
14132
+ const paged = merged.slice(offset, offset + effectiveLimit);
14133
+ return {
14134
+ content: [{
14135
+ type: "text",
14136
+ text: compact({
14137
+ results: paged,
14138
+ perRepo,
14139
+ searchMode: "bm25-cross-repo",
14140
+ group: a.group,
14141
+ total: merged.length,
14142
+ offset,
14143
+ limit: effectiveLimit,
14144
+ hasMore: offset + effectiveLimit < merged.length
14145
+ })
14146
+ }]
14147
+ };
14148
+ }
14149
+ const repoGraph = a.repo ? await (async () => {
14150
+ const registry = loadRegistry();
14151
+ const entry = registry.find((r) => r.name === a.repo || r.path === a.repo);
14152
+ if (!entry) return graph;
14153
+ const { DbManager: DbMgr } = await Promise.resolve().then(() => (init_db_manager(), db_manager_exports));
14154
+ const { loadGraphFromDB: loadG } = await Promise.resolve().then(() => (init_graph_from_db(), graph_from_db_exports));
14155
+ const { createKnowledgeGraph: createG } = await Promise.resolve().then(() => (init_knowledge_graph(), knowledge_graph_exports));
14156
+ const dbPath = path39.join(entry.path, ".code-intel", "graph.db");
14157
+ if (!fs39.existsSync(dbPath)) return graph;
14158
+ const db = new DbMgr(dbPath, true);
14159
+ await db.init();
14160
+ const g = createG();
14161
+ await loadG(g, db);
14162
+ db.close();
14163
+ return g;
14164
+ })() : graph;
13556
14165
  const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
13557
14166
  const fetchLimit = Math.min(offset + effectiveLimit, 500);
13558
- const { results: allResults, searchMode } = await hybridSearch(graph, query, fetchLimit, { vectorDbPath: vdbPath });
14167
+ const bm25 = !a.repo || a.repo === repoName ? bm25Resolver ? bm25Resolver() : null : null;
14168
+ const bm25Results = bm25 ? bm25.search(query, fetchLimit * 3) : void 0;
14169
+ const { results: allResults, searchMode } = await hybridSearch(repoGraph, query, fetchLimit, {
14170
+ vectorDbPath: vdbPath,
14171
+ bm25Results: bm25Results ?? void 0
14172
+ });
13559
14173
  const total = allResults.length;
13560
14174
  const results = allResults.slice(offset, offset + effectiveLimit);
13561
14175
  const hasMore = offset + effectiveLimit < total;
13562
14176
  const suggestNextTools = [];
13563
- const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
14177
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] === "true";
13564
14178
  if (suggestEnabled && results.length > 0) {
13565
14179
  const topName = results[0].name;
13566
14180
  suggestNextTools.push(
@@ -13571,15 +14185,16 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13571
14185
  return {
13572
14186
  content: [{
13573
14187
  type: "text",
13574
- text: JSON.stringify({
14188
+ text: compact({
13575
14189
  results,
13576
14190
  searchMode,
14191
+ repo: a.repo ?? repoName,
13577
14192
  total,
13578
14193
  offset,
13579
14194
  limit: effectiveLimit,
13580
14195
  hasMore,
13581
14196
  ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
13582
- }, null, 2)
14197
+ })
13583
14198
  }]
13584
14199
  };
13585
14200
  }
@@ -13601,7 +14216,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13601
14216
  file: graph.getNode(e.target)?.filePath
13602
14217
  }));
13603
14218
  const cluster = incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0];
13604
- const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
14219
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] === "true";
13605
14220
  const suggestNextTools = [];
13606
14221
  if (suggestEnabled) {
13607
14222
  const topCallerName = callers[0]?.name;
@@ -13613,7 +14228,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13613
14228
  return {
13614
14229
  content: [{
13615
14230
  type: "text",
13616
- text: JSON.stringify({
14231
+ text: compact({
13617
14232
  node: {
13618
14233
  id: node.id,
13619
14234
  kind: node.kind,
@@ -13636,7 +14251,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13636
14251
  cluster,
13637
14252
  content: node.content?.slice(0, 500),
13638
14253
  ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
13639
- }, null, 2)
14254
+ })
13640
14255
  }]
13641
14256
  };
13642
14257
  }
@@ -13644,7 +14259,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13644
14259
  case "blast_radius": {
13645
14260
  const target = a.target;
13646
14261
  const direction = a.direction ?? "both";
13647
- const maxHops = a.max_hops ?? 5;
14262
+ const maxHops = a.max_hops ?? 2;
13648
14263
  const node = findNodeByName(graph, target);
13649
14264
  if (!node) return { content: [{ type: "text", text: `Symbol "${target}" not found.` }] };
13650
14265
  const affected = /* @__PURE__ */ new Set();
@@ -13671,7 +14286,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13671
14286
  return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
13672
14287
  });
13673
14288
  const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
13674
- const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
14289
+ const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] === "true";
13675
14290
  const suggestNextTools = [];
13676
14291
  if (suggestEnabled) {
13677
14292
  const highestRiskSymbol = node.name;
@@ -13684,13 +14299,13 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13684
14299
  return {
13685
14300
  content: [{
13686
14301
  type: "text",
13687
- text: JSON.stringify({
14302
+ text: compact({
13688
14303
  target: node.name,
13689
14304
  affectedCount: affected.size,
13690
14305
  riskLevel: risk,
13691
14306
  affected: affectedDetails,
13692
14307
  ...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
13693
- }, null, 2)
14308
+ })
13694
14309
  }]
13695
14310
  };
13696
14311
  }
@@ -13698,7 +14313,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13698
14313
  case "file_symbols": {
13699
14314
  const filePath = a.file_path;
13700
14315
  const offset = a.offset ?? 0;
13701
- const effectiveLimit = Math.min(a.limit ?? 50, 500);
14316
+ const effectiveLimit = Math.min(a.limit ?? 10, 500);
13702
14317
  const allMatches = [];
13703
14318
  for (const node of graph.allNodes()) {
13704
14319
  if (node.filePath && node.filePath.includes(filePath)) {
@@ -13715,7 +14330,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13715
14330
  return {
13716
14331
  content: [{
13717
14332
  type: "text",
13718
- text: JSON.stringify({ symbols: matches, total, offset, limit: effectiveLimit, hasMore }, null, 2)
14333
+ text: compact({ symbols: matches, total, offset, limit: effectiveLimit, hasMore })
13719
14334
  }]
13720
14335
  };
13721
14336
  }
@@ -13756,7 +14371,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13756
14371
  return {
13757
14372
  content: [{
13758
14373
  type: "text",
13759
- text: JSON.stringify({ from: fromName, to: toName, hops: foundPath.length - 1, path: pathDetails }, null, 2)
14374
+ text: compact({ from: fromName, to: toName, hops: foundPath.length - 1, path: pathDetails })
13760
14375
  }]
13761
14376
  };
13762
14377
  }
@@ -13764,7 +14379,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13764
14379
  case "list_exports": {
13765
14380
  const kindFilter = a.kind;
13766
14381
  const offset = a.offset ?? 0;
13767
- const effectiveLimit = Math.min(a.limit ?? 50, 500);
14382
+ const effectiveLimit = Math.min(a.limit ?? 10, 500);
13768
14383
  const allExports = [];
13769
14384
  for (const node of graph.allNodes()) {
13770
14385
  if (!node.exported) continue;
@@ -13777,7 +14392,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13777
14392
  return {
13778
14393
  content: [{
13779
14394
  type: "text",
13780
- text: JSON.stringify({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore }, null, 2)
14395
+ text: compact({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore })
13781
14396
  }]
13782
14397
  };
13783
14398
  }
@@ -13789,12 +14404,12 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13789
14404
  routes.push({ name: node.name, filePath: node.filePath, startLine: node.startLine });
13790
14405
  }
13791
14406
  }
13792
- return { content: [{ type: "text", text: JSON.stringify(routes, null, 2) }] };
14407
+ return { content: [{ type: "text", text: compact(routes) }] };
13793
14408
  }
13794
14409
  // ── clusters ───────────────────────────────────────────────────────────
13795
14410
  case "clusters": {
13796
14411
  const offset = a.offset ?? 0;
13797
- const effectiveLimit = Math.min(a.limit ?? 50, 500);
14412
+ const effectiveLimit = Math.min(a.limit ?? 10, 500);
13798
14413
  const allClusters = [];
13799
14414
  for (const node of graph.allNodes()) {
13800
14415
  if (node.kind === "cluster") {
@@ -13821,14 +14436,14 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13821
14436
  return {
13822
14437
  content: [{
13823
14438
  type: "text",
13824
- text: JSON.stringify({ clusters, total, offset, limit: effectiveLimit, hasMore }, null, 2)
14439
+ text: compact({ clusters, total, offset, limit: effectiveLimit, hasMore })
13825
14440
  }]
13826
14441
  };
13827
14442
  }
13828
14443
  // ── flows ──────────────────────────────────────────────────────────────
13829
14444
  case "flows": {
13830
14445
  const offset = a.offset ?? 0;
13831
- const effectiveLimit = Math.min(a.limit ?? 50, 500);
14446
+ const effectiveLimit = Math.min(a.limit ?? 10, 500);
13832
14447
  const allFlows = [];
13833
14448
  for (const node of graph.allNodes()) {
13834
14449
  if (node.kind === "flow") {
@@ -13848,7 +14463,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13848
14463
  return {
13849
14464
  content: [{
13850
14465
  type: "text",
13851
- text: JSON.stringify({ flows, total, offset, limit: effectiveLimit, hasMore }, null, 2)
14466
+ text: compact({ flows, total, offset, limit: effectiveLimit, hasMore })
13852
14467
  }]
13853
14468
  };
13854
14469
  }
@@ -13916,14 +14531,14 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13916
14531
  return {
13917
14532
  content: [{
13918
14533
  type: "text",
13919
- text: JSON.stringify({
14534
+ text: compact({
13920
14535
  baseRef,
13921
14536
  changedFiles: changedFiles.map((f) => f.filePath),
13922
14537
  directlyChangedSymbols: changedSymbols,
13923
14538
  transitivelyAffectedSymbols: affectedSymbols,
13924
14539
  totalAffected: allAffected.size,
13925
14540
  riskLevel: risk
13926
- }, null, 2)
14541
+ })
13927
14542
  }]
13928
14543
  };
13929
14544
  }
@@ -13931,14 +14546,14 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13931
14546
  case "query": {
13932
14547
  const gqlInput = a.gql;
13933
14548
  if (!gqlInput) {
13934
- return { content: [{ type: "text", text: JSON.stringify({ error: "Missing required parameter: gql" }) }], isError: true };
14549
+ return { content: [{ type: "text", text: compact({ error: "Missing required parameter: gql" }) }], isError: true };
13935
14550
  }
13936
14551
  const { parseGQL: parseGQL2, isGQLParseError: isGQLParseError2 } = await Promise.resolve().then(() => (init_gql_parser(), gql_parser_exports));
13937
14552
  const { executeGQL: executeGQL2 } = await Promise.resolve().then(() => (init_gql_executor(), gql_executor_exports));
13938
14553
  const ast = parseGQL2(gqlInput);
13939
14554
  if (isGQLParseError2(ast)) {
13940
14555
  return {
13941
- content: [{ type: "text", text: JSON.stringify({ error: `GQL parse error: ${ast.message}`, pos: ast.pos, expected: ast.expected, got: ast.got }) }],
14556
+ content: [{ type: "text", text: compact({ error: `GQL parse error: ${ast.message}`, pos: ast.pos, expected: ast.expected, got: ast.got }) }],
13942
14557
  isError: true
13943
14558
  };
13944
14559
  }
@@ -13949,7 +14564,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13949
14564
  return {
13950
14565
  content: [{
13951
14566
  type: "text",
13952
- text: JSON.stringify({
14567
+ text: compact({
13953
14568
  nodes: result.nodes,
13954
14569
  edges: result.edges,
13955
14570
  groups: result.groups,
@@ -13957,7 +14572,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13957
14572
  executionTimeMs: result.executionTimeMs,
13958
14573
  truncated: result.truncated,
13959
14574
  totalCount: result.totalCount
13960
- }, null, 2)
14575
+ })
13961
14576
  }]
13962
14577
  };
13963
14578
  }
@@ -13971,7 +14586,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13971
14586
  for (const node of graph.allNodes()) {
13972
14587
  if (node.name === nameMatch[1]) results.push(node);
13973
14588
  }
13974
- return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, results }, null, 2) }] };
14589
+ return { content: [{ type: "text", text: compact({ deprecation: deprecationWarning, results }) }] };
13975
14590
  }
13976
14591
  const kindMatch = q?.match(/:\s*(\w+)/);
13977
14592
  if (kindMatch) {
@@ -13980,9 +14595,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13980
14595
  if (node.kind === kindMatch[1]) results.push(node);
13981
14596
  if (results.length >= 50) break;
13982
14597
  }
13983
- return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, results }, null, 2) }] };
14598
+ return { content: [{ type: "text", text: compact({ deprecation: deprecationWarning, results }) }] };
13984
14599
  }
13985
- return { content: [{ type: "text", text: JSON.stringify({ deprecation: deprecationWarning, error: "Query not recognized. Use name='X' or :kind syntax. Or use the query tool with GQL instead." }) }] };
14600
+ return { content: [{ type: "text", text: compact({ deprecation: deprecationWarning, error: "Query not recognized. Use name='X' or :kind syntax. Or use the query tool with GQL instead." }) }] };
13986
14601
  }
13987
14602
  // ── group_list ─────────────────────────────────────────────────────────
13988
14603
  case "group_list": {
@@ -13990,16 +14605,14 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
13990
14605
  if (groupName) {
13991
14606
  const group = loadGroup(groupName);
13992
14607
  if (!group) return { content: [{ type: "text", text: `Group "${groupName}" not found.` }] };
13993
- return { content: [{ type: "text", text: JSON.stringify(group, null, 2) }] };
14608
+ return { content: [{ type: "text", text: compact(group) }] };
13994
14609
  }
13995
14610
  const groups = listGroups();
13996
14611
  return {
13997
14612
  content: [{
13998
14613
  type: "text",
13999
- text: JSON.stringify(
14000
- groups.map((g) => ({ name: g.name, createdAt: g.createdAt, lastSync: g.lastSync, memberCount: g.members.length, members: g.members })),
14001
- null,
14002
- 2
14614
+ text: compact(
14615
+ groups.map((g) => ({ name: g.name, createdAt: g.createdAt, lastSync: g.lastSync, memberCount: g.members.length, members: g.members }))
14003
14616
  )
14004
14617
  }]
14005
14618
  };
@@ -14017,14 +14630,14 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14017
14630
  return {
14018
14631
  content: [{
14019
14632
  type: "text",
14020
- text: JSON.stringify({
14633
+ text: compact({
14021
14634
  groupName: result.groupName,
14022
14635
  syncedAt: result.syncedAt,
14023
14636
  memberCount: result.memberCount,
14024
14637
  contractCount: result.contracts.length,
14025
14638
  linkCount: result.links.length,
14026
14639
  topLinks: result.links.slice(0, 20)
14027
- }, null, 2)
14640
+ })
14028
14641
  }]
14029
14642
  };
14030
14643
  }
@@ -14044,7 +14657,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14044
14657
  return {
14045
14658
  content: [{
14046
14659
  type: "text",
14047
- text: JSON.stringify({ syncedAt: result.syncedAt, contracts, links }, null, 2)
14660
+ text: compact({ syncedAt: result.syncedAt, contracts, links })
14048
14661
  }]
14049
14662
  };
14050
14663
  }
@@ -14059,7 +14672,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14059
14672
  return {
14060
14673
  content: [{
14061
14674
  type: "text",
14062
- text: JSON.stringify({ query, merged, perRepo }, null, 2)
14675
+ text: compact({ query, merged, perRepo })
14063
14676
  }]
14064
14677
  };
14065
14678
  }
@@ -14091,12 +14704,12 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14091
14704
  return {
14092
14705
  content: [{
14093
14706
  type: "text",
14094
- text: JSON.stringify({
14707
+ text: compact({
14095
14708
  group: groupName,
14096
14709
  lastSync: group.lastSync ?? null,
14097
14710
  syncAgeMinutes: syncAge,
14098
14711
  members: memberStatus
14099
- }, null, 2)
14712
+ })
14100
14713
  }]
14101
14714
  };
14102
14715
  }
@@ -14105,11 +14718,11 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14105
14718
  const fromName = a.from;
14106
14719
  const toName = a.to;
14107
14720
  const result = explainRelationship(graph, fromName, toName);
14108
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
14721
+ return { content: [{ type: "text", text: compact(result) }] };
14109
14722
  }
14110
14723
  // ── pr_impact ──────────────────────────────────────────────────────────
14111
14724
  case "pr_impact": {
14112
- const maxHops = a.maxHops ?? 5;
14725
+ const maxHops = a.maxHops ?? 2;
14113
14726
  let changedFiles = a.changedFiles ?? [];
14114
14727
  if (a.diff && typeof a.diff === "string") {
14115
14728
  const diffFiles = parseDiffFiles(a.diff);
@@ -14119,37 +14732,37 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14119
14732
  return {
14120
14733
  content: [{
14121
14734
  type: "text",
14122
- text: JSON.stringify({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
14735
+ text: compact({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
14123
14736
  }]
14124
14737
  };
14125
14738
  }
14126
14739
  const result = computePRImpact(graph, changedFiles, maxHops);
14127
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
14740
+ return { content: [{ type: "text", text: compact(result) }] };
14128
14741
  }
14129
14742
  // ── similar_symbols ────────────────────────────────────────────────────
14130
14743
  case "similar_symbols": {
14131
14744
  const symbolName = a.symbol;
14132
14745
  const limit = a.limit ?? 10;
14133
14746
  const result = findSimilarSymbols(graph, symbolName, limit);
14134
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
14747
+ return { content: [{ type: "text", text: compact(result) }] };
14135
14748
  }
14136
14749
  // ── health_report ──────────────────────────────────────────────────────
14137
14750
  case "health_report": {
14138
14751
  const scope = a.scope ?? ".";
14139
14752
  const result = computeHealthReport(graph, scope);
14140
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
14753
+ return { content: [{ type: "text", text: compact(result) }] };
14141
14754
  }
14142
14755
  // ── suggest_tests ──────────────────────────────────────────────────────
14143
14756
  case "suggest_tests": {
14144
14757
  const sym = a.symbol;
14145
14758
  const result = suggestTests(graph, sym);
14146
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
14759
+ return { content: [{ type: "text", text: compact(result) }] };
14147
14760
  }
14148
14761
  // ── cluster_summary ────────────────────────────────────────────────────
14149
14762
  case "cluster_summary": {
14150
14763
  const cluster = a.cluster;
14151
14764
  const result = summarizeCluster(graph, cluster);
14152
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
14765
+ return { content: [{ type: "text", text: compact(result) }] };
14153
14766
  }
14154
14767
  case "deprecated_usage": {
14155
14768
  const scope = a.scope;
@@ -14157,7 +14770,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14157
14770
  const detector = new DeprecatedDetector2();
14158
14771
  detector.tagDeprecated(graph);
14159
14772
  const findings = detector.detect(graph, scope);
14160
- return { content: [{ type: "text", text: JSON.stringify({ findings, total: findings.length }, null, 2) }] };
14773
+ return { content: [{ type: "text", text: compact({ findings, total: findings.length }) }] };
14161
14774
  }
14162
14775
  // ── complexity_hotspots ────────────────────────────────────────────────
14163
14776
  case "complexity_hotspots": {
@@ -14165,7 +14778,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14165
14778
  const scope = a.scope;
14166
14779
  const limit = typeof a.limit === "number" ? a.limit : 20;
14167
14780
  const hotspots = computeComplexity2(graph, scope).slice(0, limit);
14168
- return { content: [{ type: "text", text: JSON.stringify({ hotspots, total: hotspots.length }, null, 2) }] };
14781
+ return { content: [{ type: "text", text: compact({ hotspots, total: hotspots.length }) }] };
14169
14782
  }
14170
14783
  // ── coverage_gaps ──────────────────────────────────────────────────────
14171
14784
  case "coverage_gaps": {
@@ -14177,12 +14790,12 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14177
14790
  return {
14178
14791
  content: [{
14179
14792
  type: "text",
14180
- text: JSON.stringify({
14793
+ text: compact({
14181
14794
  untestedByRisk,
14182
14795
  coveragePct: summary.coveragePct,
14183
14796
  totalExported: summary.totalExported,
14184
14797
  testedExported: summary.testedExported
14185
- }, null, 2)
14798
+ })
14186
14799
  }]
14187
14800
  };
14188
14801
  }
@@ -14193,7 +14806,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14193
14806
  const scope = a.scope;
14194
14807
  const includeTestFiles = a.includeTestFiles ?? false;
14195
14808
  const findings = scanner.scan(graph, { scope, includeTestFiles });
14196
- return { content: [{ type: "text", text: JSON.stringify({ findings, total: findings.length }, null, 2) }] };
14809
+ return { content: [{ type: "text", text: compact({ findings, total: findings.length }) }] };
14197
14810
  }
14198
14811
  // ── vulnerability_scan ─────────────────────────────────────────────────
14199
14812
  case "vulnerability_scan": {
@@ -14206,7 +14819,7 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
14206
14819
  const minRank = sevRank[minSev] ?? 1;
14207
14820
  let findings = detector.detect(graph, { scope, types });
14208
14821
  findings = findings.filter((f) => (sevRank[f.severity] ?? 1) >= minRank);
14209
- return { content: [{ type: "text", text: JSON.stringify({ findings, total: findings.length }, null, 2) }] };
14822
+ return { content: [{ type: "text", text: compact({ findings, total: findings.length }) }] };
14210
14823
  }
14211
14824
  default:
14212
14825
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
@@ -14227,21 +14840,21 @@ function registerResources(server, graph, repoName) {
14227
14840
  for (const node of graph.allNodes()) {
14228
14841
  kindCounts[node.kind] = (kindCounts[node.kind] ?? 0) + 1;
14229
14842
  }
14230
- return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ repo: repoName, stats: graph.size, nodeCounts: kindCounts }) }] };
14843
+ return { contents: [{ uri, mimeType: "application/json", text: compact({ repo: repoName, stats: graph.size, nodeCounts: kindCounts }) }] };
14231
14844
  }
14232
14845
  if (uri.endsWith("/clusters")) {
14233
14846
  const clusters = [];
14234
14847
  for (const node of graph.allNodes()) {
14235
14848
  if (node.kind === "cluster") clusters.push({ id: node.id, name: node.name, memberCount: node.metadata?.memberCount });
14236
14849
  }
14237
- return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(clusters) }] };
14850
+ return { contents: [{ uri, mimeType: "application/json", text: compact(clusters) }] };
14238
14851
  }
14239
14852
  if (uri.endsWith("/flows")) {
14240
14853
  const flows = [];
14241
14854
  for (const node of graph.allNodes()) {
14242
14855
  if (node.kind === "flow") flows.push({ id: node.id, name: node.name, steps: node.metadata?.steps, entryPoint: node.metadata?.entryPoint });
14243
14856
  }
14244
- return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(flows) }] };
14857
+ return { contents: [{ uri, mimeType: "application/json", text: compact(flows) }] };
14245
14858
  }
14246
14859
  throw new Error(`Unknown resource: ${uri}`);
14247
14860
  });
@@ -14297,8 +14910,8 @@ async function writeSkillFiles(graph, workspaceRoot, projectName) {
14297
14910
  const outputDir = path39.join(workspaceRoot, ".claude", "skills", "code-intel");
14298
14911
  const areas = buildAreaMap(graph, workspaceRoot);
14299
14912
  if (areas.length === 0) return { skills: [], outputDir };
14300
- fs38.rmSync(outputDir, { recursive: true, force: true });
14301
- fs38.mkdirSync(outputDir, { recursive: true });
14913
+ fs39.rmSync(outputDir, { recursive: true, force: true });
14914
+ fs39.mkdirSync(outputDir, { recursive: true });
14302
14915
  const skills = [];
14303
14916
  const usedNames = /* @__PURE__ */ new Set();
14304
14917
  for (const area of areas) {
@@ -14306,8 +14919,8 @@ async function writeSkillFiles(graph, workspaceRoot, projectName) {
14306
14919
  usedNames.add(kebab);
14307
14920
  const content = renderSkill(area, projectName, kebab);
14308
14921
  const dir = path39.join(outputDir, kebab);
14309
- fs38.mkdirSync(dir, { recursive: true });
14310
- fs38.writeFileSync(path39.join(dir, "SKILL.md"), content, "utf-8");
14922
+ fs39.mkdirSync(dir, { recursive: true });
14923
+ fs39.writeFileSync(path39.join(dir, "SKILL.md"), content, "utf-8");
14311
14924
  skills.push({ name: kebab, label: area.label, symbolCount: area.nodes.length, fileCount: area.files.size });
14312
14925
  }
14313
14926
  return { skills, outputDir };
@@ -14490,14 +15103,22 @@ function writeContextFiles(workspaceRoot, projectName, stats, skills) {
14490
15103
  upsertFile(path39.join(workspaceRoot, "AGENTS.md"), block, "AGENTS.md");
14491
15104
  upsertFile(path39.join(workspaceRoot, "CLAUDE.md"), block, "CLAUDE.md");
14492
15105
  const githubDir = path39.join(workspaceRoot, ".github");
14493
- if (!fs38.existsSync(githubDir)) fs38.mkdirSync(githubDir, { recursive: true });
15106
+ if (!fs39.existsSync(githubDir)) fs39.mkdirSync(githubDir, { recursive: true });
14494
15107
  upsertFile(path39.join(githubDir, "copilot-instructions.md"), block, "copilot-instructions.md");
14495
15108
  const cursorDir = path39.join(workspaceRoot, ".cursor", "rules");
14496
- if (!fs38.existsSync(cursorDir)) fs38.mkdirSync(cursorDir, { recursive: true });
15109
+ if (!fs39.existsSync(cursorDir)) fs39.mkdirSync(cursorDir, { recursive: true });
14497
15110
  upsertFile(path39.join(cursorDir, "code-intel.mdc"), block, "code-intel.mdc");
14498
15111
  const kiroDir = path39.join(workspaceRoot, ".kiro", "steering");
14499
- if (!fs38.existsSync(kiroDir)) fs38.mkdirSync(kiroDir, { recursive: true });
15112
+ if (!fs39.existsSync(kiroDir)) fs39.mkdirSync(kiroDir, { recursive: true });
14500
15113
  upsertFile(path39.join(kiroDir, "code-intel.md"), block, "code-intel.md");
15114
+ upsertFile(path39.join(workspaceRoot, ".clinerules"), block, ".clinerules");
15115
+ upsertFile(path39.join(workspaceRoot, ".windsurfrules"), block, ".windsurfrules");
15116
+ const kilocodeDir = path39.join(workspaceRoot, ".kilocode", "rules");
15117
+ if (!fs39.existsSync(kilocodeDir)) fs39.mkdirSync(kilocodeDir, { recursive: true });
15118
+ upsertFile(path39.join(kilocodeDir, "code-intel-rules.md"), block, "code-intel-rules.md");
15119
+ const agentsDir = path39.join(workspaceRoot, ".agents", "rules");
15120
+ if (!fs39.existsSync(agentsDir)) fs39.mkdirSync(agentsDir, { recursive: true });
15121
+ upsertFile(path39.join(agentsDir, "code-intel-rules.md"), block, "code-intel-rules.md");
14501
15122
  }
14502
15123
  function buildBlock(projectName, stats, skills) {
14503
15124
  const skillTableRows = skills.map(
@@ -14523,6 +15144,14 @@ ${skills.map((s) => `- Working in **${s.label}**? \u2192 Load \`.claude/skills/c
14523
15144
  > \u26A0 This section is auto-managed by \`code-intel analyze\`. Do **not** edit between the markers \u2014 your changes will be overwritten.
14524
15145
  > Add your own notes below the \`${BLOCK_END}\` marker.
14525
15146
 
15147
+ <!-- TOOL POLICY: ENFORCED -->
15148
+ FORBIDDEN: grep, rg, find, cat, sed, ls (for symbol/code discovery)
15149
+ REQUIRED BEFORE ANY CODE ACTION:
15150
+ 1. code-intel search "<concept>" \u2190 replaces grep/rg/find
15151
+ 2. code-intel inspect <symbol> \u2190 replaces cat + manual file reading
15152
+ 3. code-intel impact <symbol> \u2190 replaces manual caller tracing
15153
+ <!-- /TOOL POLICY -->
15154
+
14526
15155
  Indexed: **${stats.nodes.toLocaleString()} nodes** | **${stats.edges.toLocaleString()} edges** | **${stats.files} files** | analyzed in ${(stats.duration / 1e3).toFixed(1)}s
14527
15156
 
14528
15157
  > Index stale? Re-run: \`code-intel analyze\`
@@ -14558,7 +15187,9 @@ These rules apply to **every coding agent or AI assistant** working in this repo
14558
15187
  ## Never Do
14559
15188
 
14560
15189
  - NEVER ignore impact warnings \u2014 always report blast radius to the user.
14561
- - NEVER skip \`code-intel search\` before grepping or opening files.
15190
+ - **STOP** \u2014 do not call grep, rg, find, cat, sed, or read a file cold.
15191
+ Always run \`code-intel search "<concept>"\` first.
15192
+ Violating this wastes ~3,000 tokens per lookup and degrades session quality.
14562
15193
  - NEVER make changes to a symbol with \u2265 5 callers without running \`code-intel impact\` first.
14563
15194
  - NEVER use find-and-replace for symbol renames.
14564
15195
 
@@ -14634,7 +15265,7 @@ ${skillTable}
14634
15265
  ${BLOCK_END}`;
14635
15266
  }
14636
15267
  function upsertFile(filePath, block, fileName) {
14637
- if (!fs38.existsSync(filePath)) {
15268
+ if (!fs39.existsSync(filePath)) {
14638
15269
  const newContent = [
14639
15270
  `# ${fileName}`,
14640
15271
  "",
@@ -14645,17 +15276,17 @@ function upsertFile(filePath, block, fileName) {
14645
15276
  "<!-- Add your own custom notes below this line. They will never be overwritten by code-intel. -->",
14646
15277
  ""
14647
15278
  ].join("\n");
14648
- fs38.writeFileSync(filePath, newContent, "utf-8");
15279
+ fs39.writeFileSync(filePath, newContent, "utf-8");
14649
15280
  return;
14650
15281
  }
14651
- const existing = fs38.readFileSync(filePath, "utf-8");
15282
+ const existing = fs39.readFileSync(filePath, "utf-8");
14652
15283
  const startIdx = findLineMarker(existing, BLOCK_START);
14653
15284
  const endIdx = findLineMarker(existing, BLOCK_END, startIdx === -1 ? 0 : startIdx);
14654
15285
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
14655
15286
  const before = existing.slice(0, startIdx);
14656
15287
  const after = existing.slice(endIdx + BLOCK_END.length);
14657
15288
  const updated = (before + block + after).trimEnd() + "\n";
14658
- fs38.writeFileSync(filePath, updated, "utf-8");
15289
+ fs39.writeFileSync(filePath, updated, "utf-8");
14659
15290
  return;
14660
15291
  }
14661
15292
  const appended = [
@@ -14668,7 +15299,7 @@ function upsertFile(filePath, block, fileName) {
14668
15299
  block,
14669
15300
  ""
14670
15301
  ].join("\n");
14671
- fs38.writeFileSync(filePath, appended, "utf-8");
15302
+ fs39.writeFileSync(filePath, appended, "utf-8");
14672
15303
  }
14673
15304
  function findLineMarker(content, marker, startFrom = 0) {
14674
15305
  let idx = content.indexOf(marker, startFrom);
@@ -14717,7 +15348,7 @@ function filterChangedByMtime(allFilePaths, workspaceRoot, storedMtimes) {
14717
15348
  continue;
14718
15349
  }
14719
15350
  try {
14720
- const { mtimeMs } = fs38.statSync(absPath);
15351
+ const { mtimeMs } = fs39.statSync(absPath);
14721
15352
  if (mtimeMs > stored) changed.push(absPath);
14722
15353
  } catch {
14723
15354
  changed.push(absPath);
@@ -14729,7 +15360,7 @@ function buildMtimeSnapshot(filePaths, workspaceRoot) {
14729
15360
  const snap = {};
14730
15361
  for (const absPath of filePaths) {
14731
15362
  try {
14732
- const { mtimeMs } = fs38.statSync(absPath);
15363
+ const { mtimeMs } = fs39.statSync(absPath);
14733
15364
  snap[path39.relative(workspaceRoot, absPath)] = mtimeMs;
14734
15365
  } catch {
14735
15366
  }
@@ -14770,10 +15401,10 @@ function expandGlob(root, pattern) {
14770
15401
  const parts = pattern.replace(/\/\*\*?$/, "").split("/").filter(Boolean);
14771
15402
  if (parts.length === 0) return [];
14772
15403
  const dir = path39.join(root, ...parts);
14773
- if (!fs38.existsSync(dir)) return [];
14774
- return fs38.readdirSync(dir).map((entry) => path39.join(dir, entry)).filter((p) => {
15404
+ if (!fs39.existsSync(dir)) return [];
15405
+ return fs39.readdirSync(dir).map((entry) => path39.join(dir, entry)).filter((p) => {
14775
15406
  try {
14776
- return fs38.statSync(p).isDirectory();
15407
+ return fs39.statSync(p).isDirectory();
14777
15408
  } catch {
14778
15409
  return false;
14779
15410
  }
@@ -14785,9 +15416,9 @@ function resolvePackages(root, patterns) {
14785
15416
  const dirs = expandGlob(root, pattern);
14786
15417
  for (const dir of dirs) {
14787
15418
  const pkgJsonPath = path39.join(dir, "package.json");
14788
- if (!fs38.existsSync(pkgJsonPath)) continue;
15419
+ if (!fs39.existsSync(pkgJsonPath)) continue;
14789
15420
  try {
14790
- const pkgJson = JSON.parse(fs38.readFileSync(pkgJsonPath, "utf-8"));
15421
+ const pkgJson = JSON.parse(fs39.readFileSync(pkgJsonPath, "utf-8"));
14791
15422
  const name = pkgJson.name ?? path39.basename(dir);
14792
15423
  packages.push({ name, path: dir });
14793
15424
  } catch {
@@ -14798,12 +15429,12 @@ function resolvePackages(root, patterns) {
14798
15429
  }
14799
15430
  async function detectWorkspace(root) {
14800
15431
  const turboJsonPath = path39.join(root, "turbo.json");
14801
- if (fs38.existsSync(turboJsonPath)) {
15432
+ if (fs39.existsSync(turboJsonPath)) {
14802
15433
  let patterns = [];
14803
15434
  const pkgJsonPath = path39.join(root, "package.json");
14804
- if (fs38.existsSync(pkgJsonPath)) {
15435
+ if (fs39.existsSync(pkgJsonPath)) {
14805
15436
  try {
14806
- const pkgJson = JSON.parse(fs38.readFileSync(pkgJsonPath, "utf-8"));
15437
+ const pkgJson = JSON.parse(fs39.readFileSync(pkgJsonPath, "utf-8"));
14807
15438
  if (pkgJson.workspaces) {
14808
15439
  patterns = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces.packages;
14809
15440
  }
@@ -14812,15 +15443,15 @@ async function detectWorkspace(root) {
14812
15443
  }
14813
15444
  if (patterns.length === 0) {
14814
15445
  const fallbackDir = path39.join(root, "packages");
14815
- if (fs38.existsSync(fallbackDir)) patterns = ["packages/*"];
15446
+ if (fs39.existsSync(fallbackDir)) patterns = ["packages/*"];
14816
15447
  }
14817
15448
  const packages = resolvePackages(root, patterns);
14818
15449
  return { type: "turborepo", root, packages };
14819
15450
  }
14820
15451
  const npmPkgJsonPath = path39.join(root, "package.json");
14821
- if (fs38.existsSync(npmPkgJsonPath)) {
15452
+ if (fs39.existsSync(npmPkgJsonPath)) {
14822
15453
  try {
14823
- const pkgJson = JSON.parse(fs38.readFileSync(npmPkgJsonPath, "utf-8"));
15454
+ const pkgJson = JSON.parse(fs39.readFileSync(npmPkgJsonPath, "utf-8"));
14824
15455
  if (pkgJson.workspaces) {
14825
15456
  const patterns = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces.packages;
14826
15457
  const packages = resolvePackages(root, patterns);
@@ -14830,10 +15461,10 @@ async function detectWorkspace(root) {
14830
15461
  }
14831
15462
  }
14832
15463
  const pnpmYamlPath = path39.join(root, "pnpm-workspace.yaml");
14833
- if (fs38.existsSync(pnpmYamlPath)) {
15464
+ if (fs39.existsSync(pnpmYamlPath)) {
14834
15465
  const patterns = [];
14835
15466
  try {
14836
- const content = fs38.readFileSync(pnpmYamlPath, "utf-8");
15467
+ const content = fs39.readFileSync(pnpmYamlPath, "utf-8");
14837
15468
  let inPackages = false;
14838
15469
  for (const line of content.split("\n")) {
14839
15470
  if (/^packages\s*:/.test(line)) {
@@ -14854,13 +15485,13 @@ async function detectWorkspace(root) {
14854
15485
  return { type: "pnpm", root, packages };
14855
15486
  }
14856
15487
  const nxJsonPath = path39.join(root, "nx.json");
14857
- if (fs38.existsSync(nxJsonPath)) {
15488
+ if (fs39.existsSync(nxJsonPath)) {
14858
15489
  const packages = [];
14859
15490
  const scanForProjects = (dir, depth) => {
14860
15491
  if (depth > 2) return;
14861
15492
  let entries;
14862
15493
  try {
14863
- entries = fs38.readdirSync(dir);
15494
+ entries = fs39.readdirSync(dir);
14864
15495
  } catch {
14865
15496
  return;
14866
15497
  }
@@ -14868,14 +15499,14 @@ async function detectWorkspace(root) {
14868
15499
  if (entry === "node_modules" || entry.startsWith(".")) continue;
14869
15500
  const fullPath = path39.join(dir, entry);
14870
15501
  try {
14871
- if (!fs38.statSync(fullPath).isDirectory()) continue;
15502
+ if (!fs39.statSync(fullPath).isDirectory()) continue;
14872
15503
  } catch {
14873
15504
  continue;
14874
15505
  }
14875
15506
  const projectJsonPath = path39.join(fullPath, "project.json");
14876
- if (fs38.existsSync(projectJsonPath)) {
15507
+ if (fs39.existsSync(projectJsonPath)) {
14877
15508
  try {
14878
- const proj = JSON.parse(fs38.readFileSync(projectJsonPath, "utf-8"));
15509
+ const proj = JSON.parse(fs39.readFileSync(projectJsonPath, "utf-8"));
14879
15510
  const name = proj.name ?? path39.basename(fullPath);
14880
15511
  packages.push({ name, path: fullPath });
14881
15512
  } catch {
@@ -14984,9 +15615,9 @@ var MigrationRunner = class {
14984
15615
  autoBackupBeforeMigration() {
14985
15616
  try {
14986
15617
  const dbFile = this.db.name;
14987
- if (!dbFile || !fs38.existsSync(dbFile)) return;
14988
- const backupDir = path39.join(os13.homedir(), ".code-intel", "backups", "pre-migration");
14989
- fs38.mkdirSync(backupDir, { recursive: true });
15618
+ if (!dbFile || !fs39.existsSync(dbFile)) return;
15619
+ const backupDir = path39.join(os18.homedir(), ".code-intel", "backups", "pre-migration");
15620
+ fs39.mkdirSync(backupDir, { recursive: true });
14990
15621
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
14991
15622
  const baseName = path39.basename(dbFile, ".db");
14992
15623
  const backupPath = path39.join(backupDir, `${baseName}-pre-migration-${ts}.db`);
@@ -14994,7 +15625,7 @@ var MigrationRunner = class {
14994
15625
  this.db.pragma("wal_checkpoint(TRUNCATE)");
14995
15626
  } catch {
14996
15627
  }
14997
- fs38.copyFileSync(dbFile, backupPath);
15628
+ fs39.copyFileSync(dbFile, backupPath);
14998
15629
  } catch {
14999
15630
  }
15000
15631
  }
@@ -15141,10 +15772,10 @@ init_tracing();
15141
15772
  init_init_wizard();
15142
15773
  init_config_manager();
15143
15774
  init_codes();
15144
- var GLOBAL_DIR3 = path39.join(os13.homedir(), ".code-intel");
15775
+ var GLOBAL_DIR3 = path39.join(os18.homedir(), ".code-intel");
15145
15776
  function loadRepoPaths() {
15146
15777
  try {
15147
- const data = fs38.readFileSync(path39.join(GLOBAL_DIR3, "repos.json"), "utf-8");
15778
+ const data = fs39.readFileSync(path39.join(GLOBAL_DIR3, "repos.json"), "utf-8");
15148
15779
  const repos = JSON.parse(data);
15149
15780
  return repos.map((r) => r.path);
15150
15781
  } catch {
@@ -15154,7 +15785,7 @@ function loadRepoPaths() {
15154
15785
  function loadGroupNames() {
15155
15786
  const groupsDir = path39.join(GLOBAL_DIR3, "groups");
15156
15787
  try {
15157
- return fs38.readdirSync(groupsDir).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
15788
+ return fs39.readdirSync(groupsDir).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
15158
15789
  } catch {
15159
15790
  return [];
15160
15791
  }
@@ -15415,10 +16046,10 @@ function autoInstallCompletion() {
15415
16046
  }
15416
16047
  console.log(` Detected shell: ${shell}`);
15417
16048
  if (shell === "fish") {
15418
- const dir = path39.join(os13.homedir(), ".config", "fish", "completions");
16049
+ const dir = path39.join(os18.homedir(), ".config", "fish", "completions");
15419
16050
  const dest = path39.join(dir, "code-intel.fish");
15420
- fs38.mkdirSync(dir, { recursive: true });
15421
- fs38.writeFileSync(dest, fishCompletion(), "utf-8");
16051
+ fs39.mkdirSync(dir, { recursive: true });
16052
+ fs39.writeFileSync(dest, fishCompletion(), "utf-8");
15422
16053
  console.log(` \u2705 Fish completion installed \u2192 ${dest}
15423
16054
  `);
15424
16055
  return;
@@ -15428,15 +16059,15 @@ source <(code-intel completion zsh)
15428
16059
  ` : `
15429
16060
  source <(code-intel completion bash)
15430
16061
  `;
15431
- const rcFile = shell === "zsh" ? path39.join(os13.homedir(), ".zshrc") : path39.join(os13.homedir(), ".bashrc");
16062
+ const rcFile = shell === "zsh" ? path39.join(os18.homedir(), ".zshrc") : path39.join(os18.homedir(), ".bashrc");
15432
16063
  try {
15433
- const existing = fs38.existsSync(rcFile) ? fs38.readFileSync(rcFile, "utf-8") : "";
16064
+ const existing = fs39.existsSync(rcFile) ? fs39.readFileSync(rcFile, "utf-8") : "";
15434
16065
  if (existing.includes("code-intel completion")) {
15435
16066
  console.log(` \u2139 Completion already configured in ${rcFile}
15436
16067
  `);
15437
16068
  return;
15438
16069
  }
15439
- fs38.appendFileSync(rcFile, script, "utf-8");
16070
+ fs39.appendFileSync(rcFile, script, "utf-8");
15440
16071
  console.log(` \u2705 ${shell} completion added to ${rcFile}`);
15441
16072
  console.log(` Restart your shell or run: source ${rcFile}
15442
16073
  `);
@@ -15455,20 +16086,20 @@ function generateCompletion(shell) {
15455
16086
  return fishCompletion();
15456
16087
  }
15457
16088
  }
15458
- var GLOBAL_DIR4 = path39.join(os13.homedir(), ".code-intel");
16089
+ var GLOBAL_DIR4 = path39.join(os18.homedir(), ".code-intel");
15459
16090
  var META_PATH = path39.join(GLOBAL_DIR4, "update-meta.json");
15460
16091
  var PACKAGE_NAME = "code-intel";
15461
16092
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
15462
16093
  function loadMeta() {
15463
16094
  try {
15464
- return JSON.parse(fs38.readFileSync(META_PATH, "utf-8"));
16095
+ return JSON.parse(fs39.readFileSync(META_PATH, "utf-8"));
15465
16096
  } catch {
15466
16097
  return null;
15467
16098
  }
15468
16099
  }
15469
16100
  function saveMeta(meta) {
15470
- fs38.mkdirSync(GLOBAL_DIR4, { recursive: true });
15471
- fs38.writeFileSync(META_PATH, JSON.stringify(meta, null, 2) + "\n", "utf-8");
16101
+ fs39.mkdirSync(GLOBAL_DIR4, { recursive: true });
16102
+ fs39.writeFileSync(META_PATH, JSON.stringify(meta, null, 2) + "\n", "utf-8");
15472
16103
  }
15473
16104
  function isNewer(current, candidate) {
15474
16105
  const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
@@ -15581,14 +16212,170 @@ async function runUpdate(opts = {}) {
15581
16212
  process.exit(1);
15582
16213
  }
15583
16214
  }
16215
+ var SOURCE_EXT = /* @__PURE__ */ new Set([
16216
+ "ts",
16217
+ "tsx",
16218
+ "js",
16219
+ "jsx",
16220
+ "mjs",
16221
+ "cjs",
16222
+ "py",
16223
+ "pyi",
16224
+ "rs",
16225
+ "go",
16226
+ "java",
16227
+ "kt",
16228
+ "kts",
16229
+ "rb",
16230
+ "cs",
16231
+ "cpp",
16232
+ "cc",
16233
+ "cxx",
16234
+ "c",
16235
+ "h",
16236
+ "hpp",
16237
+ "swift",
16238
+ "scala",
16239
+ "php"
16240
+ ]);
16241
+ var REGEX_META_RE = /[.*+?^${}()|[\]\\]/;
16242
+ var SYMBOL_ID_RE = /^[A-Za-z_$][A-Za-z0-9_$.-]*$/;
16243
+ function isSymbolLike(term) {
16244
+ return SYMBOL_ID_RE.test(term) && !REGEX_META_RE.test(term);
16245
+ }
16246
+ function isSourceFile(filePath) {
16247
+ const dot = filePath.lastIndexOf(".");
16248
+ if (dot === -1) return false;
16249
+ return SOURCE_EXT.has(filePath.slice(dot + 1).toLowerCase());
16250
+ }
16251
+ function fileStem(filePath) {
16252
+ const base = filePath.includes("/") ? filePath.slice(filePath.lastIndexOf("/") + 1) : filePath;
16253
+ const dot = base.lastIndexOf(".");
16254
+ return dot === -1 ? base : base.slice(0, dot);
16255
+ }
16256
+ function extractGrepSymbol(cmd) {
16257
+ if (/(?:^|\s)-[a-zA-Z]*[cvlLoZ]/.test(cmd)) return null;
16258
+ const quoted = cmd.match(/(?:^|\s)["']([^"']+)["'](?:\s|$)/);
16259
+ if (quoted) {
16260
+ const term = quoted[1];
16261
+ return isSymbolLike(term) ? term : null;
16262
+ }
16263
+ const tokens = cmd.split(/\s+/).slice(1);
16264
+ for (const tok of tokens) {
16265
+ if (tok.startsWith("-")) continue;
16266
+ if (tok.startsWith("/")) continue;
16267
+ if (tok.startsWith("./") || tok.startsWith("../")) continue;
16268
+ if (tok.includes("/")) continue;
16269
+ if (tok === "." || tok === "..") continue;
16270
+ return isSymbolLike(tok) ? tok : null;
16271
+ }
16272
+ return null;
16273
+ }
16274
+ function rewriteCommand(cmd) {
16275
+ try {
16276
+ const trimmed = cmd.trim();
16277
+ if (!trimmed) return null;
16278
+ if (trimmed.startsWith("code-intel ") || trimmed === "code-intel") return null;
16279
+ if (/(?:&&|\|\||;|\|)/.test(trimmed)) return null;
16280
+ if (/^grep\s/.test(trimmed)) {
16281
+ const sym = extractGrepSymbol(trimmed);
16282
+ if (sym) return `code-intel search "${sym}"`;
16283
+ return null;
16284
+ }
16285
+ if (/^rg\s/.test(trimmed)) {
16286
+ if (/\s--files(?:\s|$)/.test(trimmed)) return null;
16287
+ if (/\s--files-with-matches(?:\s|$)/.test(trimmed)) return null;
16288
+ if (/\s--type-not\b/.test(trimmed)) return null;
16289
+ const sym = extractGrepSymbol(trimmed);
16290
+ if (sym) return `code-intel search "${sym}"`;
16291
+ return null;
16292
+ }
16293
+ if (/^cat\s/.test(trimmed)) {
16294
+ const m = trimmed.match(/^cat\s+(\S+)$/);
16295
+ if (!m) return null;
16296
+ const filePath = m[1];
16297
+ if (filePath === "-") return null;
16298
+ if (filePath.startsWith(">")) return null;
16299
+ if (!isSourceFile(filePath)) return null;
16300
+ return `code-intel inspect ${fileStem(filePath)}`;
16301
+ }
16302
+ if (/^(?:head|tail)\s/.test(trimmed)) {
16303
+ if (/\s-f\b/.test(trimmed)) return null;
16304
+ const m = trimmed.match(
16305
+ /^(?:head|tail)\s+(?:-\d+\s+|-n\s+\d+\s+|--lines=\d+\s+)?(\S+)$/
16306
+ );
16307
+ if (!m) return null;
16308
+ const filePath = m[1];
16309
+ if (!isSourceFile(filePath)) return null;
16310
+ return `code-intel inspect ${fileStem(filePath)}`;
16311
+ }
16312
+ return null;
16313
+ } catch {
16314
+ return null;
16315
+ }
16316
+ }
16317
+ function runRewrite(cmd) {
16318
+ const rewritten = rewriteCommand(cmd.trim());
16319
+ if (rewritten === null) {
16320
+ process2.exit(1);
16321
+ }
16322
+ process2.stdout.write(rewritten);
16323
+ process2.exit(0);
16324
+ }
16325
+ function runClaudeHook() {
16326
+ process2.on("uncaughtException", () => process2.exit(0));
16327
+ process2.on("unhandledRejection", () => process2.exit(0));
16328
+ let input = "";
16329
+ process2.stdin.setEncoding("utf-8");
16330
+ process2.stdin.on("data", (chunk) => {
16331
+ input += chunk;
16332
+ });
16333
+ process2.stdin.on("end", () => {
16334
+ try {
16335
+ if (!input.trim()) {
16336
+ process2.exit(0);
16337
+ }
16338
+ const parsed = JSON.parse(input);
16339
+ const cmd = parsed?.tool_input?.command;
16340
+ if (typeof cmd !== "string" || !cmd.trim()) {
16341
+ process2.exit(0);
16342
+ }
16343
+ const rewritten = rewriteCommand(cmd);
16344
+ if (rewritten === null || rewritten === cmd) {
16345
+ process2.exit(0);
16346
+ }
16347
+ const response = {
16348
+ hookSpecificOutput: {
16349
+ hookEventName: "PreToolUse",
16350
+ permissionDecision: "allow",
16351
+ permissionDecisionReason: "code-intel: semantic search replaces grep/cat",
16352
+ updatedInput: {
16353
+ ...parsed.tool_input,
16354
+ command: rewritten
16355
+ }
16356
+ }
16357
+ };
16358
+ process2.stdout.write(JSON.stringify(response));
16359
+ process2.exit(0);
16360
+ } catch {
16361
+ process2.exit(0);
16362
+ }
16363
+ });
16364
+ process2.stdin.on("error", () => {
16365
+ process2.exit(0);
16366
+ });
16367
+ }
15584
16368
 
15585
16369
  // src/cli/main.ts
15586
16370
  var __filename$1 = fileURLToPath(import.meta.url);
15587
16371
  var __dirname2 = dirname(__filename$1);
15588
16372
  var _pkg = JSON.parse(readFileSync(join(__dirname2, "../../package.json"), "utf-8"));
15589
- initTracing();
16373
+ var IS_HOOK_MODE = process.argv[2] === "hook";
16374
+ if (!IS_HOOK_MODE) {
16375
+ initTracing();
16376
+ }
15590
16377
  var debugMode = process.argv.includes("--debug");
15591
- {
16378
+ if (!IS_HOOK_MODE) {
15592
16379
  const checks = runPrerequisiteChecks();
15593
16380
  for (const c of checks) {
15594
16381
  const icon = c.level === "error" ? "\u2717" : "\u26A0";
@@ -15596,20 +16383,28 @@ var debugMode = process.argv.includes("--debug");
15596
16383
  `);
15597
16384
  }
15598
16385
  }
15599
- if (!configExists()) {
16386
+ if (!IS_HOOK_MODE && !configExists()) {
15600
16387
  process.stderr.write(
15601
16388
  " \u2139 No config found. Run `code-intel init` to set up your environment.\n"
15602
16389
  );
15603
16390
  }
15604
16391
  process.on("uncaughtException", (err) => {
16392
+ if (IS_HOOK_MODE) {
16393
+ process.exit(0);
16394
+ }
15605
16395
  process.stderr.write(formatCliError(err, debugMode));
15606
16396
  process.exit(1);
15607
16397
  });
15608
16398
  process.on("unhandledRejection", (err) => {
16399
+ if (IS_HOOK_MODE) {
16400
+ process.exit(0);
16401
+ }
15609
16402
  process.stderr.write(formatCliError(err, debugMode));
15610
16403
  process.exit(1);
15611
16404
  });
15612
- backgroundVersionCheck(_pkg.version);
16405
+ if (!IS_HOOK_MODE) {
16406
+ backgroundVersionCheck(_pkg.version);
16407
+ }
15613
16408
  var program = new Command();
15614
16409
  var BANNER = `
15615
16410
  \u25C8 Code Intelligence Platform v${_pkg.version}
@@ -15659,6 +16454,8 @@ program.name("code-intel").description("Code Intelligence Platform \u2014 Static
15659
16454
  \u2502 server \u2502
15660
16455
  \u2502 code-intel mcp [path] Launch the MCP stdio server consumed by AI-enabled editors \u2502
15661
16456
  \u2502 code-intel serve [path] --port <n> Start the HTTP API and serve the interactive web UI (default :4747) \u2502
16457
+ \u2502 code-intel serve --detach Start the server in the background (frees the terminal) \u2502
16458
+ \u2502 code-intel stop [path] Stop a background server started with --detach \u2502
15662
16459
  \u2502 \u2502
15663
16460
  \u2502 registry \u2502
15664
16461
  \u2502 code-intel list Display all repositories that have been indexed \u2502
@@ -15688,14 +16485,37 @@ program.name("code-intel").description("Code Intelligence Platform \u2014 Static
15688
16485
 
15689
16486
  Docs: https://github.com/vohongtho/code-intel-platform
15690
16487
  `);
16488
+ function ensureGitignore(workspaceRoot, silent) {
16489
+ const gitignorePath = path39.join(workspaceRoot, ".gitignore");
16490
+ const entry = ".code-intel/";
16491
+ try {
16492
+ let existing = "";
16493
+ if (fs39.existsSync(gitignorePath)) {
16494
+ existing = fs39.readFileSync(gitignorePath, "utf-8");
16495
+ }
16496
+ const lines = existing.split("\n").map((l) => l.trim());
16497
+ if (lines.includes(".code-intel/") || lines.includes(".code-intel")) {
16498
+ return;
16499
+ }
16500
+ const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
16501
+ fs39.appendFileSync(gitignorePath, `${suffix}${entry}
16502
+ `, "utf-8");
16503
+ logger_default.info(".gitignore updated: added .code-intel/");
16504
+ if (!silent) {
16505
+ console.log(` \u2713 .gitignore: added .code-intel/`);
16506
+ }
16507
+ } catch (err) {
16508
+ logger_default.warn(`.gitignore update failed: ${err instanceof Error ? err.message : err}`);
16509
+ }
16510
+ }
15691
16511
  async function analyzeWorkspace(targetPath, options) {
15692
16512
  const workspaceRoot = path39.resolve(targetPath);
15693
- if (!fs38.existsSync(workspaceRoot)) {
16513
+ if (!fs39.existsSync(workspaceRoot)) {
15694
16514
  logger_default.error(`Path does not exist: ${workspaceRoot}`);
15695
16515
  console.error(` \u2717 Path does not exist: ${workspaceRoot}`);
15696
16516
  process.exit(1);
15697
16517
  }
15698
- if (!fs38.statSync(workspaceRoot).isDirectory()) {
16518
+ if (!fs39.statSync(workspaceRoot).isDirectory()) {
15699
16519
  logger_default.error(`Path is not a directory: ${workspaceRoot}`);
15700
16520
  console.error(` \u2717 Path is not a directory: ${workspaceRoot}`);
15701
16521
  process.exit(1);
@@ -15720,14 +16540,14 @@ async function analyzeWorkspace(targetPath, options) {
15720
16540
  ];
15721
16541
  for (const f of wipeFiles) {
15722
16542
  try {
15723
- if (fs38.existsSync(f)) fs38.unlinkSync(f);
16543
+ if (fs39.existsSync(f)) fs39.unlinkSync(f);
15724
16544
  } catch {
15725
16545
  }
15726
16546
  }
15727
16547
  }
15728
16548
  if (!options?.skipGit) {
15729
16549
  const gitDir = path39.join(workspaceRoot, ".git");
15730
- if (!fs38.existsSync(gitDir)) {
16550
+ if (!fs39.existsSync(gitDir)) {
15731
16551
  logger_default.warn(`${workspaceRoot} is not a Git repository`);
15732
16552
  }
15733
16553
  }
@@ -15783,7 +16603,9 @@ async function analyzeWorkspace(targetPath, options) {
15783
16603
  provider: options.llmProvider ?? "ollama",
15784
16604
  model: options.llmModel,
15785
16605
  batchSize: options.llmBatchSize,
15786
- maxNodesPerRun: options.llmMaxNodes
16606
+ maxNodesPerRun: options.llmMaxNodes,
16607
+ baseUrl: options.llmBaseUrl,
16608
+ apiKey: options.llmApiKey
15787
16609
  } : void 0,
15788
16610
  onProgress: options?.silent ? void 0 : (phase, msg) => {
15789
16611
  if (!options?.silent) {
@@ -15866,8 +16688,8 @@ async function analyzeWorkspace(targetPath, options) {
15866
16688
  };
15867
16689
  const profilePath = path39.join(workspaceRoot, ".code-intel", "profile.json");
15868
16690
  try {
15869
- fs38.mkdirSync(path39.join(workspaceRoot, ".code-intel"), { recursive: true });
15870
- fs38.writeFileSync(profilePath, JSON.stringify(profileJson, null, 2));
16691
+ fs39.mkdirSync(path39.join(workspaceRoot, ".code-intel"), { recursive: true });
16692
+ fs39.writeFileSync(profilePath, JSON.stringify(profileJson, null, 2));
15871
16693
  if (!options?.silent) console.log(` \u2713 Profile written: ${profilePath}`);
15872
16694
  } catch (err) {
15873
16695
  logger_default.warn(`Failed to write profile.json: ${err instanceof Error ? err.message : err}`);
@@ -15894,7 +16716,7 @@ async function analyzeWorkspace(targetPath, options) {
15894
16716
  }
15895
16717
  if (isIncremental && incrementalChangedFiles && incrementalChangedFiles.length > 0) {
15896
16718
  const dbPath = getDbPath(workspaceRoot);
15897
- if (fs38.existsSync(dbPath)) {
16719
+ if (fs39.existsSync(dbPath)) {
15898
16720
  try {
15899
16721
  const db = new DbManager(dbPath);
15900
16722
  await db.init();
@@ -15967,7 +16789,7 @@ async function analyzeWorkspace(targetPath, options) {
15967
16789
  ];
15968
16790
  for (const f of newStaleFiles) {
15969
16791
  try {
15970
- if (fs38.existsSync(f)) fs38.unlinkSync(f);
16792
+ if (fs39.existsSync(f)) fs39.unlinkSync(f);
15971
16793
  } catch {
15972
16794
  }
15973
16795
  }
@@ -15984,21 +16806,21 @@ async function analyzeWorkspace(targetPath, options) {
15984
16806
  ];
15985
16807
  for (const f of staleFiles) {
15986
16808
  try {
15987
- if (fs38.existsSync(f)) fs38.unlinkSync(f);
16809
+ if (fs39.existsSync(f)) fs39.unlinkSync(f);
15988
16810
  } catch {
15989
16811
  }
15990
16812
  }
15991
16813
  for (const f of newStaleFiles) {
15992
16814
  if (f === dbPathNew) continue;
15993
- if (fs38.existsSync(f)) {
16815
+ if (fs39.existsSync(f)) {
15994
16816
  const dest = f.replace(dbPathNew, dbPath);
15995
16817
  try {
15996
- fs38.renameSync(f, dest);
16818
+ fs39.renameSync(f, dest);
15997
16819
  } catch {
15998
16820
  }
15999
16821
  }
16000
16822
  }
16001
- fs38.renameSync(dbPathNew, dbPath);
16823
+ fs39.renameSync(dbPathNew, dbPath);
16002
16824
  stopSpinner();
16003
16825
  logger_default.info(`DB persisted: ${nodeCount} nodes, ${edgeCount} edges`);
16004
16826
  if (!options?.silent) {
@@ -16030,7 +16852,7 @@ async function analyzeWorkspace(targetPath, options) {
16030
16852
  const staleVdb = [vdbPath, `${vdbPath}-shm`, `${vdbPath}-wal`];
16031
16853
  for (const f of staleVdb) {
16032
16854
  try {
16033
- if (fs38.existsSync(f)) fs38.unlinkSync(f);
16855
+ if (fs39.existsSync(f)) fs39.unlinkSync(f);
16034
16856
  } catch {
16035
16857
  }
16036
16858
  }
@@ -16093,6 +16915,7 @@ async function analyzeWorkspace(targetPath, options) {
16093
16915
  logger_default.warn(`Context file write failed: ${err instanceof Error ? err.message : err}`);
16094
16916
  }
16095
16917
  }
16918
+ ensureGitignore(workspaceRoot, options?.silent ?? false);
16096
16919
  if (!options?.silent) {
16097
16920
  const dur = result.totalDuration;
16098
16921
  const durStr = dur >= 1e3 ? `${(dur / 1e3).toFixed(1)}s` : `${dur}ms`;
@@ -16216,6 +17039,396 @@ program.command("completion").description("Generate shell completion scripts (ba
16216
17039
  }
16217
17040
  process.stdout.write(generateCompletion(shell));
16218
17041
  });
17042
+ var CODE_INTEL_HOOK_CMD = "code-intel-hook claude";
17043
+ var CODE_INTEL_CURSOR_HOOK_CMD = "code-intel-hook cursor";
17044
+ var CODE_INTEL_GEMINI_HOOK_CMD = "code-intel-hook gemini";
17045
+ function hookAlreadyPresent(root) {
17046
+ const entries = root?.hooks?.PreToolUse;
17047
+ if (!Array.isArray(entries)) return false;
17048
+ return entries.some(
17049
+ (entry) => Array.isArray(entry?.hooks) && entry.hooks.some(
17050
+ (h) => typeof h?.command === "string" && (h.command.includes("code-intel-hook") || h.command.includes("code-intel hook"))
17051
+ )
17052
+ );
17053
+ }
17054
+ function insertHookEntry(root) {
17055
+ const existingHooks = root.hooks ?? {};
17056
+ const existingPreToolUse = Array.isArray(
17057
+ existingHooks.PreToolUse
17058
+ ) ? existingHooks.PreToolUse : [];
17059
+ const newEntry = {
17060
+ matcher: "Bash",
17061
+ hooks: [{ type: "command", command: CODE_INTEL_HOOK_CMD }]
17062
+ };
17063
+ return {
17064
+ ...root,
17065
+ hooks: {
17066
+ ...existingHooks,
17067
+ PreToolUse: [newEntry, ...existingPreToolUse]
17068
+ }
17069
+ };
17070
+ }
17071
+ function installClaudeHook() {
17072
+ const settingsPath = path39.join(os18.homedir(), ".claude", "settings.json");
17073
+ let root = {};
17074
+ if (fs39.existsSync(settingsPath)) {
17075
+ try {
17076
+ const raw = fs39.readFileSync(settingsPath, "utf-8");
17077
+ root = JSON.parse(raw);
17078
+ } catch (err) {
17079
+ return {
17080
+ result: "skipped",
17081
+ reason: `Could not parse ~/.claude/settings.json: ${err instanceof Error ? err.message : String(err)}`
17082
+ };
17083
+ }
17084
+ }
17085
+ if (hookAlreadyPresent(root)) {
17086
+ return { result: "already-present" };
17087
+ }
17088
+ const updated = insertHookEntry(root);
17089
+ if (fs39.existsSync(settingsPath)) {
17090
+ try {
17091
+ fs39.copyFileSync(settingsPath, `${settingsPath}.bak`);
17092
+ } catch {
17093
+ logger_default.warn(" \u26A0 Could not create settings.json backup \u2014 proceeding anyway");
17094
+ }
17095
+ }
17096
+ try {
17097
+ fs39.mkdirSync(path39.dirname(settingsPath), { recursive: true });
17098
+ const tmp = `${settingsPath}.tmp.${Date.now()}`;
17099
+ fs39.writeFileSync(tmp, JSON.stringify(updated, null, 2) + "\n", "utf-8");
17100
+ fs39.renameSync(tmp, settingsPath);
17101
+ } catch (err) {
17102
+ return {
17103
+ result: "skipped",
17104
+ reason: `Could not write ~/.claude/settings.json: ${err instanceof Error ? err.message : String(err)}`
17105
+ };
17106
+ }
17107
+ return { result: "installed" };
17108
+ }
17109
+ var COPILOT_HOOK_JSON_CONTENT = JSON.stringify({
17110
+ hooks: {
17111
+ PreToolUse: [
17112
+ { type: "command", command: CODE_INTEL_HOOK_CMD.replace("claude", "copilot"), cwd: ".", timeout: 5 }
17113
+ ]
17114
+ }
17115
+ }, null, 2) + "\n";
17116
+ function installCopilotHook(workspaceRoot = ".") {
17117
+ const githubDir = path39.join(workspaceRoot, ".github");
17118
+ const hooksDir = path39.join(githubDir, "hooks");
17119
+ const hookFile = path39.join(hooksDir, "code-intel-rewrite.json");
17120
+ try {
17121
+ fs39.mkdirSync(hooksDir, { recursive: true });
17122
+ if (fs39.existsSync(hookFile)) {
17123
+ try {
17124
+ const existing = JSON.parse(fs39.readFileSync(hookFile, "utf-8"));
17125
+ const hooks = existing?.hooks?.PreToolUse;
17126
+ if (Array.isArray(hooks) && hooks.some(
17127
+ (h) => typeof h?.command === "string" && h.command.includes("code-intel")
17128
+ )) {
17129
+ return { result: "already-present" };
17130
+ }
17131
+ } catch {
17132
+ }
17133
+ }
17134
+ const tmp = `${hookFile}.tmp.${Date.now()}`;
17135
+ fs39.writeFileSync(tmp, COPILOT_HOOK_JSON_CONTENT, "utf-8");
17136
+ fs39.renameSync(tmp, hookFile);
17137
+ return { result: "installed" };
17138
+ } catch (err) {
17139
+ return { result: "skipped", reason: `Could not write .github/hooks/code-intel-rewrite.json: ${err instanceof Error ? err.message : String(err)}` };
17140
+ }
17141
+ }
17142
+ var OPENCODE_PLUGIN_CONTENT = `import type { Plugin } from "@opencode-ai/plugin"
17143
+ import { execSync } from "node:child_process"
17144
+
17145
+ // code-intel OpenCode plugin \u2014 rewrites symbol-discovery commands to semantic equivalents.
17146
+ // Requires: code-intel installed and in PATH.
17147
+ // To change rewrite rules, edit hook-rewriter.ts in the code-intel source.
17148
+
17149
+ let codeIntelAvailable: boolean | null = null
17150
+
17151
+ function checkCodeIntel(): boolean {
17152
+ if (codeIntelAvailable !== null) return codeIntelAvailable
17153
+ try {
17154
+ execSync("which code-intel", { stdio: "ignore" })
17155
+ codeIntelAvailable = true
17156
+ } catch {
17157
+ codeIntelAvailable = false
17158
+ }
17159
+ return codeIntelAvailable
17160
+ }
17161
+
17162
+ function tryRewrite(command: string): string | null {
17163
+ try {
17164
+ const result = execSync(\`code-intel rewrite \${JSON.stringify(command)}\`, {
17165
+ encoding: "utf-8",
17166
+ timeout: 2000,
17167
+ }).trim()
17168
+ return result && result !== command ? result : null
17169
+ } catch {
17170
+ return null
17171
+ }
17172
+ }
17173
+
17174
+ export const CodeIntelOpenCodePlugin: Plugin = async ({ $ }) => {
17175
+ if (!checkCodeIntel()) {
17176
+ console.warn("[code-intel] code-intel binary not found in PATH \u2014 plugin disabled")
17177
+ return {}
17178
+ }
17179
+ return {
17180
+ "tool.execute.before": async (input, output) => {
17181
+ const tool = String(input?.tool ?? "").toLowerCase()
17182
+ if (tool !== "bash" && tool !== "shell") return
17183
+ const args = output?.args
17184
+ if (!args || typeof args !== "object") return
17185
+ const command = (args as Record<string, unknown>).command
17186
+ if (typeof command !== "string" || !command) return
17187
+ try {
17188
+ const rewritten = tryRewrite(command)
17189
+ if (rewritten) {
17190
+ ;(args as Record<string, unknown>).command = rewritten
17191
+ }
17192
+ } catch {
17193
+ // non-blocking
17194
+ }
17195
+ },
17196
+ }
17197
+ }
17198
+ `;
17199
+ function installOpenCodePlugin() {
17200
+ const pluginDir = path39.join(os18.homedir(), ".config", "opencode", "plugins");
17201
+ const pluginFile = path39.join(pluginDir, "code-intel.ts");
17202
+ const opencodeCfg = path39.join(os18.homedir(), ".config", "opencode");
17203
+ if (!fs39.existsSync(opencodeCfg)) {
17204
+ return { result: "skipped", reason: "OpenCode not installed (~/.config/opencode not found)" };
17205
+ }
17206
+ if (fs39.existsSync(pluginFile)) {
17207
+ return { result: "already-present" };
17208
+ }
17209
+ try {
17210
+ fs39.mkdirSync(pluginDir, { recursive: true });
17211
+ const tmp = `${pluginFile}.tmp.${Date.now()}`;
17212
+ fs39.writeFileSync(tmp, OPENCODE_PLUGIN_CONTENT, "utf-8");
17213
+ fs39.renameSync(tmp, pluginFile);
17214
+ return { result: "installed" };
17215
+ } catch (err) {
17216
+ return { result: "skipped", reason: `Could not write plugin: ${err instanceof Error ? err.message : String(err)}` };
17217
+ }
17218
+ }
17219
+ var OPENCLAW_PLUGIN_CONTENT = `import { execSync } from "node:child_process";
17220
+
17221
+ // code-intel OpenClaw plugin \u2014 rewrites symbol-discovery commands to semantic equivalents.
17222
+ // Requires: code-intel installed and in PATH.
17223
+ // To change rewrite rules, edit hook-rewriter.ts in the code-intel source.
17224
+
17225
+ let codeIntelAvailable: boolean | null = null;
17226
+
17227
+ function checkCodeIntel(): boolean {
17228
+ if (codeIntelAvailable !== null) return codeIntelAvailable;
17229
+ try {
17230
+ execSync("which code-intel", { stdio: "ignore" });
17231
+ codeIntelAvailable = true;
17232
+ } catch {
17233
+ codeIntelAvailable = false;
17234
+ }
17235
+ return codeIntelAvailable;
17236
+ }
17237
+
17238
+ function tryRewrite(command: string): string | null {
17239
+ try {
17240
+ const result = execSync(\`code-intel rewrite \${JSON.stringify(command)}\`, {
17241
+ encoding: "utf-8",
17242
+ timeout: 2000,
17243
+ }).trim();
17244
+ return result && result !== command ? result : null;
17245
+ } catch {
17246
+ return null;
17247
+ }
17248
+ }
17249
+
17250
+ export default function register(api: any) {
17251
+ const pluginConfig = api.config ?? {};
17252
+ const enabled = pluginConfig.enabled !== false;
17253
+ const verbose = pluginConfig.verbose === true;
17254
+
17255
+ if (!enabled) return;
17256
+
17257
+ if (!checkCodeIntel()) {
17258
+ console.warn("[code-intel] code-intel binary not found in PATH \u2014 plugin disabled");
17259
+ return;
17260
+ }
17261
+
17262
+ api.on(
17263
+ "before_tool_call",
17264
+ (event: { toolName: string; params: Record<string, unknown> }) => {
17265
+ if (event.toolName !== "exec") return;
17266
+ const command = event.params?.command;
17267
+ if (typeof command !== "string") return;
17268
+ const rewritten = tryRewrite(command);
17269
+ if (!rewritten) return;
17270
+ if (verbose) console.log(\`[code-intel] \${command} -> \${rewritten}\`);
17271
+ return { params: { ...event.params, command: rewritten } };
17272
+ },
17273
+ { priority: 10 }
17274
+ );
17275
+
17276
+ if (verbose) console.log("[code-intel] OpenClaw plugin registered");
17277
+ }
17278
+ `;
17279
+ function installOpenClawPlugin() {
17280
+ const openclawDir = path39.join(os18.homedir(), ".openclaw");
17281
+ if (!fs39.existsSync(openclawDir)) {
17282
+ return { result: "skipped", reason: "OpenClaw not installed (~/.openclaw directory not found)" };
17283
+ }
17284
+ const pluginDir = path39.join(openclawDir, "extensions", "code-intel");
17285
+ const pluginFile = path39.join(pluginDir, "index.ts");
17286
+ if (fs39.existsSync(pluginFile)) {
17287
+ return { result: "already-present" };
17288
+ }
17289
+ try {
17290
+ fs39.mkdirSync(pluginDir, { recursive: true });
17291
+ const tmp = `${pluginFile}.tmp.${Date.now()}`;
17292
+ fs39.writeFileSync(tmp, OPENCLAW_PLUGIN_CONTENT, "utf-8");
17293
+ fs39.renameSync(tmp, pluginFile);
17294
+ return { result: "installed" };
17295
+ } catch (err) {
17296
+ return { result: "skipped", reason: `Could not write plugin: ${err instanceof Error ? err.message : String(err)}` };
17297
+ }
17298
+ }
17299
+ var CODE_INTEL_RULES_CONTENT = `# Code Intelligence \u2014 Tool Policy
17300
+
17301
+ ## MANDATORY: Use code-intel before any code action
17302
+
17303
+ **FORBIDDEN** (never use for symbol/code discovery): \`grep\`, \`rg\`, \`find\`, \`cat\`, \`sed\`, \`ls\`
17304
+
17305
+ **REQUIRED workflow**:
17306
+ \`\`\`bash
17307
+ code-intel search "<concept>" # replaces grep/rg/find
17308
+ code-intel inspect <symbol> # replaces cat + manual reading
17309
+ code-intel impact <symbol> # replaces manual caller tracing
17310
+ \`\`\`
17311
+
17312
+ Rules:
17313
+ - NEVER open a file cold \u2014 always \`code-intel search\` first
17314
+ - NEVER use grep for symbols \u2014 use \`code-intel search\` instead
17315
+ - If impact shows \u2265 5 callers \u2192 warn user: HIGH blast radius
17316
+ `;
17317
+ function installRulesFile(filePath, agentName) {
17318
+ try {
17319
+ const dir = path39.dirname(filePath);
17320
+ if (!fs39.existsSync(dir)) {
17321
+ fs39.mkdirSync(dir, { recursive: true });
17322
+ }
17323
+ if (fs39.existsSync(filePath)) {
17324
+ const existing = fs39.readFileSync(filePath, "utf-8");
17325
+ if (existing.includes("code-intel search") || existing.includes("code-intel inspect")) {
17326
+ return { result: "already-present" };
17327
+ }
17328
+ fs39.appendFileSync(filePath, "\n---\n\n" + CODE_INTEL_RULES_CONTENT);
17329
+ return { result: "installed" };
17330
+ }
17331
+ fs39.writeFileSync(filePath, CODE_INTEL_RULES_CONTENT, "utf-8");
17332
+ return { result: "installed" };
17333
+ } catch (err) {
17334
+ return { result: "skipped", reason: `Could not write ${agentName} rules: ${err instanceof Error ? err.message : String(err)}` };
17335
+ }
17336
+ }
17337
+ function cursorHookAlreadyPresent(root) {
17338
+ const hooks = root?.hooks;
17339
+ const preToolUse = hooks?.preToolUse;
17340
+ if (!Array.isArray(preToolUse)) return false;
17341
+ return preToolUse.some((entry) => {
17342
+ const cmd = entry?.command;
17343
+ return typeof cmd === "string" && (cmd.includes("code-intel-hook") || cmd.includes("code-intel hook"));
17344
+ });
17345
+ }
17346
+ function installCursorHook() {
17347
+ const cursorDir = path39.join(os18.homedir(), ".cursor");
17348
+ const hooksPath = path39.join(cursorDir, "hooks.json");
17349
+ let root = { version: 1 };
17350
+ if (fs39.existsSync(hooksPath)) {
17351
+ try {
17352
+ const raw = fs39.readFileSync(hooksPath, "utf-8");
17353
+ if (raw.trim()) root = JSON.parse(raw);
17354
+ } catch (err) {
17355
+ return { result: "skipped", reason: `Could not parse ~/.cursor/hooks.json: ${err instanceof Error ? err.message : String(err)}` };
17356
+ }
17357
+ } else if (!fs39.existsSync(cursorDir)) {
17358
+ return { result: "skipped", reason: "Cursor not installed (~/.cursor directory not found)" };
17359
+ }
17360
+ if (cursorHookAlreadyPresent(root)) return { result: "already-present" };
17361
+ if (fs39.existsSync(hooksPath)) {
17362
+ try {
17363
+ fs39.copyFileSync(hooksPath, `${hooksPath}.bak`);
17364
+ } catch {
17365
+ }
17366
+ }
17367
+ const existingHooks = root.hooks ?? {};
17368
+ const existingPreToolUse = Array.isArray(existingHooks.preToolUse) ? existingHooks.preToolUse : [];
17369
+ const newEntry = { command: CODE_INTEL_CURSOR_HOOK_CMD, matcher: "Shell" };
17370
+ const updated = { ...root, hooks: { ...existingHooks, preToolUse: [newEntry, ...existingPreToolUse] } };
17371
+ try {
17372
+ fs39.mkdirSync(cursorDir, { recursive: true });
17373
+ const tmp = `${hooksPath}.tmp.${Date.now()}`;
17374
+ fs39.writeFileSync(tmp, JSON.stringify(updated, null, 2) + "\n", "utf-8");
17375
+ fs39.renameSync(tmp, hooksPath);
17376
+ } catch (err) {
17377
+ return { result: "skipped", reason: `Could not write ~/.cursor/hooks.json: ${err instanceof Error ? err.message : String(err)}` };
17378
+ }
17379
+ return { result: "installed" };
17380
+ }
17381
+ function geminiHookAlreadyPresent(root) {
17382
+ const hooks = root?.hooks;
17383
+ const beforeTool = hooks?.BeforeTool;
17384
+ if (!Array.isArray(beforeTool)) return false;
17385
+ return beforeTool.some((entry) => {
17386
+ const entryHooks = entry?.hooks;
17387
+ if (!Array.isArray(entryHooks)) return false;
17388
+ return entryHooks.some((h) => {
17389
+ const cmd = h?.command;
17390
+ return typeof cmd === "string" && (cmd.includes("code-intel-hook") || cmd.includes("code-intel hook"));
17391
+ });
17392
+ });
17393
+ }
17394
+ function installGeminiHook() {
17395
+ const geminiDir = path39.join(os18.homedir(), ".gemini");
17396
+ const settingsPath = path39.join(geminiDir, "settings.json");
17397
+ if (!fs39.existsSync(geminiDir)) {
17398
+ return { result: "skipped", reason: "Gemini CLI not installed (~/.gemini directory not found)" };
17399
+ }
17400
+ let root = {};
17401
+ if (fs39.existsSync(settingsPath)) {
17402
+ try {
17403
+ const raw = fs39.readFileSync(settingsPath, "utf-8");
17404
+ if (raw.trim()) root = JSON.parse(raw);
17405
+ } catch (err) {
17406
+ return { result: "skipped", reason: `Could not parse ~/.gemini/settings.json: ${err instanceof Error ? err.message : String(err)}` };
17407
+ }
17408
+ }
17409
+ if (geminiHookAlreadyPresent(root)) return { result: "already-present" };
17410
+ if (fs39.existsSync(settingsPath)) {
17411
+ try {
17412
+ fs39.copyFileSync(settingsPath, `${settingsPath}.bak`);
17413
+ } catch {
17414
+ }
17415
+ }
17416
+ const existingHooks = root.hooks ?? {};
17417
+ const existingBeforeTool = Array.isArray(existingHooks.BeforeTool) ? existingHooks.BeforeTool : [];
17418
+ const newEntry = {
17419
+ matcher: "run_shell_command",
17420
+ hooks: [{ type: "command", command: CODE_INTEL_GEMINI_HOOK_CMD }]
17421
+ };
17422
+ const updated = { ...root, hooks: { ...existingHooks, BeforeTool: [newEntry, ...existingBeforeTool] } };
17423
+ try {
17424
+ const tmp = `${settingsPath}.tmp.${Date.now()}`;
17425
+ fs39.writeFileSync(tmp, JSON.stringify(updated, null, 2) + "\n", "utf-8");
17426
+ fs39.renameSync(tmp, settingsPath);
17427
+ } catch (err) {
17428
+ return { result: "skipped", reason: `Could not write ~/.gemini/settings.json: ${err instanceof Error ? err.message : String(err)}` };
17429
+ }
17430
+ return { result: "installed" };
17431
+ }
16219
17432
  program.command("setup").description("Configure MCP server for your editors (one-time setup)").option("--completion", "Auto-install shell completion for the detected shell").addHelpText("after", `
16220
17433
  Configure the code-intel MCP server for Claude Desktop, VS Code, or any
16221
17434
  editor that supports the Model Context Protocol.
@@ -16247,8 +17460,8 @@ program.command("setup").description("Configure MCP server for your editors (one
16247
17460
  const configFile = `${configDir}/claude_desktop_config.json`;
16248
17461
  try {
16249
17462
  let existing = {};
16250
- if (fs38.existsSync(configFile)) {
16251
- existing = JSON.parse(fs38.readFileSync(configFile, "utf-8"));
17463
+ if (fs39.existsSync(configFile)) {
17464
+ existing = JSON.parse(fs39.readFileSync(configFile, "utf-8"));
16252
17465
  }
16253
17466
  const merged = {
16254
17467
  ...existing,
@@ -16257,8 +17470,8 @@ program.command("setup").description("Configure MCP server for your editors (one
16257
17470
  ...mcpConfig.mcpServers
16258
17471
  }
16259
17472
  };
16260
- fs38.mkdirSync(configDir, { recursive: true });
16261
- fs38.writeFileSync(configFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
17473
+ fs39.mkdirSync(configDir, { recursive: true });
17474
+ fs39.writeFileSync(configFile, JSON.stringify(merged, null, 2) + "\n", "utf-8");
16262
17475
  console.log(`
16263
17476
  \u2705 Written to ${configFile}`);
16264
17477
  } catch (err) {
@@ -16271,8 +17484,129 @@ program.command("setup").description("Configure MCP server for your editors (one
16271
17484
  console.log(" " + JSON.stringify({ servers: { "code-intel": { type: "stdio", command: "npx", args: ["code-intel", "mcp", "."] } } }, null, 2).split("\n").join("\n "));
16272
17485
  console.log('\n To verify in VS Code: open Command Palette \u2192 "MCP: List Servers" and confirm code-intel is Running.');
16273
17486
  console.log("\n Next: run `code-intel analyze` inside your project to build the knowledge graph.\n");
17487
+ const hookResult = installClaudeHook();
17488
+ if (hookResult.result === "installed") {
17489
+ const backupPath = path39.join(os18.homedir(), ".claude", "settings.json.bak");
17490
+ console.log("\n \u2705 Claude Code hook installed");
17491
+ console.log(" grep/rg/cat on source files \u2192 code-intel search/inspect");
17492
+ if (fs39.existsSync(backupPath)) {
17493
+ console.log(` Backup: ${backupPath}`);
17494
+ }
17495
+ console.log("\n \u21BA Restart Claude Code for the hook to take effect.");
17496
+ } else if (hookResult.result === "already-present") {
17497
+ console.log("\n \u2705 Claude Code hook already installed");
17498
+ } else {
17499
+ logger_default.warn(`
17500
+ \u26A0 Claude Code hook skipped: ${hookResult.reason}`);
17501
+ console.log("\n Add manually to ~/.claude/settings.json under hooks.PreToolUse[]:");
17502
+ console.log(" (insert as the FIRST entry so it runs before any existing hooks)\n");
17503
+ console.log(" " + JSON.stringify({
17504
+ matcher: "Bash",
17505
+ hooks: [{ type: "command", command: CODE_INTEL_HOOK_CMD }]
17506
+ }, null, 2).split("\n").join("\n "));
17507
+ }
17508
+ const cursorResult = installCursorHook();
17509
+ if (cursorResult.result === "installed") {
17510
+ console.log("\n \u2705 Cursor hook installed");
17511
+ console.log(" grep/rg/cat on source files \u2192 code-intel search/inspect");
17512
+ } else if (cursorResult.result === "already-present") {
17513
+ console.log("\n \u2705 Cursor hook already installed");
17514
+ } else {
17515
+ console.log(`
17516
+ \u2139 Cursor hook skipped: ${cursorResult.reason}`);
17517
+ }
17518
+ const geminiResult = installGeminiHook();
17519
+ if (geminiResult.result === "installed") {
17520
+ console.log("\n \u2705 Gemini CLI hook installed");
17521
+ console.log(" grep/rg/cat on source files \u2192 code-intel search/inspect");
17522
+ } else if (geminiResult.result === "already-present") {
17523
+ console.log("\n \u2705 Gemini CLI hook already installed");
17524
+ } else {
17525
+ console.log(`
17526
+ \u2139 Gemini CLI hook skipped: ${geminiResult.reason}`);
17527
+ }
17528
+ const copilotResult = installCopilotHook(process.cwd());
17529
+ if (copilotResult.result === "installed") {
17530
+ console.log("\n \u2705 GitHub Copilot hook installed (.github/hooks/code-intel-rewrite.json)");
17531
+ console.log(" grep/rg/cat on source files \u2192 code-intel search/inspect");
17532
+ } else if (copilotResult.result === "already-present") {
17533
+ console.log("\n \u2705 GitHub Copilot hook already installed");
17534
+ } else {
17535
+ console.log(`
17536
+ \u2139 GitHub Copilot hook skipped: ${copilotResult.reason}`);
17537
+ }
17538
+ const openCodeResult = installOpenCodePlugin();
17539
+ if (openCodeResult.result === "installed") {
17540
+ console.log("\n \u2705 OpenCode plugin installed (~/.config/opencode/plugins/code-intel.ts)");
17541
+ console.log(" grep/rg/cat on source files \u2192 code-intel search/inspect");
17542
+ } else if (openCodeResult.result === "already-present") {
17543
+ console.log("\n \u2705 OpenCode plugin already installed");
17544
+ } else {
17545
+ console.log(`
17546
+ \u2139 OpenCode plugin skipped: ${openCodeResult.reason}`);
17547
+ }
17548
+ const openClawResult = installOpenClawPlugin();
17549
+ if (openClawResult.result === "installed") {
17550
+ console.log("\n \u2705 OpenClaw plugin installed (~/.openclaw/extensions/code-intel/)");
17551
+ console.log(" grep/rg/cat on source files \u2192 code-intel search/inspect");
17552
+ } else if (openClawResult.result === "already-present") {
17553
+ console.log("\n \u2705 OpenClaw plugin already installed");
17554
+ } else {
17555
+ console.log(`
17556
+ \u2139 OpenClaw plugin skipped: ${openClawResult.reason}`);
17557
+ }
17558
+ console.log("\n \u2500\u2500\u2500 Prompt-level agents (rules files) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
17559
+ const cwd = process.cwd();
17560
+ const clineResult = installRulesFile(path39.join(cwd, ".clinerules"), "Cline");
17561
+ if (clineResult.result === "installed") {
17562
+ console.log(" \u2705 Cline/Roo Code rules installed (.clinerules)");
17563
+ } else if (clineResult.result === "already-present") {
17564
+ console.log(" \u2705 Cline/Roo Code rules already present (.clinerules)");
17565
+ } else {
17566
+ console.log(` \u2139 Cline/Roo Code rules skipped: ${clineResult.reason}`);
17567
+ }
17568
+ const windsurfResult = installRulesFile(path39.join(cwd, ".windsurfrules"), "Windsurf");
17569
+ if (windsurfResult.result === "installed") {
17570
+ console.log(" \u2705 Windsurf rules installed (.windsurfrules)");
17571
+ } else if (windsurfResult.result === "already-present") {
17572
+ console.log(" \u2705 Windsurf rules already present (.windsurfrules)");
17573
+ } else {
17574
+ console.log(` \u2139 Windsurf rules skipped: ${windsurfResult.reason}`);
17575
+ }
17576
+ const kiloResult = installRulesFile(
17577
+ path39.join(cwd, ".kilocode", "rules", "code-intel-rules.md"),
17578
+ "Kilo Code"
17579
+ );
17580
+ if (kiloResult.result === "installed") {
17581
+ console.log(" \u2705 Kilo Code rules installed (.kilocode/rules/code-intel-rules.md)");
17582
+ } else if (kiloResult.result === "already-present") {
17583
+ console.log(" \u2705 Kilo Code rules already present");
17584
+ } else {
17585
+ console.log(` \u2139 Kilo Code rules skipped: ${kiloResult.reason}`);
17586
+ }
17587
+ const antigravityResult = installRulesFile(
17588
+ path39.join(cwd, ".agents", "rules", "code-intel-rules.md"),
17589
+ "Antigravity"
17590
+ );
17591
+ if (antigravityResult.result === "installed") {
17592
+ console.log(" \u2705 Google Antigravity rules installed (.agents/rules/code-intel-rules.md)");
17593
+ } else if (antigravityResult.result === "already-present") {
17594
+ console.log(" \u2705 Google Antigravity rules already present");
17595
+ } else {
17596
+ console.log(` \u2139 Google Antigravity rules skipped: ${antigravityResult.reason}`);
17597
+ }
17598
+ const codexResult = installRulesFile(path39.join(cwd, "AGENTS.md"), "Codex");
17599
+ if (codexResult.result === "installed") {
17600
+ console.log(" \u2705 Codex CLI rules appended (AGENTS.md)");
17601
+ } else if (codexResult.result === "already-present") {
17602
+ console.log(" \u2705 Codex CLI rules already present (AGENTS.md)");
17603
+ } else {
17604
+ console.log(` \u2139 Codex CLI rules skipped: ${codexResult.reason}`);
17605
+ }
17606
+ console.log("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
17607
+ console.log("\n \u25C8 Setup complete. Run `code-intel analyze` to build the knowledge graph.\n");
16274
17608
  });
16275
- 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("--incremental", "Only re-parse files changed since last analysis (git diff or mtime)").option("--parallel", "Use worker threads for parse + resolve phases (faster on multi-core)").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").option("--summarize", "Generate AI summaries for function/class/method/interface nodes (opt-in)").option("--llm-provider <provider>", "LLM provider for --summarize: openai | anthropic | ollama (default: ollama)").option("--llm-model <model>", "LLM model name (e.g. gpt-4o-mini, claude-haiku-4-5, llama3)").option("--llm-batch-size <n>", "Concurrent LLM calls per batch (default: 20)", "20").option("--llm-max-nodes <n>", "Max nodes to summarize per run (cost guard)").option("--no-group-sync", "Skip automatic group sync after analysis").option("--dry-run", "Preview files that would be scanned + estimated time; no DB write").option("--max-memory <MB>", "Limit graph memory (MB); spill node content to free RAM when exceeded").option("--profile", "Write per-phase profiling data to .code-intel/profile.json").addHelpText("after", `
17609
+ 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("--incremental", "Only re-parse files changed since last analysis (git diff or mtime)").option("--parallel", "Use worker threads for parse + resolve phases (faster on multi-core)").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").option("--summarize", "Generate AI summaries for function/class/method/interface nodes (opt-in)").option("--llm-provider <provider>", "LLM provider for --summarize: openai | anthropic | ollama | custom (default: ollama)").option("--llm-model <model>", "LLM model name (e.g. gpt-4o-mini, claude-haiku-4-5, llama3, mistral)").option("--llm-base-url <url>", "Base URL for custom OpenAI-compatible API (e.g. http://localhost:1234/v1)").option("--llm-api-key <key>", "API key/token for the LLM provider (custom or override)").option("--llm-batch-size <n>", "Concurrent LLM calls per batch (default: 20)", "20").option("--llm-max-nodes <n>", "Max nodes to summarize per run (cost guard)").option("--no-group-sync", "Skip automatic group sync after analysis").option("--dry-run", "Preview files that would be scanned + estimated time; no DB write").option("--max-memory <MB>", "Limit graph memory (MB); spill node content to free RAM when exceeded").option("--profile", "Write per-phase profiling data to .code-intel/profile.json").addHelpText("after", `
16276
17610
  Parses your source code with tree-sitter, builds a Knowledge Graph of
16277
17611
  symbols and their relationships, persists it to .code-intel/graph.db,
16278
17612
  and auto-generates AGENTS.md + CLAUDE.md context blocks.
@@ -16292,6 +17626,7 @@ program.command("analyze").description("Index a repository and build the knowled
16292
17626
  $ code-intel analyze --summarize Generate AI summaries (uses Ollama by default)
16293
17627
  $ code-intel analyze --summarize --llm-provider openai --llm-model gpt-4o-mini
16294
17628
  $ code-intel analyze --summarize --llm-provider anthropic --llm-max-nodes 500
17629
+ $ code-intel analyze --summarize --llm-provider custom --llm-base-url http://localhost:1234/v1 --llm-model mistral --llm-api-key mytoken
16295
17630
  $ code-intel analyze --dry-run Preview files that would be scanned
16296
17631
  `).action(async (targetPath, opts) => {
16297
17632
  if (opts.dryRun) {
@@ -16338,6 +17673,8 @@ program.command("analyze").description("Index a repository and build the knowled
16338
17673
  const v = parseInt(opts.llmMaxNodes ?? "", 10);
16339
17674
  return Number.isFinite(v) && v >= 1 ? v : void 0;
16340
17675
  })(),
17676
+ llmBaseUrl: opts.llmBaseUrl,
17677
+ llmApiKey: opts.llmApiKey,
16341
17678
  maxMemoryMB: (() => {
16342
17679
  const v = parseInt(opts.maxMemory ?? "", 10);
16343
17680
  return Number.isFinite(v) && v >= 1 ? v : void 0;
@@ -16360,11 +17697,11 @@ program.command("mcp").description("Start MCP server over stdio \u2014 exposes a
16360
17697
  const workspaceRoot = path39.resolve(targetPath);
16361
17698
  const repoName = path39.basename(workspaceRoot);
16362
17699
  const dbPath = getDbPath(workspaceRoot);
16363
- if (!fs38.existsSync(workspaceRoot) || !fs38.statSync(workspaceRoot).isDirectory()) {
17700
+ if (!fs39.existsSync(workspaceRoot) || !fs39.statSync(workspaceRoot).isDirectory()) {
16364
17701
  console.error(` \u2717 Path does not exist: ${workspaceRoot}`);
16365
17702
  process.exit(1);
16366
17703
  }
16367
- const existingIndex = fs38.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
17704
+ const existingIndex = fs39.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
16368
17705
  if (existingIndex) {
16369
17706
  const graph = createKnowledgeGraph();
16370
17707
  const db = new DbManager(dbPath, true);
@@ -16377,11 +17714,18 @@ program.command("mcp").description("Start MCP server over stdio \u2014 exposes a
16377
17714
  await startMcpStdio(graph, name, root);
16378
17715
  }
16379
17716
  });
16380
- 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", `
17717
+ 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").option("-d, --detach", "Run server in background (detached); PID written to .code-intel/serve.pid").addHelpText("after", `
16381
17718
  If a .code-intel/graph.db index already exists for the path, the server
16382
17719
  loads the persisted graph directly and starts immediately \u2014 no re-analysis.
16383
17720
  Use --force to discard the existing index and re-analyze from scratch.
16384
17721
 
17722
+ Background mode:
17723
+ $ code-intel serve --detach # start in background
17724
+ $ code-intel serve --detach --port 8080
17725
+ $ code-intel stop # stop the background server
17726
+
17727
+ PID file: <path>/.code-intel/serve.pid
17728
+
16385
17729
  The web UI offers:
16386
17730
  \xB7 Force-directed Knowledge Graph with color-coded node types
16387
17731
  \xB7 BM25 text search + optional semantic (vector) search
@@ -16394,15 +17738,54 @@ program.command("serve").description("Start the local HTTP server + web UI for g
16394
17738
  $ code-intel serve ./my-project
16395
17739
  $ code-intel serve --port 8080
16396
17740
  $ code-intel serve --force
17741
+ $ code-intel serve --detach
16397
17742
  `).action(async (targetPath, options) => {
16398
17743
  const workspaceRoot = path39.resolve(targetPath);
16399
17744
  const repoName = path39.basename(workspaceRoot);
16400
17745
  const dbPath = getDbPath(workspaceRoot);
16401
- if (!fs38.existsSync(workspaceRoot) || !fs38.statSync(workspaceRoot).isDirectory()) {
17746
+ if (!fs39.existsSync(workspaceRoot) || !fs39.statSync(workspaceRoot).isDirectory()) {
16402
17747
  console.error(` \u2717 Path does not exist: ${workspaceRoot}`);
16403
17748
  process.exit(1);
16404
17749
  }
16405
- const existingIndex = !options.force && fs38.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
17750
+ if (options.detach) {
17751
+ const { spawn } = await import('child_process');
17752
+ const pidDir = path39.join(workspaceRoot, ".code-intel");
17753
+ const pidFile = path39.join(pidDir, "serve.pid");
17754
+ if (fs39.existsSync(pidFile)) {
17755
+ const existingPid = parseInt(fs39.readFileSync(pidFile, "utf-8").trim(), 10);
17756
+ try {
17757
+ process.kill(existingPid, 0);
17758
+ console.log(` \u26A0 Server already running (PID ${existingPid}). Stop it first with: code-intel stop`);
17759
+ process.exit(0);
17760
+ } catch {
17761
+ fs39.unlinkSync(pidFile);
17762
+ }
17763
+ }
17764
+ const selfArgs = ["serve", targetPath, "--port", options.port];
17765
+ if (options.force) selfArgs.push("--force");
17766
+ const logDir = path39.join(os18.homedir(), ".code-intel", "logs");
17767
+ fs39.mkdirSync(logDir, { recursive: true });
17768
+ const logFile = path39.join(logDir, `serve-${Date.now()}.log`);
17769
+ const logFd = fs39.openSync(logFile, "a");
17770
+ const child = spawn(process.execPath, [process.argv[1], ...selfArgs], {
17771
+ detached: true,
17772
+ stdio: ["ignore", logFd, logFd],
17773
+ env: process.env
17774
+ });
17775
+ child.unref();
17776
+ fs39.mkdirSync(pidDir, { recursive: true });
17777
+ fs39.writeFileSync(pidFile, String(child.pid), "utf-8");
17778
+ console.log(` \u2705 Server started in background`);
17779
+ console.log(` PID : ${child.pid}`);
17780
+ console.log(` Port : ${options.port}`);
17781
+ console.log(` URL : http://localhost:${options.port}`);
17782
+ console.log(` Log : ${logFile}`);
17783
+ console.log(` PID file : ${pidFile}`);
17784
+ console.log(`
17785
+ Stop with: code-intel stop`);
17786
+ process.exit(0);
17787
+ }
17788
+ const existingIndex = !options.force && fs39.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
16406
17789
  if (existingIndex) {
16407
17790
  const meta = loadMetadata(workspaceRoot);
16408
17791
  if (meta.parser === "regex" || meta.parser === void 0) {
@@ -16421,10 +17804,81 @@ program.command("serve").description("Start the local HTTP server + web UI for g
16421
17804
  await startHttpServer(lazyGraph, repoName, parseInt(options.port, 10), workspaceRoot);
16422
17805
  }
16423
17806
  } else {
16424
- logger_default.warn(` [serve] No index found for: ${workspaceRoot}`);
16425
- logger_default.warn(` [serve] Run \`code-intel analyze\` first, then re-run \`code-intel serve\`.`);
17807
+ console.log(` \u2139 No index found for: ${workspaceRoot}`);
17808
+ const registry = loadRegistry();
17809
+ const available = registry.filter((r) => fs39.existsSync(getDbPath(r.path)) && loadMetadata(r.path) !== null).sort((a, b) => new Date(b.indexedAt).getTime() - new Date(a.indexedAt).getTime());
17810
+ if (available.length > 0) {
17811
+ const fallback = available[0];
17812
+ console.log(` \u25C8 Falling back to most recently indexed repo: ${fallback.path}`);
17813
+ console.log(` (${fallback.stats.nodes} nodes \xB7 ${fallback.stats.edges} edges \xB7 indexed ${fallback.indexedAt})`);
17814
+ console.log(` \u2139 To index this folder run: code-intel analyze
17815
+ `);
17816
+ const fallbackMeta = loadMetadata(fallback.path);
17817
+ const fallbackDbPath = getDbPath(fallback.path);
17818
+ if (fallbackMeta.parser === "regex" || fallbackMeta.parser === void 0) {
17819
+ console.log(` \u26A0 Fallback index was built with regex parser. Starting with empty graph.`);
17820
+ const emptyGraph = createKnowledgeGraph();
17821
+ await startHttpServer(emptyGraph, fallback.name, parseInt(options.port, 10), fallback.path);
17822
+ } else {
17823
+ const lazyGraph = new LazyKnowledgeGraph();
17824
+ const db = new DbManager(fallbackDbPath, true);
17825
+ await db.init();
17826
+ await lazyGraph.init(db, fallbackMeta.stats.nodes, fallbackMeta.stats.edges);
17827
+ setImmediate(() => lazyGraph.warmTopNodes(500).catch(() => {
17828
+ }));
17829
+ await startHttpServer(lazyGraph, fallback.name, parseInt(options.port, 10), fallback.path);
17830
+ }
17831
+ if (available.length > 1) {
17832
+ console.log(`
17833
+ Other indexed repos available (switch via web UI or re-run serve with path):`);
17834
+ for (const r of available.slice(1)) {
17835
+ console.log(` code-intel serve ${r.path}`);
17836
+ }
17837
+ }
17838
+ } else {
17839
+ console.log(` \u2139 No indexed repositories found. Starting server with empty graph.`);
17840
+ console.log(` Run \`code-intel analyze\` to index a repository, then reload the UI.
17841
+ `);
17842
+ const emptyGraph = createKnowledgeGraph();
17843
+ await startHttpServer(emptyGraph, repoName, parseInt(options.port, 10), workspaceRoot);
17844
+ }
17845
+ }
17846
+ });
17847
+ program.command("stop").description("Stop a background server started with `code-intel serve --detach`").argument("[path]", "Project path whose server to stop (default: current directory)", ".").addHelpText("after", `
17848
+ Reads the PID from <path>/.code-intel/serve.pid and sends SIGTERM.
17849
+ The PID file is removed automatically after a successful stop.
17850
+
17851
+ Examples:
17852
+ $ code-intel stop
17853
+ $ code-intel stop ./my-project
17854
+ `).action((targetPath) => {
17855
+ const workspaceRoot = path39.resolve(targetPath);
17856
+ const pidFile = path39.join(workspaceRoot, ".code-intel", "serve.pid");
17857
+ if (!fs39.existsSync(pidFile)) {
17858
+ console.log(` \u2139 No background server found for: ${workspaceRoot}`);
17859
+ console.log(` (PID file not present: ${pidFile})`);
17860
+ process.exit(0);
17861
+ }
17862
+ const pid = parseInt(fs39.readFileSync(pidFile, "utf-8").trim(), 10);
17863
+ if (isNaN(pid)) {
17864
+ console.error(` \u2717 Invalid PID file: ${pidFile}`);
17865
+ fs39.unlinkSync(pidFile);
16426
17866
  process.exit(1);
16427
17867
  }
17868
+ try {
17869
+ process.kill(pid, "SIGTERM");
17870
+ fs39.unlinkSync(pidFile);
17871
+ console.log(` \u2705 Stopped server (PID ${pid})`);
17872
+ } catch (err) {
17873
+ const code = err.code;
17874
+ if (code === "ESRCH") {
17875
+ fs39.unlinkSync(pidFile);
17876
+ console.log(` \u2139 Server (PID ${pid}) was not running. Stale PID file removed.`);
17877
+ } else {
17878
+ console.error(` \u2717 Failed to stop server (PID ${pid}): ${err.message}`);
17879
+ process.exit(1);
17880
+ }
17881
+ }
16428
17882
  });
16429
17883
  program.command("watch").description("Start HTTP server + file watcher (auto-reindex on file changes)").argument("[path]", "Path to watch (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", `
16430
17884
  Starts the HTTP server and automatically re-indexes changed files.
@@ -16440,11 +17894,11 @@ program.command("watch").description("Start HTTP server + file watcher (auto-rei
16440
17894
  const workspaceRoot = path39.resolve(targetPath);
16441
17895
  const repoName = path39.basename(workspaceRoot);
16442
17896
  const dbPath = getDbPath(workspaceRoot);
16443
- if (!fs38.existsSync(workspaceRoot) || !fs38.statSync(workspaceRoot).isDirectory()) {
17897
+ if (!fs39.existsSync(workspaceRoot) || !fs39.statSync(workspaceRoot).isDirectory()) {
16444
17898
  console.error(` \u2717 Path does not exist: ${workspaceRoot}`);
16445
17899
  process.exit(1);
16446
17900
  }
16447
- const existingIndex = !options.force && fs38.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
17901
+ const existingIndex = !options.force && fs39.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
16448
17902
  let graph;
16449
17903
  if (existingIndex) {
16450
17904
  const meta = loadMetadata(workspaceRoot);
@@ -16564,16 +18018,16 @@ function trashDirName(repoPath) {
16564
18018
  }
16565
18019
  function softDeleteCodeIntel(repoPath) {
16566
18020
  const codeIntelDir = path39.join(repoPath, ".code-intel");
16567
- if (!fs38.existsSync(codeIntelDir)) return;
18021
+ if (!fs39.existsSync(codeIntelDir)) return;
16568
18022
  const trashName = trashDirName();
16569
18023
  const trashDir = path39.join(repoPath, trashName);
16570
18024
  let dest = trashDir;
16571
18025
  let counter = 1;
16572
- while (fs38.existsSync(dest)) {
18026
+ while (fs39.existsSync(dest)) {
16573
18027
  dest = `${trashDir}-${counter++}`;
16574
18028
  }
16575
- fs38.renameSync(codeIntelDir, dest);
16576
- fs38.writeFileSync(
18029
+ fs39.renameSync(codeIntelDir, dest);
18030
+ fs39.writeFileSync(
16577
18031
  path39.join(dest, "TRASH_META.json"),
16578
18032
  JSON.stringify({ deletedAt: (/* @__PURE__ */ new Date()).toISOString(), repoPath, permanent: false }, null, 2)
16579
18033
  );
@@ -16583,15 +18037,15 @@ function softDeleteCodeIntel(repoPath) {
16583
18037
  function purgeStaleTrashes(repoPath) {
16584
18038
  const cutoff = Date.now() - TRASH_TTL_DAYS * 24 * 60 * 60 * 1e3;
16585
18039
  try {
16586
- for (const entry of fs38.readdirSync(repoPath)) {
18040
+ for (const entry of fs39.readdirSync(repoPath)) {
16587
18041
  if (!entry.startsWith(".code-intel-trash-")) continue;
16588
18042
  const fullPath = path39.join(repoPath, entry);
16589
18043
  const metaPath = path39.join(fullPath, "TRASH_META.json");
16590
- if (fs38.existsSync(metaPath)) {
18044
+ if (fs39.existsSync(metaPath)) {
16591
18045
  try {
16592
- const meta = JSON.parse(fs38.readFileSync(metaPath, "utf-8"));
18046
+ const meta = JSON.parse(fs39.readFileSync(metaPath, "utf-8"));
16593
18047
  if (new Date(meta.deletedAt).getTime() < cutoff) {
16594
- fs38.rmSync(fullPath, { recursive: true, force: true });
18048
+ fs39.rmSync(fullPath, { recursive: true, force: true });
16595
18049
  console.log(` \u2713 Auto-purged stale trash: ${fullPath}`);
16596
18050
  }
16597
18051
  } catch {
@@ -16618,18 +18072,18 @@ program.command("clean").description("Soft-delete the knowledge graph index for
16618
18072
  if (opts.dryRun) {
16619
18073
  const showDryRun = (repoPath) => {
16620
18074
  const codeIntelDir = path39.join(path39.resolve(repoPath), ".code-intel");
16621
- if (!fs38.existsSync(codeIntelDir)) {
18075
+ if (!fs39.existsSync(codeIntelDir)) {
16622
18076
  console.log(` (no .code-intel/ found at ${repoPath})`);
16623
18077
  return;
16624
18078
  }
16625
18079
  let totalBytes = 0;
16626
18080
  const countDir = (dir) => {
16627
18081
  try {
16628
- for (const entry of fs38.readdirSync(dir, { withFileTypes: true })) {
18082
+ for (const entry of fs39.readdirSync(dir, { withFileTypes: true })) {
16629
18083
  const full = path39.join(dir, entry.name);
16630
18084
  if (entry.isDirectory()) countDir(full);
16631
18085
  else try {
16632
- totalBytes += fs38.statSync(full).size;
18086
+ totalBytes += fs39.statSync(full).size;
16633
18087
  } catch {
16634
18088
  }
16635
18089
  }
@@ -16666,14 +18120,14 @@ program.command("clean").description("Soft-delete the knowledge graph index for
16666
18120
  let found = 0;
16667
18121
  for (const root of roots) {
16668
18122
  try {
16669
- for (const entry of fs38.readdirSync(root)) {
18123
+ for (const entry of fs39.readdirSync(root)) {
16670
18124
  if (!entry.startsWith(".code-intel-trash-")) continue;
16671
18125
  const fullPath = path39.join(root, entry);
16672
18126
  const metaPath = path39.join(fullPath, "TRASH_META.json");
16673
18127
  let deletedAt = "unknown";
16674
- if (fs38.existsSync(metaPath)) {
18128
+ if (fs39.existsSync(metaPath)) {
16675
18129
  try {
16676
- deletedAt = JSON.parse(fs38.readFileSync(metaPath, "utf-8")).deletedAt;
18130
+ deletedAt = JSON.parse(fs39.readFileSync(metaPath, "utf-8")).deletedAt;
16677
18131
  } catch {
16678
18132
  }
16679
18133
  }
@@ -16702,8 +18156,8 @@ program.command("clean").description("Soft-delete the knowledge graph index for
16702
18156
  for (const r of repos) {
16703
18157
  if (opts.purge) {
16704
18158
  const codeIntelDir = path39.join(r.path, ".code-intel");
16705
- if (fs38.existsSync(codeIntelDir)) {
16706
- fs38.rmSync(codeIntelDir, { recursive: true, force: true });
18159
+ if (fs39.existsSync(codeIntelDir)) {
18160
+ fs39.rmSync(codeIntelDir, { recursive: true, force: true });
16707
18161
  console.log(` \u2713 Hard-deleted ${codeIntelDir}`);
16708
18162
  }
16709
18163
  } else {
@@ -16720,8 +18174,8 @@ program.command("clean").description("Soft-delete the knowledge graph index for
16720
18174
  const workspaceRoot = path39.resolve(targetPath);
16721
18175
  if (opts.purge) {
16722
18176
  const codeIntelDir = path39.join(workspaceRoot, ".code-intel");
16723
- if (fs38.existsSync(codeIntelDir)) {
16724
- fs38.rmSync(codeIntelDir, { recursive: true, force: true });
18177
+ if (fs39.existsSync(codeIntelDir)) {
18178
+ fs39.rmSync(codeIntelDir, { recursive: true, force: true });
16725
18179
  console.log(`
16726
18180
  \u2713 Hard-deleted ${codeIntelDir}`);
16727
18181
  }
@@ -16735,7 +18189,7 @@ program.command("clean").description("Soft-delete the knowledge graph index for
16735
18189
  async function loadOrAnalyzeWorkspace(targetPath) {
16736
18190
  const workspaceRoot = path39.resolve(targetPath);
16737
18191
  const dbPath = getDbPath(workspaceRoot);
16738
- const existingIndex = fs38.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
18192
+ const existingIndex = fs39.existsSync(dbPath) && loadMetadata(workspaceRoot) !== null;
16739
18193
  if (existingIndex) {
16740
18194
  const graph = createKnowledgeGraph();
16741
18195
  const db = new DbManager(dbPath, true);
@@ -17223,7 +18677,7 @@ groupCmd.command("status <name>").description("Check index freshness and sync st
17223
18677
  }
17224
18678
  const metaPath = path39.join(regEntry.path, ".code-intel", "meta.json");
17225
18679
  try {
17226
- const meta = JSON.parse(fs38.readFileSync(metaPath, "utf-8"));
18680
+ const meta = JSON.parse(fs39.readFileSync(metaPath, "utf-8"));
17227
18681
  const indexedAt = meta.indexedAt;
17228
18682
  const ageMin = Math.round((now - new Date(indexedAt).getTime()) / 6e4);
17229
18683
  const stale = ageMin > 1440 ? " \u26A0 STALE (>24h)" : "";
@@ -17557,7 +19011,7 @@ backupCmd.command("list").description("List all available backups").action(() =>
17557
19011
  Backups (${entries.length}):
17558
19012
  `);
17559
19013
  for (const e of entries) {
17560
- const exists = fs38.existsSync(e.path);
19014
+ const exists = fs39.existsSync(e.path);
17561
19015
  const status = exists ? "\u2713" : "\u2717 (missing)";
17562
19016
  console.log(` ${status} ${e.id.slice(0, 8)} ${e.createdAt} ${(e.size / 1024).toFixed(1)} KB \u2192 ${e.repoPath}`);
17563
19017
  }
@@ -17589,8 +19043,8 @@ program.command("migrate").description("Manage database schema migrations").opti
17589
19043
  $ code-intel migrate
17590
19044
  $ code-intel migrate --rollback
17591
19045
  `).action((opts) => {
17592
- const dbPath = opts.db ?? path39.join(os13.homedir(), ".code-intel", "users.db");
17593
- if (!fs38.existsSync(dbPath)) {
19046
+ const dbPath = opts.db ?? path39.join(os18.homedir(), ".code-intel", "users.db");
19047
+ if (!fs39.existsSync(dbPath)) {
17594
19048
  console.error(`
17595
19049
  \u2717 Database not found: ${dbPath}
17596
19050
  Run \`code-intel serve\` or \`code-intel user create\` first.
@@ -17705,15 +19159,15 @@ authCmd.command("login").description("Authenticate via OIDC device flow \u2014 o
17705
19159
  }
17706
19160
  try {
17707
19161
  const tokens = await pollDeviceFlow3(config, deviceResponse);
17708
- const tokenPath = path39.join(os13.homedir(), ".code-intel", "oidc-token.json");
19162
+ const tokenPath = path39.join(os18.homedir(), ".code-intel", "oidc-token.json");
17709
19163
  const tokenData = {
17710
19164
  accessToken: tokens.accessToken,
17711
19165
  refreshToken: tokens.refreshToken,
17712
19166
  server: serverUrl,
17713
19167
  storedAt: (/* @__PURE__ */ new Date()).toISOString()
17714
19168
  };
17715
- fs38.mkdirSync(path39.dirname(tokenPath), { recursive: true });
17716
- fs38.writeFileSync(tokenPath, JSON.stringify(tokenData, null, 2), { mode: 384 });
19169
+ fs39.mkdirSync(path39.dirname(tokenPath), { recursive: true });
19170
+ fs39.writeFileSync(tokenPath, JSON.stringify(tokenData, null, 2), { mode: 384 });
17717
19171
  console.log(` \u2705 Authenticated successfully!`);
17718
19172
  console.log(` Token stored at: ${tokenPath}`);
17719
19173
  console.log(` Use CODE_INTEL_TOKEN env var or --token flag to use it with CLI/MCP.
@@ -17726,13 +19180,13 @@ authCmd.command("login").description("Authenticate via OIDC device flow \u2014 o
17726
19180
  }
17727
19181
  });
17728
19182
  authCmd.command("status").description("Show the current OIDC authentication status").action(() => {
17729
- const tokenPath = path39.join(os13.homedir(), ".code-intel", "oidc-token.json");
17730
- if (!fs38.existsSync(tokenPath)) {
19183
+ const tokenPath = path39.join(os18.homedir(), ".code-intel", "oidc-token.json");
19184
+ if (!fs39.existsSync(tokenPath)) {
17731
19185
  console.log("\n Not authenticated via OIDC. Run: code-intel auth login\n");
17732
19186
  return;
17733
19187
  }
17734
19188
  try {
17735
- const data = JSON.parse(fs38.readFileSync(tokenPath, "utf-8"));
19189
+ const data = JSON.parse(fs39.readFileSync(tokenPath, "utf-8"));
17736
19190
  console.log(`
17737
19191
  \u2705 OIDC token stored`);
17738
19192
  console.log(` Server : ${data.server ?? "unknown"}`);
@@ -17744,9 +19198,9 @@ authCmd.command("status").description("Show the current OIDC authentication stat
17744
19198
  }
17745
19199
  });
17746
19200
  authCmd.command("logout").description("Remove locally stored OIDC token").action(() => {
17747
- const tokenPath = path39.join(os13.homedir(), ".code-intel", "oidc-token.json");
17748
- if (fs38.existsSync(tokenPath)) {
17749
- fs38.unlinkSync(tokenPath);
19201
+ const tokenPath = path39.join(os18.homedir(), ".code-intel", "oidc-token.json");
19202
+ if (fs39.existsSync(tokenPath)) {
19203
+ fs39.unlinkSync(tokenPath);
17750
19204
  console.log("\n \u2705 OIDC token removed. You are now logged out.\n");
17751
19205
  } else {
17752
19206
  console.log("\n No stored token found.\n");
@@ -17871,7 +19325,7 @@ program.command("config-validate <file>").description("Validate a JSON config fi
17871
19325
  $ code-intel config-validate ~/.code-intel/config.json
17872
19326
  `).action((file) => {
17873
19327
  const filePath = path39.resolve(file);
17874
- if (!fs38.existsSync(filePath)) {
19328
+ if (!fs39.existsSync(filePath)) {
17875
19329
  console.error(`
17876
19330
  \u2717 File not found: ${filePath}
17877
19331
  `);
@@ -17879,7 +19333,7 @@ program.command("config-validate <file>").description("Validate a JSON config fi
17879
19333
  }
17880
19334
  let cfg;
17881
19335
  try {
17882
- cfg = JSON.parse(fs38.readFileSync(filePath, "utf-8"));
19336
+ cfg = JSON.parse(fs39.readFileSync(filePath, "utf-8"));
17883
19337
  } catch (err) {
17884
19338
  console.error(`
17885
19339
  \u2717 Could not parse JSON: ${err instanceof Error ? err.message : err}
@@ -17900,7 +19354,7 @@ ${err instanceof Error ? err.message : err}
17900
19354
  });
17901
19355
  (function ensurePermissions() {
17902
19356
  try {
17903
- const dir = path39.join(os13.homedir(), ".code-intel");
19357
+ const dir = path39.join(os18.homedir(), ".code-intel");
17904
19358
  secureMkdir(dir);
17905
19359
  tightenDbFiles(dir);
17906
19360
  } catch {
@@ -17920,7 +19374,7 @@ program.command("health").description("Run code health checks: dead code, circul
17920
19374
  const workspaceRoot = path39.resolve(targetPath);
17921
19375
  const dbPath = getDbPath(workspaceRoot);
17922
19376
  const meta = loadMetadata(workspaceRoot);
17923
- if (!meta || !fs38.existsSync(dbPath)) {
19377
+ if (!meta || !fs39.existsSync(dbPath)) {
17924
19378
  console.error(`
17925
19379
  \u2717 ${workspaceRoot} is not indexed.`);
17926
19380
  console.error(" Run `code-intel analyze` first to build the index.\n");
@@ -18065,13 +19519,13 @@ program.command("query").description("Execute a GQL (Graph Query Language) query
18065
19519
  gqlInput = content;
18066
19520
  } else if (opts.file) {
18067
19521
  const filePath = path39.resolve(opts.file);
18068
- if (!fs38.existsSync(filePath)) {
19522
+ if (!fs39.existsSync(filePath)) {
18069
19523
  console.error(`
18070
19524
  \u2717 File not found: ${filePath}
18071
19525
  `);
18072
19526
  process.exit(1);
18073
19527
  }
18074
- gqlInput = fs38.readFileSync(filePath, "utf-8");
19528
+ gqlInput = fs39.readFileSync(filePath, "utf-8");
18075
19529
  } else if (gqlArg) {
18076
19530
  gqlInput = gqlArg;
18077
19531
  } else {
@@ -18599,7 +20053,7 @@ program.command("doctor").description("Run diagnostics \u2014 check Node.js, git
18599
20053
  continue;
18600
20054
  }
18601
20055
  const vdbPath = getVectorDbPath2(repo.path);
18602
- if (fs38.existsSync(vdbPath)) {
20056
+ if (fs39.existsSync(vdbPath)) {
18603
20057
  try {
18604
20058
  const Database22 = (await import('better-sqlite3')).default;
18605
20059
  const vdb = new Database22(vdbPath, { readonly: true, fileMustExist: true });
@@ -18636,6 +20090,104 @@ program.command("doctor").description("Run diagnostics \u2014 check Node.js, git
18636
20090
  console.log(" \u2705 All checks passed.\n");
18637
20091
  }
18638
20092
  });
20093
+ program.command("context").description("Build a token-efficient context document for a set of seed symbols").argument("<symbols...>", "One or more symbol names to use as seeds").option("-p, --path <path>", "Path to the repository (default: current directory)", ".").option("-l, --limit <n>", "Max seeds to resolve (default: 10)", "10").option("--intent <intent>", "Query intent: code | callers | architecture | auto (default: auto)", "auto").option("--max-tokens <n>", "Max total tokens for output (default: 6000)", "6000").option("--show-context", "Print per-block token breakdown after output").addHelpText("after", `
20094
+ Builds a structured [SUMMARY] / [LOGIC] / [RELATION] / [FOCUS CODE] context
20095
+ document for one or more symbols. Designed to give AI assistants dense,
20096
+ token-efficient context instead of raw file reads.
20097
+
20098
+ Examples:
20099
+ $ code-intel context UserService
20100
+ $ code-intel context createUser login --intent callers
20101
+ $ code-intel context AuthService --show-context
20102
+ $ code-intel context handlePayment --max-tokens 3000 --intent code
20103
+ `).action(async (symbols, opts) => {
20104
+ const { graph } = await loadOrAnalyzeWorkspace(opts.path);
20105
+ const { build: build2, detectQueryIntent: detectQueryIntent2 } = await Promise.resolve().then(() => (init_builder(), builder_exports));
20106
+ const { measureBlocks: measureBlocks2 } = await Promise.resolve().then(() => (init_token_counter(), token_counter_exports));
20107
+ const maxSeeds = parseInt(opts.limit, 10);
20108
+ const maxTokens = parseInt(opts.maxTokens, 10);
20109
+ const intent = ["code", "callers", "architecture", "auto"].includes(opts.intent) ? opts.intent : detectQueryIntent2(opts.intent);
20110
+ const seeds = [];
20111
+ for (const symbol of symbols.slice(0, maxSeeds)) {
20112
+ for (const node of graph.allNodes()) {
20113
+ if (node.name === symbol) {
20114
+ seeds.push({ nodeId: node.id, refinedScore: 1 });
20115
+ break;
20116
+ }
20117
+ }
20118
+ }
20119
+ if (seeds.length === 0) {
20120
+ console.log(`
20121
+ No symbols found for: ${symbols.join(", ")}
20122
+ `);
20123
+ console.log(' Try: code-intel search "<name>" to find symbol names.\n');
20124
+ return;
20125
+ }
20126
+ const doc = build2(seeds, graph, { maxTokens, queryIntent: intent });
20127
+ console.log("");
20128
+ if (doc.summary) {
20129
+ console.log(doc.summary);
20130
+ console.log("");
20131
+ }
20132
+ if (doc.logic) {
20133
+ console.log(doc.logic);
20134
+ console.log("");
20135
+ }
20136
+ if (doc.relation) {
20137
+ console.log(doc.relation);
20138
+ console.log("");
20139
+ }
20140
+ if (doc.focusCode) {
20141
+ console.log(doc.focusCode);
20142
+ console.log("");
20143
+ }
20144
+ if (doc.truncated) {
20145
+ console.log(" \u26A0 Output was truncated due to token budget. Use --max-tokens to increase.");
20146
+ console.log("");
20147
+ }
20148
+ if (opts.showContext) {
20149
+ const counts = measureBlocks2(doc);
20150
+ console.log(" \u2500\u2500 Token breakdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
20151
+ console.log(` [SUMMARY] ${String(counts.summary).padStart(5)} tokens`);
20152
+ console.log(` [LOGIC] ${String(counts.logic).padStart(5)} tokens`);
20153
+ console.log(` [RELATION] ${String(counts.relation).padStart(5)} tokens`);
20154
+ console.log(` [FOCUS CODE] ${String(counts.focusCode).padStart(5)} tokens`);
20155
+ console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
20156
+ console.log(` Total ${String(counts.total).padStart(5)} tokens (budget: ${maxTokens})`);
20157
+ console.log(` Intent ${doc.intent}`);
20158
+ console.log(` Truncated ${doc.truncated}`);
20159
+ console.log("");
20160
+ }
20161
+ });
20162
+ {
20163
+ const rewriteCmd = new Command("rewrite").description("Rewrite a shell command to its code-intel equivalent (used by hooks)").argument("<cmd>", "Raw shell command to check").addHelpText("after", `
20164
+ Exit codes:
20165
+ 0 Rewrite found \u2014 rewritten command printed to stdout
20166
+ 1 No code-intel equivalent \u2014 pass through unchanged
20167
+
20168
+ Examples:
20169
+ $ code-intel rewrite 'grep "AuthService" src/'
20170
+ code-intel search "AuthService"
20171
+
20172
+ $ code-intel rewrite 'git status'
20173
+ (no output, exit 1)
20174
+ `).action((cmd) => {
20175
+ runRewrite(cmd);
20176
+ });
20177
+ program.addCommand(rewriteCmd, { hidden: true });
20178
+ }
20179
+ {
20180
+ const hookCmd = new Command("hook").description("Run as a PreToolUse hook for an AI agent (reads/writes JSON on stdin/stdout)").argument("<agent>", "Agent type: claude").action((agent) => {
20181
+ if (agent === "claude") {
20182
+ runClaudeHook();
20183
+ } else {
20184
+ process.stderr.write(`[code-intel] Unknown hook agent: ${agent}
20185
+ `);
20186
+ process.exit(0);
20187
+ }
20188
+ });
20189
+ program.addCommand(hookCmd, { hidden: true });
20190
+ }
18639
20191
  program.parse();
18640
20192
  //# sourceMappingURL=main.js.map
18641
20193
  //# sourceMappingURL=main.js.map