codesearch-mcp 0.0.1-alpha
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 +129 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +124 -0
- package/dist/src/ast.d.ts +2 -0
- package/dist/src/ast.js +60 -0
- package/dist/src/config.d.ts +26 -0
- package/dist/src/config.js +60 -0
- package/dist/src/db/data-source.d.ts +2 -0
- package/dist/src/db/data-source.js +13 -0
- package/dist/src/db/entities/code-chunk.entity.d.ts +9 -0
- package/dist/src/db/entities/code-chunk.entity.js +54 -0
- package/dist/src/db/entities/code-dependency.entity.d.ts +9 -0
- package/dist/src/db/entities/code-dependency.entity.js +54 -0
- package/dist/src/db/index.d.ts +4 -0
- package/dist/src/db/index.js +12 -0
- package/dist/src/db/init.d.ts +2 -0
- package/dist/src/db/init.js +50 -0
- package/dist/src/env-loader.d.ts +1 -0
- package/dist/src/env-loader.js +41 -0
- package/dist/src/file-utils.d.ts +12 -0
- package/dist/src/file-utils.js +97 -0
- package/dist/src/graph.d.ts +4 -0
- package/dist/src/graph.js +259 -0
- package/dist/src/indexer.d.ts +13 -0
- package/dist/src/indexer.js +217 -0
- package/dist/src/interrupt.d.ts +3 -0
- package/dist/src/interrupt.js +27 -0
- package/dist/src/mcp.d.ts +1 -0
- package/dist/src/mcp.js +248 -0
- package/dist/src/project-selector.d.ts +7 -0
- package/dist/src/project-selector.js +116 -0
- package/dist/src/searcher.d.ts +50 -0
- package/dist/src/searcher.js +205 -0
- package/dist/src/setup.d.ts +1 -0
- package/dist/src/setup.js +145 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +9 -0
- package/docker/docker-compose.yml +19 -0
- package/package.json +49 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const dotenv = __importStar(require("dotenv"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
// Global config first, then local .env overrides (for development)
|
|
40
|
+
dotenv.config({ path: path.join(os.homedir(), '.config', 'codesearch-mcp', '.env') });
|
|
41
|
+
dotenv.config();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ignore from 'ignore';
|
|
2
|
+
export type Relations = Record<string, string | string[]>;
|
|
3
|
+
export interface ProjectMeta {
|
|
4
|
+
description: string | null;
|
|
5
|
+
stack: string | null;
|
|
6
|
+
notes: string | null;
|
|
7
|
+
relations: Relations | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function readRagMeta(projectDir: string): ProjectMeta | null;
|
|
10
|
+
export declare const IGNORE_DIRS: Set<string>;
|
|
11
|
+
export declare function loadIgnore(projectDir: string): ReturnType<typeof ignore>;
|
|
12
|
+
export declare function collectFiles(dir: string, ig: ReturnType<typeof ignore>, rootDir: string): string[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.IGNORE_DIRS = void 0;
|
|
40
|
+
exports.readRagMeta = readRagMeta;
|
|
41
|
+
exports.loadIgnore = loadIgnore;
|
|
42
|
+
exports.collectFiles = collectFiles;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const ignore_1 = __importDefault(require("ignore"));
|
|
46
|
+
// Returns null if file is missing or contains invalid JSON.
|
|
47
|
+
// Returns ProjectMeta (fields may be null) if file exists and parses correctly.
|
|
48
|
+
function readRagMeta(projectDir) {
|
|
49
|
+
const metaPath = path.join(projectDir, '.ragmeta.json');
|
|
50
|
+
if (!fs.existsSync(metaPath))
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
const raw = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
54
|
+
return {
|
|
55
|
+
description: raw.description ?? null,
|
|
56
|
+
stack: raw.stack ?? null,
|
|
57
|
+
notes: raw.notes ?? null,
|
|
58
|
+
relations: raw.relations ?? null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.IGNORE_DIRS = new Set([
|
|
66
|
+
'node_modules', '.git', 'dist', 'build', '.next', 'out',
|
|
67
|
+
'coverage', '.cache', '.turbo', 'migrations', '__generated__',
|
|
68
|
+
'.idea', '.vscode', 'public', 'uploads',
|
|
69
|
+
]);
|
|
70
|
+
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|prisma|graphql|gql)$/;
|
|
71
|
+
const IGNORE_EXTENSIONS = /\.d\.ts$/;
|
|
72
|
+
function loadIgnore(projectDir) {
|
|
73
|
+
const ig = (0, ignore_1.default)();
|
|
74
|
+
const ragIgnorePath = path.join(projectDir, '.ragignore');
|
|
75
|
+
if (fs.existsSync(ragIgnorePath)) {
|
|
76
|
+
ig.add(fs.readFileSync(ragIgnorePath, 'utf-8'));
|
|
77
|
+
}
|
|
78
|
+
return ig;
|
|
79
|
+
}
|
|
80
|
+
function collectFiles(dir, ig, rootDir) {
|
|
81
|
+
const result = [];
|
|
82
|
+
for (const file of fs.readdirSync(dir)) {
|
|
83
|
+
const fullPath = path.join(dir, file);
|
|
84
|
+
const relPath = path.relative(rootDir, fullPath);
|
|
85
|
+
if (ig.ignores(relPath))
|
|
86
|
+
continue;
|
|
87
|
+
const stat = fs.statSync(fullPath);
|
|
88
|
+
if (stat.isDirectory()) {
|
|
89
|
+
if (!exports.IGNORE_DIRS.has(file))
|
|
90
|
+
result.push(...collectFiles(fullPath, ig, rootDir));
|
|
91
|
+
}
|
|
92
|
+
else if (SOURCE_EXTENSIONS.test(file) && !IGNORE_EXTENSIONS.test(file)) {
|
|
93
|
+
result.push(fullPath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.generateProjectGraph = generateProjectGraph;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const file_utils_1 = require("./file-utils");
|
|
40
|
+
function buildEdges(nodes) {
|
|
41
|
+
const edges = [];
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
if (!node.relations)
|
|
44
|
+
continue;
|
|
45
|
+
for (const [label, targets] of Object.entries(node.relations)) {
|
|
46
|
+
const list = Array.isArray(targets) ? targets : [targets];
|
|
47
|
+
for (const to of list)
|
|
48
|
+
edges.push({ from: node.name, to, label });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return edges;
|
|
52
|
+
}
|
|
53
|
+
function escHtml(s) {
|
|
54
|
+
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
55
|
+
}
|
|
56
|
+
function printConsole(nodes, edges, workspacePath) {
|
|
57
|
+
if (edges.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
console.log('\n=== Project Relations ===');
|
|
60
|
+
const grouped = new Map();
|
|
61
|
+
for (const e of edges) {
|
|
62
|
+
if (!grouped.has(e.from))
|
|
63
|
+
grouped.set(e.from, []);
|
|
64
|
+
grouped.get(e.from).push(e);
|
|
65
|
+
}
|
|
66
|
+
for (const [from, fromEdges] of grouped) {
|
|
67
|
+
const byLabel = new Map();
|
|
68
|
+
for (const e of fromEdges) {
|
|
69
|
+
if (!byLabel.has(e.label))
|
|
70
|
+
byLabel.set(e.label, []);
|
|
71
|
+
byLabel.get(e.label).push(e);
|
|
72
|
+
}
|
|
73
|
+
for (const [label, labelEdges] of byLabel) {
|
|
74
|
+
const targets = labelEdges.map(e => e.to).join(', ');
|
|
75
|
+
console.log(` ${from.padEnd(30)} → ${targets.padEnd(30)} [${label}]`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
console.log(`\n Full graph: file://${path.join(workspacePath, 'project-graph.html')}`);
|
|
79
|
+
}
|
|
80
|
+
function buildHtml(nodes, edges) {
|
|
81
|
+
const visNodes = nodes.map(n => ({
|
|
82
|
+
id: n.name,
|
|
83
|
+
label: n.name,
|
|
84
|
+
title: [n.description, n.stack].filter(Boolean).join('\n') || n.name,
|
|
85
|
+
color: n.relations ? '#4A90D9' : '#A8C7E8',
|
|
86
|
+
}));
|
|
87
|
+
const known = new Set(nodes.map(n => n.name));
|
|
88
|
+
for (const e of edges) {
|
|
89
|
+
if (!known.has(e.to)) {
|
|
90
|
+
visNodes.push({ id: e.to, label: e.to, title: e.to, color: '#E8A838' });
|
|
91
|
+
known.add(e.to);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const visEdges = edges.map((e, i) => ({ id: i, from: e.from, to: e.to, label: e.label, arrows: 'to' }));
|
|
95
|
+
const allLabels = [...new Set(edges.map(e => e.label))].sort();
|
|
96
|
+
return `<!DOCTYPE html>
|
|
97
|
+
<html lang="en">
|
|
98
|
+
<head>
|
|
99
|
+
<meta charset="UTF-8">
|
|
100
|
+
<title>Project Graph</title>
|
|
101
|
+
<script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
|
|
102
|
+
<style>
|
|
103
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
104
|
+
body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #eee; display: flex; flex-direction: column; height: 100vh; }
|
|
105
|
+
#header { padding: 14px 20px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; align-items: center; gap: 20px; flex-shrink: 0; }
|
|
106
|
+
#header h1 { font-size: 16px; font-weight: 600; white-space: nowrap; }
|
|
107
|
+
#header p { font-size: 12px; color: #888; }
|
|
108
|
+
#filters { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }
|
|
109
|
+
.filter-btn {
|
|
110
|
+
padding: 4px 10px; border-radius: 12px; border: 1px solid #4A90D9;
|
|
111
|
+
background: #4A90D9; color: #fff; font-size: 11px; cursor: pointer; user-select: none; transition: opacity .15s;
|
|
112
|
+
}
|
|
113
|
+
.filter-btn.off { background: transparent; color: #4A90D9; opacity: .6; }
|
|
114
|
+
#main { display: flex; flex: 1; overflow: hidden; }
|
|
115
|
+
#network { flex: 1; }
|
|
116
|
+
#legend { width: 200px; background: #16213e; border-left: 1px solid #0f3460; padding: 16px 14px; font-size: 12px; flex-shrink: 0; overflow-y: auto; }
|
|
117
|
+
#legend h3 { font-size: 11px; text-transform: uppercase; color: #888; margin-bottom: 10px; letter-spacing: .5px; }
|
|
118
|
+
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
|
|
119
|
+
#legend .entry { margin-bottom: 8px; }
|
|
120
|
+
#hint { margin-top: 16px; padding-top: 12px; border-top: 1px solid #0f3460; color: #666; line-height: 1.5; }
|
|
121
|
+
#project-list { margin-top: 16px; padding-top: 12px; border-top: 1px solid #0f3460; }
|
|
122
|
+
.proj-btn {
|
|
123
|
+
display: flex; align-items: center; gap: 7px; width: 100%; padding: 5px 6px;
|
|
124
|
+
border-radius: 5px; border: none; background: none; color: #ccc;
|
|
125
|
+
font-size: 12px; cursor: pointer; text-align: left; transition: background .1s;
|
|
126
|
+
}
|
|
127
|
+
.proj-btn:hover { background: #0f3460; }
|
|
128
|
+
.proj-btn.off { opacity: .35; text-decoration: line-through; }
|
|
129
|
+
.proj-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
130
|
+
</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
<div id="header">
|
|
134
|
+
<div>
|
|
135
|
+
<h1>Project Graph</h1>
|
|
136
|
+
<p>${nodes.length} projects · ${edges.length} edges · ${new Date().toLocaleString()}</p>
|
|
137
|
+
</div>
|
|
138
|
+
<div id="filters">
|
|
139
|
+
${allLabels.map(l => `<button class="filter-btn" data-label="${escHtml(l)}">${escHtml(l)}</button>`).join('\n ')}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div id="main">
|
|
143
|
+
<div id="network"></div>
|
|
144
|
+
<div id="legend">
|
|
145
|
+
<h3>Nodes</h3>
|
|
146
|
+
<div class="entry"><span class="dot" style="background:#4A90D9"></span>has .ragmeta.json</div>
|
|
147
|
+
<div class="entry"><span class="dot" style="background:#5a5a7a"></span>no .ragmeta.json</div>
|
|
148
|
+
<div class="entry"><span class="dot" style="background:#E8A838"></span>not indexed</div>
|
|
149
|
+
<div id="hint">Drag to rearrange.<br>Edge buttons — toggle type.<br>Projects — toggle node.</div>
|
|
150
|
+
<div id="project-list">
|
|
151
|
+
<h3>Projects</h3>
|
|
152
|
+
${visNodes.map(n => `<button class="proj-btn" data-node="${escHtml(n.id)}"><span class="proj-dot" style="background:${escHtml(n.color)}"></span>${escHtml(n.label)}</button>`).join('\n ')}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<script>
|
|
157
|
+
const allEdges = ${JSON.stringify(visEdges)};
|
|
158
|
+
const nodesDs = new vis.DataSet(${JSON.stringify(visNodes)});
|
|
159
|
+
const edgesDs = new vis.DataSet(allEdges);
|
|
160
|
+
|
|
161
|
+
const network = new vis.Network(
|
|
162
|
+
document.getElementById('network'),
|
|
163
|
+
{ nodes: nodesDs, edges: edgesDs },
|
|
164
|
+
{
|
|
165
|
+
nodes: {
|
|
166
|
+
shape: 'box',
|
|
167
|
+
font: { color: '#fff', size: 13 },
|
|
168
|
+
borderWidth: 2,
|
|
169
|
+
shadow: true,
|
|
170
|
+
margin: { top: 8, bottom: 8, left: 12, right: 12 },
|
|
171
|
+
},
|
|
172
|
+
edges: {
|
|
173
|
+
font: { color: '#aaa', size: 11, align: 'middle', strokeWidth: 0, background: '#1a1a2e' },
|
|
174
|
+
color: { color: '#4A90D9', highlight: '#7eb8f7', hover: '#7eb8f7' },
|
|
175
|
+
smooth: { type: 'curvedCW', roundness: 0.2 },
|
|
176
|
+
arrows: { to: { scaleFactor: 0.6 } },
|
|
177
|
+
selectionWidth: 2,
|
|
178
|
+
},
|
|
179
|
+
physics: {
|
|
180
|
+
solver: 'barnesHut',
|
|
181
|
+
barnesHut: {
|
|
182
|
+
gravitationalConstant: -18000,
|
|
183
|
+
springConstant: 0.025,
|
|
184
|
+
springLength: 280,
|
|
185
|
+
damping: 0.25,
|
|
186
|
+
},
|
|
187
|
+
stabilization: { iterations: 400, updateInterval: 25 },
|
|
188
|
+
},
|
|
189
|
+
interaction: { hover: true, tooltipDelay: 80, dragNodes: true },
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
network.on('stabilizationIterationsDone', () => {
|
|
194
|
+
network.fit({ animation: { duration: 600, easingFunction: 'easeInOutQuad' } });
|
|
195
|
+
setTimeout(() => network.setOptions({ physics: { enabled: false } }), 650);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
network.on('hoverEdge', ({ edge }) => {
|
|
199
|
+
edgesDs.update({ id: edge, font: { color: '#fff', size: 16, background: '#16213e', strokeWidth: 0 } });
|
|
200
|
+
});
|
|
201
|
+
network.on('blurEdge', ({ edge }) => {
|
|
202
|
+
edgesDs.update({ id: edge, font: { color: '#aaa', size: 11, background: '#1a1a2e', strokeWidth: 0 } });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const activeLabels = new Set(${JSON.stringify(allLabels)});
|
|
206
|
+
const hiddenNodes = new Set();
|
|
207
|
+
|
|
208
|
+
function refreshEdges() {
|
|
209
|
+
const visible = allEdges.filter(e =>
|
|
210
|
+
activeLabels.has(e.label) && !hiddenNodes.has(e.from) && !hiddenNodes.has(e.to)
|
|
211
|
+
);
|
|
212
|
+
edgesDs.clear();
|
|
213
|
+
edgesDs.add(visible);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
217
|
+
btn.addEventListener('click', () => {
|
|
218
|
+
const label = btn.dataset.label;
|
|
219
|
+
if (activeLabels.has(label)) { activeLabels.delete(label); btn.classList.add('off'); }
|
|
220
|
+
else { activeLabels.add(label); btn.classList.remove('off'); }
|
|
221
|
+
refreshEdges();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
document.querySelectorAll('.proj-btn').forEach(btn => {
|
|
226
|
+
btn.addEventListener('click', () => {
|
|
227
|
+
const id = btn.dataset.node;
|
|
228
|
+
if (hiddenNodes.has(id)) {
|
|
229
|
+
hiddenNodes.delete(id);
|
|
230
|
+
nodesDs.update({ id, hidden: false });
|
|
231
|
+
btn.classList.remove('off');
|
|
232
|
+
} else {
|
|
233
|
+
hiddenNodes.add(id);
|
|
234
|
+
nodesDs.update({ id, hidden: true });
|
|
235
|
+
btn.classList.add('off');
|
|
236
|
+
}
|
|
237
|
+
refreshEdges();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
</script>
|
|
241
|
+
</body>
|
|
242
|
+
</html>`;
|
|
243
|
+
}
|
|
244
|
+
// Single entry point — reads meta once, prints console summary and writes HTML
|
|
245
|
+
function generateProjectGraph(projects, workspacePath) {
|
|
246
|
+
const nodes = projects.map(p => ({
|
|
247
|
+
name: p.name,
|
|
248
|
+
...((0, file_utils_1.readRagMeta)(p.path) ?? { description: null, stack: null, notes: null, relations: null }),
|
|
249
|
+
}));
|
|
250
|
+
const edges = buildEdges(nodes);
|
|
251
|
+
printConsole(nodes, edges, workspacePath);
|
|
252
|
+
const htmlPath = path.join(workspacePath, 'project-graph.html');
|
|
253
|
+
try {
|
|
254
|
+
fs.writeFileSync(htmlPath, buildHtml(nodes, edges), 'utf-8');
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.error(` ⚠ Could not write project-graph.html: ${err.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface IndexStats {
|
|
2
|
+
serviceName: string;
|
|
3
|
+
indexed: number;
|
|
4
|
+
skipped: number;
|
|
5
|
+
failed: number;
|
|
6
|
+
chunks: number;
|
|
7
|
+
elapsedSec: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function discoverProjects(rootDir: string): {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
}[];
|
|
13
|
+
export declare function scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal): Promise<IndexStats>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.discoverProjects = discoverProjects;
|
|
37
|
+
exports.scanAndIndex = scanAndIndex;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const textsplitters_1 = require("@langchain/textsplitters");
|
|
41
|
+
const ollama_1 = require("ollama");
|
|
42
|
+
const cli_progress_1 = require("cli-progress");
|
|
43
|
+
const db_1 = require("./db");
|
|
44
|
+
const ast_1 = require("./ast");
|
|
45
|
+
const config_1 = require("./config");
|
|
46
|
+
const file_utils_1 = require("./file-utils");
|
|
47
|
+
const ollama = new ollama_1.Ollama({ host: config_1.config.ollama.host });
|
|
48
|
+
// JS/TS splitter uses language-aware separators (functions, classes, blocks)
|
|
49
|
+
const jsSplitter = textsplitters_1.RecursiveCharacterTextSplitter.fromLanguage('js', {
|
|
50
|
+
chunkSize: config_1.config.indexer.chunkSize,
|
|
51
|
+
chunkOverlap: config_1.config.indexer.chunkOverlap,
|
|
52
|
+
});
|
|
53
|
+
// Schema splitter uses generic separators — better for Prisma/GraphQL block structure
|
|
54
|
+
const schemaSplitter = new textsplitters_1.RecursiveCharacterTextSplitter({
|
|
55
|
+
chunkSize: config_1.config.indexer.chunkSize,
|
|
56
|
+
chunkOverlap: config_1.config.indexer.chunkOverlap,
|
|
57
|
+
separators: ['\n\n', '\n', ' '],
|
|
58
|
+
});
|
|
59
|
+
const SCHEMA_EXTENSIONS = /\.(prisma|graphql|gql)$/;
|
|
60
|
+
function getSplitter(filePath) {
|
|
61
|
+
return SCHEMA_EXTENSIONS.test(filePath) ? schemaSplitter : jsSplitter;
|
|
62
|
+
}
|
|
63
|
+
// Detect JS/TS projects by the presence of package.json in their root
|
|
64
|
+
function discoverProjects(rootDir) {
|
|
65
|
+
return fs.readdirSync(rootDir, { withFileTypes: true })
|
|
66
|
+
.filter(d => d.isDirectory() && !file_utils_1.IGNORE_DIRS.has(d.name))
|
|
67
|
+
.map(d => ({ name: d.name, path: path.join(rootDir, d.name) }))
|
|
68
|
+
.filter(p => fs.existsSync(path.join(p.path, 'package.json')));
|
|
69
|
+
}
|
|
70
|
+
// Embed a pre-formatted prompt (caller is responsible for prefix and context)
|
|
71
|
+
async function embedText(prompt) {
|
|
72
|
+
const { embedding } = await ollama.embeddings({
|
|
73
|
+
model: config_1.config.ollama.model,
|
|
74
|
+
prompt,
|
|
75
|
+
});
|
|
76
|
+
return embedding;
|
|
77
|
+
}
|
|
78
|
+
// Ingest a single file: skip if unchanged, otherwise delete old chunks and re-index.
|
|
79
|
+
// Returns status and chunk count. Never throws — failures are caught and reported.
|
|
80
|
+
async function ingestFile(filePath, serviceName, mtimeMs, projectRoot) {
|
|
81
|
+
try {
|
|
82
|
+
const repo = db_1.db.getRepository(db_1.CodeChunk);
|
|
83
|
+
const relFilePath = path.relative(config_1.config.workspaceRoot, filePath);
|
|
84
|
+
// Incremental check: if mtime matches what's stored, the file hasn't changed
|
|
85
|
+
const existing = await repo.findOne({
|
|
86
|
+
select: ['id', 'lastModified'],
|
|
87
|
+
where: { serviceName, filePath: relFilePath },
|
|
88
|
+
});
|
|
89
|
+
if (existing?.lastModified === mtimeMs) {
|
|
90
|
+
return { status: 'skipped', chunks: 0 };
|
|
91
|
+
}
|
|
92
|
+
// Extract import edges before the transaction — disk I/O must not hold a DB connection open
|
|
93
|
+
const rawEdges = await (0, ast_1.extractImportEdges)(filePath, serviceName);
|
|
94
|
+
const edges = rawEdges
|
|
95
|
+
.filter(e => e.toFile.startsWith(config_1.config.workspaceRoot))
|
|
96
|
+
.map(e => ({
|
|
97
|
+
...e,
|
|
98
|
+
fromFile: path.relative(config_1.config.workspaceRoot, e.fromFile),
|
|
99
|
+
toFile: path.relative(config_1.config.workspaceRoot, e.toFile),
|
|
100
|
+
}));
|
|
101
|
+
// Read and embed outside the transaction — Ollama calls must not hold a DB transaction open
|
|
102
|
+
const code = fs.readFileSync(filePath, 'utf-8');
|
|
103
|
+
const chunks = await getSplitter(filePath).createDocuments([code]);
|
|
104
|
+
const relPath = path.relative(projectRoot, filePath); // project-relative for embedding header
|
|
105
|
+
const records = [];
|
|
106
|
+
for (let i = 0; i < chunks.length; i += config_1.config.indexer.batchSize) {
|
|
107
|
+
const batch = chunks.slice(i, i + config_1.config.indexer.batchSize);
|
|
108
|
+
const embedded = await Promise.all(batch.map(async (chunk) => {
|
|
109
|
+
const startLine = (chunk.metadata?.loc?.lines?.from ?? 1);
|
|
110
|
+
const prompt = `Project: ${serviceName} | Path: ${relPath}\n${chunk.pageContent}`;
|
|
111
|
+
const embedding = await embedText(prompt);
|
|
112
|
+
return repo.create({
|
|
113
|
+
serviceName, filePath: relFilePath, startLine,
|
|
114
|
+
content: chunk.pageContent,
|
|
115
|
+
embedding: `[${embedding}]`,
|
|
116
|
+
lastModified: mtimeMs,
|
|
117
|
+
});
|
|
118
|
+
}));
|
|
119
|
+
records.push(...embedded);
|
|
120
|
+
}
|
|
121
|
+
// Atomic replace: delete stale chunks + dependency edges, insert new ones in a single transaction.
|
|
122
|
+
// Prevents partial index if the process crashes mid-write.
|
|
123
|
+
await db_1.db.transaction(async (manager) => {
|
|
124
|
+
await manager.delete(db_1.CodeChunk, { serviceName, filePath: relFilePath });
|
|
125
|
+
for (let i = 0; i < records.length; i += config_1.config.indexer.batchSize) {
|
|
126
|
+
await manager.createQueryBuilder()
|
|
127
|
+
.insert()
|
|
128
|
+
.into(db_1.CodeChunk)
|
|
129
|
+
.values(records.slice(i, i + config_1.config.indexer.batchSize))
|
|
130
|
+
.execute();
|
|
131
|
+
}
|
|
132
|
+
await manager.delete(db_1.CodeDependency, { project: serviceName, fromFile: relFilePath });
|
|
133
|
+
if (edges.length > 0) {
|
|
134
|
+
await manager.createQueryBuilder()
|
|
135
|
+
.insert()
|
|
136
|
+
.into(db_1.CodeDependency)
|
|
137
|
+
.values(edges)
|
|
138
|
+
.execute();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return { status: 'indexed', chunks: chunks.length };
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
console.error(`\n[indexer] Failed: ${filePath} — ${err.message}`);
|
|
145
|
+
return { status: 'failed', chunks: 0 };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Index a project directory incrementally.
|
|
149
|
+
// Removes chunks for deleted files, skips unchanged files, re-indexes modified ones.
|
|
150
|
+
// Pass force=true to invalidate all cached mtimes and force full re-index (e.g. after embedding strategy change).
|
|
151
|
+
// Pass signal to support graceful interruption — stops between files, never mid-transaction.
|
|
152
|
+
async function scanAndIndex(dir, serviceName, force = false, signal) {
|
|
153
|
+
const startTime = Date.now();
|
|
154
|
+
const repo = db_1.db.getRepository(db_1.CodeChunk);
|
|
155
|
+
// Clean up chunks whose source files no longer exist on disk
|
|
156
|
+
const dbPaths = await db_1.db.query(`SELECT DISTINCT "filePath" FROM code_chunk WHERE "serviceName" = $1`, [serviceName]);
|
|
157
|
+
for (const { filePath } of dbPaths) {
|
|
158
|
+
if (!fs.existsSync(path.join(config_1.config.workspaceRoot, filePath))) {
|
|
159
|
+
await repo.delete({ serviceName, filePath });
|
|
160
|
+
// Remove dependency edges on both sides — this file no longer exists as importer or target
|
|
161
|
+
await db_1.db.query(`DELETE FROM code_dependency WHERE project = $1 AND "fromFile" = $2`, [serviceName, filePath]);
|
|
162
|
+
await db_1.db.query(`DELETE FROM code_dependency WHERE project = $1 AND "toFile" = $2`, [serviceName, filePath]);
|
|
163
|
+
console.log(` Removed stale: ${path.basename(filePath)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Force mode: wipe lastModified so all files are treated as changed
|
|
167
|
+
if (force) {
|
|
168
|
+
await db_1.db.query(`UPDATE code_chunk SET "lastModified" = NULL WHERE "serviceName" = $1`, [serviceName]);
|
|
169
|
+
}
|
|
170
|
+
const ig = (0, file_utils_1.loadIgnore)(dir);
|
|
171
|
+
const files = (0, file_utils_1.collectFiles)(dir, ig, dir);
|
|
172
|
+
const bar = new cli_progress_1.SingleBar({
|
|
173
|
+
format: `{serviceName} {bar} {percentage}% | {value}/{total} | {filename}`,
|
|
174
|
+
barCompleteChar: '█',
|
|
175
|
+
barIncompleteChar: '░',
|
|
176
|
+
hideCursor: true,
|
|
177
|
+
clearOnComplete: false,
|
|
178
|
+
}, cli_progress_1.Presets.shades_grey);
|
|
179
|
+
console.log(`\nIndexing ${serviceName}: ${files.length} files`);
|
|
180
|
+
const meta = (0, file_utils_1.readRagMeta)(dir);
|
|
181
|
+
if (meta === null && fs.existsSync(path.join(dir, '.ragmeta.json'))) {
|
|
182
|
+
console.log(` ⚠ .ragmeta.json found but invalid JSON`);
|
|
183
|
+
}
|
|
184
|
+
else if (meta) {
|
|
185
|
+
const parts = [meta.description, meta.stack && `[${meta.stack}]`].filter(Boolean).join(' ');
|
|
186
|
+
console.log(` ✓ .ragmeta.json: ${parts || '(no fields)'}`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(` ⚠ No .ragmeta.json — add {"description":"...","stack":"...","notes":"..."} so list_projects can describe this project`);
|
|
190
|
+
}
|
|
191
|
+
bar.start(files.length, 0, { serviceName, filename: '' });
|
|
192
|
+
const stats = {
|
|
193
|
+
serviceName, indexed: 0, skipped: 0, failed: 0, chunks: 0, elapsedSec: 0,
|
|
194
|
+
};
|
|
195
|
+
for (const filePath of files) {
|
|
196
|
+
if (signal?.aborted) {
|
|
197
|
+
bar.stop();
|
|
198
|
+
console.log(`\n Interrupted — ${serviceName}`);
|
|
199
|
+
stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
|
|
200
|
+
return stats;
|
|
201
|
+
}
|
|
202
|
+
bar.update({ filename: path.basename(filePath) });
|
|
203
|
+
const mtimeMs = fs.statSync(filePath).mtimeMs;
|
|
204
|
+
const result = await ingestFile(filePath, serviceName, mtimeMs, dir);
|
|
205
|
+
if (result.status === 'indexed')
|
|
206
|
+
stats.indexed++;
|
|
207
|
+
else if (result.status === 'skipped')
|
|
208
|
+
stats.skipped++;
|
|
209
|
+
else
|
|
210
|
+
stats.failed++;
|
|
211
|
+
stats.chunks += result.chunks;
|
|
212
|
+
bar.increment();
|
|
213
|
+
}
|
|
214
|
+
bar.stop();
|
|
215
|
+
stats.elapsedSec = Math.round((Date.now() - startTime) / 100) / 10;
|
|
216
|
+
return stats;
|
|
217
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Graceful interruption signal for long-running CLI operations.
|
|
3
|
+
// Listens for ESC or Ctrl+C on stdin and aborts the shared controller.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.signal = void 0;
|
|
6
|
+
exports.setupInterrupt = setupInterrupt;
|
|
7
|
+
exports.teardownInterrupt = teardownInterrupt;
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
exports.signal = controller.signal;
|
|
10
|
+
function setupInterrupt() {
|
|
11
|
+
if (!process.stdin.isTTY)
|
|
12
|
+
return;
|
|
13
|
+
process.stdin.setRawMode(true);
|
|
14
|
+
process.stdin.resume();
|
|
15
|
+
process.stdin.setEncoding('utf8');
|
|
16
|
+
process.stdin.on('data', (key) => {
|
|
17
|
+
if (key === '\x1b' || key === '\x03')
|
|
18
|
+
controller.abort();
|
|
19
|
+
});
|
|
20
|
+
console.log('Press ESC or Ctrl+C to interrupt.\n');
|
|
21
|
+
}
|
|
22
|
+
function teardownInterrupt() {
|
|
23
|
+
if (!process.stdin.isTTY)
|
|
24
|
+
return;
|
|
25
|
+
process.stdin.setRawMode(false);
|
|
26
|
+
process.stdin.pause();
|
|
27
|
+
}
|