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