@zuvia-software-solutions/code-mapper 1.4.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +73 -82
- package/dist/cli/augment.js +0 -2
- package/dist/cli/eval-server.d.ts +2 -2
- package/dist/cli/eval-server.js +6 -6
- package/dist/cli/index.js +6 -10
- package/dist/cli/mcp.d.ts +1 -3
- package/dist/cli/mcp.js +3 -3
- package/dist/cli/refresh.d.ts +2 -2
- package/dist/cli/refresh.js +24 -29
- package/dist/cli/status.js +4 -13
- package/dist/cli/tool.d.ts +5 -4
- package/dist/cli/tool.js +8 -10
- package/dist/config/ignore-service.js +14 -34
- package/dist/core/augmentation/engine.js +53 -83
- package/dist/core/db/adapter.d.ts +99 -0
- package/dist/core/db/adapter.js +402 -0
- package/dist/core/db/graph-loader.d.ts +27 -0
- package/dist/core/db/graph-loader.js +148 -0
- package/dist/core/db/queries.d.ts +160 -0
- package/dist/core/db/queries.js +441 -0
- package/dist/core/db/schema.d.ts +108 -0
- package/dist/core/db/schema.js +136 -0
- package/dist/core/embeddings/embedder.d.ts +21 -12
- package/dist/core/embeddings/embedder.js +104 -50
- package/dist/core/embeddings/embedding-pipeline.d.ts +48 -22
- package/dist/core/embeddings/embedding-pipeline.js +220 -262
- package/dist/core/embeddings/text-generator.js +4 -19
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/graph.d.ts +1 -1
- package/dist/core/graph/graph.js +1 -0
- package/dist/core/graph/types.d.ts +11 -9
- package/dist/core/graph/types.js +4 -1
- package/dist/core/incremental/refresh.d.ts +46 -0
- package/dist/core/incremental/refresh.js +503 -0
- package/dist/core/incremental/types.d.ts +2 -1
- package/dist/core/incremental/types.js +42 -44
- package/dist/core/ingestion/ast-cache.js +1 -0
- package/dist/core/ingestion/call-processor.d.ts +15 -3
- package/dist/core/ingestion/call-processor.js +448 -60
- package/dist/core/ingestion/cluster-enricher.d.ts +1 -1
- package/dist/core/ingestion/cluster-enricher.js +2 -0
- package/dist/core/ingestion/community-processor.d.ts +1 -1
- package/dist/core/ingestion/community-processor.js +8 -3
- package/dist/core/ingestion/export-detection.d.ts +1 -1
- package/dist/core/ingestion/export-detection.js +1 -1
- package/dist/core/ingestion/filesystem-walker.js +1 -1
- package/dist/core/ingestion/heritage-processor.d.ts +2 -2
- package/dist/core/ingestion/heritage-processor.js +22 -11
- package/dist/core/ingestion/import-processor.d.ts +2 -2
- package/dist/core/ingestion/import-processor.js +24 -9
- package/dist/core/ingestion/language-config.js +7 -4
- package/dist/core/ingestion/mro-processor.d.ts +1 -1
- package/dist/core/ingestion/mro-processor.js +23 -11
- package/dist/core/ingestion/named-binding-extraction.js +5 -5
- package/dist/core/ingestion/parsing-processor.d.ts +4 -4
- package/dist/core/ingestion/parsing-processor.js +26 -18
- package/dist/core/ingestion/pipeline.d.ts +4 -2
- package/dist/core/ingestion/pipeline.js +50 -20
- package/dist/core/ingestion/process-processor.d.ts +2 -2
- package/dist/core/ingestion/process-processor.js +28 -14
- package/dist/core/ingestion/resolution-context.d.ts +1 -1
- package/dist/core/ingestion/resolution-context.js +14 -4
- package/dist/core/ingestion/resolvers/csharp.js +4 -3
- package/dist/core/ingestion/resolvers/go.js +3 -1
- package/dist/core/ingestion/resolvers/jvm.js +13 -4
- package/dist/core/ingestion/resolvers/standard.js +2 -2
- package/dist/core/ingestion/resolvers/utils.js +6 -2
- package/dist/core/ingestion/route-stitcher.d.ts +15 -0
- package/dist/core/ingestion/route-stitcher.js +92 -0
- package/dist/core/ingestion/structure-processor.d.ts +1 -1
- package/dist/core/ingestion/structure-processor.js +3 -2
- package/dist/core/ingestion/symbol-table.d.ts +2 -0
- package/dist/core/ingestion/symbol-table.js +5 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
- package/dist/core/ingestion/tree-sitter-queries.js +177 -0
- package/dist/core/ingestion/type-env.js +20 -0
- package/dist/core/ingestion/type-extractors/csharp.js +4 -3
- package/dist/core/ingestion/type-extractors/go.js +23 -12
- package/dist/core/ingestion/type-extractors/php.js +18 -10
- package/dist/core/ingestion/type-extractors/ruby.js +15 -3
- package/dist/core/ingestion/type-extractors/rust.js +3 -2
- package/dist/core/ingestion/type-extractors/shared.js +3 -2
- package/dist/core/ingestion/type-extractors/typescript.js +11 -5
- package/dist/core/ingestion/utils.d.ts +27 -4
- package/dist/core/ingestion/utils.js +145 -100
- package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
- package/dist/core/ingestion/workers/parse-worker.js +97 -29
- package/dist/core/ingestion/workers/worker-pool.js +3 -0
- package/dist/core/search/bm25-index.d.ts +15 -8
- package/dist/core/search/bm25-index.js +48 -98
- package/dist/core/search/hybrid-search.d.ts +9 -3
- package/dist/core/search/hybrid-search.js +30 -25
- package/dist/core/search/reranker.js +9 -7
- package/dist/core/search/types.d.ts +0 -4
- package/dist/core/semantic/tsgo-service.d.ts +7 -1
- package/dist/core/semantic/tsgo-service.js +165 -66
- package/dist/lib/tsgo-test.d.ts +2 -0
- package/dist/lib/tsgo-test.js +6 -0
- package/dist/lib/type-utils.d.ts +25 -0
- package/dist/lib/type-utils.js +22 -0
- package/dist/lib/utils.d.ts +3 -2
- package/dist/lib/utils.js +3 -2
- package/dist/mcp/compatible-stdio-transport.js +1 -1
- package/dist/mcp/local/local-backend.d.ts +29 -56
- package/dist/mcp/local/local-backend.js +808 -1118
- package/dist/mcp/resources.js +35 -25
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +5 -5
- package/dist/mcp/tools.js +24 -25
- package/dist/storage/repo-manager.d.ts +2 -12
- package/dist/storage/repo-manager.js +1 -47
- package/dist/types/pipeline.d.ts +8 -5
- package/dist/types/pipeline.js +5 -0
- package/package.json +18 -11
- package/dist/cli/serve.d.ts +0 -5
- package/dist/cli/serve.js +0 -8
- package/dist/core/incremental/child-process.d.ts +0 -8
- package/dist/core/incremental/child-process.js +0 -649
- package/dist/core/incremental/refresh-coordinator.d.ts +0 -32
- package/dist/core/incremental/refresh-coordinator.js +0 -147
- package/dist/core/lbug/csv-generator.d.ts +0 -28
- package/dist/core/lbug/csv-generator.js +0 -355
- package/dist/core/lbug/lbug-adapter.d.ts +0 -96
- package/dist/core/lbug/lbug-adapter.js +0 -753
- package/dist/core/lbug/schema.d.ts +0 -46
- package/dist/core/lbug/schema.js +0 -402
- package/dist/mcp/core/embedder.d.ts +0 -24
- package/dist/mcp/core/embedder.js +0 -168
- package/dist/mcp/core/lbug-adapter.d.ts +0 -29
- package/dist/mcp/core/lbug-adapter.js +0 -330
- package/dist/server/api.d.ts +0 -5
- package/dist/server/api.js +0 -340
- package/dist/server/mcp-http.d.ts +0 -7
- package/dist/server/mcp-http.js +0 -95
- package/models/mlx-embedder.py +0 -185
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
// code-mapper/src/mcp/core/lbug-adapter.ts
|
|
2
|
-
/** @file lbug-adapter.ts
|
|
3
|
-
* @description LadybugDB connection pool adapter keyed by repoId
|
|
4
|
-
* Provides checkout/return pool so each concurrent query gets its own Connection
|
|
5
|
-
* from the same Database (Connections are NOT thread-safe)
|
|
6
|
-
* @see https://docs.ladybugdb.com/concurrency */
|
|
7
|
-
import fs from 'fs/promises';
|
|
8
|
-
import lbug from '@ladybugdb/core';
|
|
9
|
-
const pool = new Map();
|
|
10
|
-
const dbCache = new Map();
|
|
11
|
-
/** Max repos in the pool (LRU eviction) */
|
|
12
|
-
const MAX_POOL_SIZE = 5;
|
|
13
|
-
/** Idle timeout before closing a repo's connections */
|
|
14
|
-
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
15
|
-
/** Max connections per repo (caps concurrent queries per repo) */
|
|
16
|
-
const MAX_CONNS_PER_REPO = 8;
|
|
17
|
-
/** Connections created eagerly on init */
|
|
18
|
-
const INITIAL_CONNS_PER_REPO = 2;
|
|
19
|
-
let idleTimer = null;
|
|
20
|
-
/** Start the idle cleanup timer (runs every 60s) */
|
|
21
|
-
function ensureIdleTimer() {
|
|
22
|
-
if (idleTimer)
|
|
23
|
-
return;
|
|
24
|
-
idleTimer = setInterval(() => {
|
|
25
|
-
const now = Date.now();
|
|
26
|
-
for (const [repoId, entry] of pool) {
|
|
27
|
-
if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
|
|
28
|
-
closeOne(repoId);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}, 60_000);
|
|
32
|
-
if (idleTimer && typeof idleTimer === 'object' && 'unref' in idleTimer) {
|
|
33
|
-
idleTimer.unref();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/** Evict the least-recently-used repo if pool is at capacity */
|
|
37
|
-
function evictLRU() {
|
|
38
|
-
if (pool.size < MAX_POOL_SIZE)
|
|
39
|
-
return;
|
|
40
|
-
let oldestId = null;
|
|
41
|
-
let oldestTime = Infinity;
|
|
42
|
-
for (const [id, entry] of pool) {
|
|
43
|
-
if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
|
|
44
|
-
oldestTime = entry.lastUsed;
|
|
45
|
-
oldestId = id;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (oldestId) {
|
|
49
|
-
closeOne(oldestId);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Remove a repo from the pool and release its shared Database ref
|
|
54
|
-
* Skips .closeSync() which triggers N-API segfaults on Linux/macOS;
|
|
55
|
-
* pools are read-only so GC reclamation is safe
|
|
56
|
-
*/
|
|
57
|
-
function closeOne(repoId) {
|
|
58
|
-
const entry = pool.get(repoId);
|
|
59
|
-
if (entry) {
|
|
60
|
-
const shared = dbCache.get(entry.dbPath);
|
|
61
|
-
if (shared && shared.refCount > 0) {
|
|
62
|
-
shared.refCount--;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
pool.delete(repoId);
|
|
66
|
-
}
|
|
67
|
-
function createConnection(db) {
|
|
68
|
-
return new lbug.Connection(db);
|
|
69
|
-
}
|
|
70
|
-
/** Query timeout in milliseconds */
|
|
71
|
-
const QUERY_TIMEOUT_MS = 30_000;
|
|
72
|
-
/** Waiter queue timeout in milliseconds */
|
|
73
|
-
const WAITER_TIMEOUT_MS = 15_000;
|
|
74
|
-
const LOCK_RETRY_ATTEMPTS = 3;
|
|
75
|
-
const LOCK_RETRY_DELAY_MS = 2000;
|
|
76
|
-
/**
|
|
77
|
-
* Initialize (or reuse) a Database + connection pool for a specific repo
|
|
78
|
-
* Retries on lock errors (e.g., when `code-mapper analyze` is running)
|
|
79
|
-
*/
|
|
80
|
-
export const initLbug = async (repoId, dbPath) => {
|
|
81
|
-
const existing = pool.get(repoId);
|
|
82
|
-
if (existing) {
|
|
83
|
-
existing.lastUsed = Date.now();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// Check if database exists
|
|
87
|
-
try {
|
|
88
|
-
await fs.stat(dbPath);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
throw new Error(`LadybugDB not found at ${dbPath}. Run: code-mapper analyze`);
|
|
92
|
-
}
|
|
93
|
-
evictLRU();
|
|
94
|
-
// Reuse an existing native Database if another repoId already opened this path
|
|
95
|
-
// Prevents buffer manager exhaustion from multiple mmap regions on the same file
|
|
96
|
-
let shared = dbCache.get(dbPath);
|
|
97
|
-
if (!shared) {
|
|
98
|
-
// Open read-write so incremental refresh can write through the same handle.
|
|
99
|
-
// Lock conflicts with `code-mapper analyze` are handled by retry logic.
|
|
100
|
-
let lastError = null;
|
|
101
|
-
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
|
102
|
-
try {
|
|
103
|
-
const db = new lbug.Database(dbPath, 0, // bufferManagerSize (default)
|
|
104
|
-
false, // enableCompression (default)
|
|
105
|
-
false);
|
|
106
|
-
shared = { db, refCount: 0, ftsLoaded: false };
|
|
107
|
-
dbCache.set(dbPath, shared);
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
112
|
-
const isLockError = lastError.message.includes('Could not set lock')
|
|
113
|
-
|| lastError.message.includes('lock');
|
|
114
|
-
if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS)
|
|
115
|
-
break;
|
|
116
|
-
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (!shared) {
|
|
120
|
-
throw new Error(`LadybugDB unavailable for ${repoId}. Another process may be rebuilding the index. ` +
|
|
121
|
-
`Retry later. (${lastError?.message || 'unknown error'})`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
shared.refCount++;
|
|
125
|
-
const db = shared.db;
|
|
126
|
-
// Pre-create a small pool of connections
|
|
127
|
-
const available = [];
|
|
128
|
-
for (let i = 0; i < INITIAL_CONNS_PER_REPO; i++) {
|
|
129
|
-
available.push(createConnection(db));
|
|
130
|
-
}
|
|
131
|
-
pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath });
|
|
132
|
-
ensureIdleTimer();
|
|
133
|
-
// Load FTS + VECTOR extensions once per shared Database
|
|
134
|
-
if (!shared.ftsLoaded) {
|
|
135
|
-
try {
|
|
136
|
-
await available[0].query('LOAD EXTENSION fts');
|
|
137
|
-
shared.ftsLoaded = true;
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Extension may not be installed — FTS queries will fail gracefully
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Load VECTOR extension for semantic search (HNSW index queries)
|
|
144
|
-
try {
|
|
145
|
-
await available[0].query('LOAD EXTENSION vector');
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
// Extension may not be installed — vector queries will fall back to BM25-only
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
/**
|
|
152
|
-
* Checkout a connection from the pool
|
|
153
|
-
* Returns an available connection, creates a new one if under cap,
|
|
154
|
-
* or queues the caller until one is returned
|
|
155
|
-
*/
|
|
156
|
-
function checkout(entry) {
|
|
157
|
-
// Fast path: grab an available connection
|
|
158
|
-
if (entry.available.length > 0) {
|
|
159
|
-
entry.checkedOut++;
|
|
160
|
-
return Promise.resolve(entry.available.pop());
|
|
161
|
-
}
|
|
162
|
-
// Grow the pool if under the cap
|
|
163
|
-
const totalConns = entry.available.length + entry.checkedOut;
|
|
164
|
-
if (totalConns < MAX_CONNS_PER_REPO) {
|
|
165
|
-
entry.checkedOut++;
|
|
166
|
-
return Promise.resolve(createConnection(entry.db));
|
|
167
|
-
}
|
|
168
|
-
// At capacity — queue the caller with a timeout.
|
|
169
|
-
return new Promise((resolve, reject) => {
|
|
170
|
-
const waiter = (conn) => {
|
|
171
|
-
clearTimeout(timer);
|
|
172
|
-
resolve(conn);
|
|
173
|
-
};
|
|
174
|
-
const timer = setTimeout(() => {
|
|
175
|
-
const idx = entry.waiters.indexOf(waiter);
|
|
176
|
-
if (idx !== -1)
|
|
177
|
-
entry.waiters.splice(idx, 1);
|
|
178
|
-
reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
|
|
179
|
-
}, WAITER_TIMEOUT_MS);
|
|
180
|
-
entry.waiters.push(waiter);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Return a connection to the pool after use
|
|
185
|
-
* Hands directly to queued waiters to avoid race conditions
|
|
186
|
-
*/
|
|
187
|
-
function checkin(entry, conn) {
|
|
188
|
-
if (entry.waiters.length > 0) {
|
|
189
|
-
// Hand directly to the next waiter (no intermediate available state)
|
|
190
|
-
const waiter = entry.waiters.shift();
|
|
191
|
-
waiter(conn);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
entry.checkedOut--;
|
|
195
|
-
entry.available.push(conn);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/** Race a promise against a timeout */
|
|
199
|
-
function withTimeout(promise, ms, label) {
|
|
200
|
-
let timer;
|
|
201
|
-
const timeout = new Promise((_, reject) => {
|
|
202
|
-
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
203
|
-
});
|
|
204
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
205
|
-
}
|
|
206
|
-
export const executeQuery = async (repoId, cypher) => {
|
|
207
|
-
const entry = pool.get(repoId);
|
|
208
|
-
if (!entry) {
|
|
209
|
-
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
210
|
-
}
|
|
211
|
-
entry.lastUsed = Date.now();
|
|
212
|
-
const conn = await checkout(entry);
|
|
213
|
-
try {
|
|
214
|
-
const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
|
|
215
|
-
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
216
|
-
const rows = await result.getAll();
|
|
217
|
-
return rows;
|
|
218
|
-
}
|
|
219
|
-
finally {
|
|
220
|
-
checkin(entry, conn);
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
/** Execute a parameterized query using prepare/execute to prevent Cypher injection */
|
|
224
|
-
export const executeParameterized = async (repoId, cypher, params) => {
|
|
225
|
-
const entry = pool.get(repoId);
|
|
226
|
-
if (!entry) {
|
|
227
|
-
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
228
|
-
}
|
|
229
|
-
entry.lastUsed = Date.now();
|
|
230
|
-
const conn = await checkout(entry);
|
|
231
|
-
try {
|
|
232
|
-
const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
|
|
233
|
-
if (!stmt.isSuccess()) {
|
|
234
|
-
const errMsg = await stmt.getErrorMessage();
|
|
235
|
-
throw new Error(`Prepare failed: ${errMsg}`);
|
|
236
|
-
}
|
|
237
|
-
const queryResult = await withTimeout(conn.execute(stmt, params), QUERY_TIMEOUT_MS, 'Execute');
|
|
238
|
-
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
239
|
-
const rows = await result.getAll();
|
|
240
|
-
return rows;
|
|
241
|
-
}
|
|
242
|
-
finally {
|
|
243
|
-
checkin(entry, conn);
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
/** Close one or all repo pools (provide repoId for single, omit for all) */
|
|
247
|
-
export const closeLbug = async (repoId) => {
|
|
248
|
-
if (repoId) {
|
|
249
|
-
closeOne(repoId);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
for (const id of [...pool.keys()]) {
|
|
253
|
-
closeOne(id);
|
|
254
|
-
}
|
|
255
|
-
if (idleTimer) {
|
|
256
|
-
clearInterval(idleTimer);
|
|
257
|
-
idleTimer = null;
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
/** Invalidate a repo's pool and shared DB cache, forcing fresh connections on next init.
|
|
261
|
-
* Closes the native Database handle to release the file lock so a child process
|
|
262
|
-
* can open the DB in read-write mode for incremental refresh.
|
|
263
|
-
* Used before AND after the incremental refresh child process writes to the DB. */
|
|
264
|
-
export const invalidateAndReopen = async (repoId) => {
|
|
265
|
-
const entry = pool.get(repoId);
|
|
266
|
-
if (!entry)
|
|
267
|
-
return;
|
|
268
|
-
const entryDbPath = entry.dbPath;
|
|
269
|
-
closeOne(repoId);
|
|
270
|
-
// Close and remove the native Database handle to release the file lock.
|
|
271
|
-
// Without this, the child process cannot acquire a write lock on the DB file.
|
|
272
|
-
const shared = dbCache.get(entryDbPath);
|
|
273
|
-
if (shared && shared.refCount <= 0) {
|
|
274
|
-
try {
|
|
275
|
-
await shared.db.close();
|
|
276
|
-
console.error('Code Mapper: Native DB handle closed successfully');
|
|
277
|
-
}
|
|
278
|
-
catch (err) {
|
|
279
|
-
console.error(`Code Mapper: DB close failed (may still release lock on GC): ${err instanceof Error ? err.message : err}`);
|
|
280
|
-
}
|
|
281
|
-
dbCache.delete(entryDbPath);
|
|
282
|
-
}
|
|
283
|
-
else if (shared) {
|
|
284
|
-
console.error(`Code Mapper: Cannot close DB — ${shared.refCount} other refs still active`);
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
/** Check if a specific repo's pool is active */
|
|
288
|
-
export const isLbugReady = (repoId) => pool.has(repoId);
|
|
289
|
-
/** Write-connection retry config */
|
|
290
|
-
const WRITE_RETRY_ATTEMPTS = 3;
|
|
291
|
-
const WRITE_RETRY_DELAY_MS = 1500;
|
|
292
|
-
/**
|
|
293
|
-
* Open a short-lived read-write connection, execute the callback, then close.
|
|
294
|
-
* The read-only pool must be invalidated BEFORE calling this to avoid lock
|
|
295
|
-
* conflicts. Retries on lock errors (e.g., concurrent `code-mapper analyze`).
|
|
296
|
-
*/
|
|
297
|
-
export const withWriteConnection = async (dbPath, fn) => {
|
|
298
|
-
let lastError = null;
|
|
299
|
-
for (let attempt = 1; attempt <= WRITE_RETRY_ATTEMPTS; attempt++) {
|
|
300
|
-
let db = null;
|
|
301
|
-
let conn = null;
|
|
302
|
-
try {
|
|
303
|
-
db = new lbug.Database(dbPath);
|
|
304
|
-
conn = new lbug.Connection(db);
|
|
305
|
-
const result = await fn(conn);
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
|
-
catch (err) {
|
|
309
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
310
|
-
const isLockError = lastError.message.includes('Could not set lock') ||
|
|
311
|
-
lastError.message.includes('lock');
|
|
312
|
-
if (!isLockError || attempt === WRITE_RETRY_ATTEMPTS)
|
|
313
|
-
break;
|
|
314
|
-
await new Promise((resolve) => setTimeout(resolve, WRITE_RETRY_DELAY_MS * attempt));
|
|
315
|
-
}
|
|
316
|
-
finally {
|
|
317
|
-
try {
|
|
318
|
-
if (conn)
|
|
319
|
-
await conn.close();
|
|
320
|
-
}
|
|
321
|
-
catch { }
|
|
322
|
-
try {
|
|
323
|
-
if (db)
|
|
324
|
-
await db.close();
|
|
325
|
-
}
|
|
326
|
-
catch { }
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
throw lastError ?? new Error('withWriteConnection failed');
|
|
330
|
-
};
|
package/dist/server/api.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/** @file api.ts
|
|
2
|
-
* @description REST API server for browser clients to query local .code-mapper/ indexes
|
|
3
|
-
* Also hosts MCP server over StreamableHTTP for remote AI tool access
|
|
4
|
-
* Binds to 127.0.0.1 by default with CORS restricted to localhost */
|
|
5
|
-
export declare const createServer: (port: number, host?: string) => Promise<void>;
|
package/dist/server/api.js
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
// code-mapper/src/server/api.ts
|
|
2
|
-
/** @file api.ts
|
|
3
|
-
* @description REST API server for browser clients to query local .code-mapper/ indexes
|
|
4
|
-
* Also hosts MCP server over StreamableHTTP for remote AI tool access
|
|
5
|
-
* Binds to 127.0.0.1 by default with CORS restricted to localhost */
|
|
6
|
-
import express from 'express';
|
|
7
|
-
import cors from 'cors';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import fs from 'fs/promises';
|
|
10
|
-
import { loadMeta, listRegisteredRepos } from '../storage/repo-manager.js';
|
|
11
|
-
import { executeQuery, closeLbug, withLbugDb } from '../core/lbug/lbug-adapter.js';
|
|
12
|
-
import { NODE_TABLES } from '../core/lbug/schema.js';
|
|
13
|
-
import { searchFTSFromLbug } from '../core/search/bm25-index.js';
|
|
14
|
-
import { hybridSearch } from '../core/search/hybrid-search.js';
|
|
15
|
-
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
16
|
-
// at server startup — crashes on unsupported Node ABI versions (#89)
|
|
17
|
-
import { LocalBackend } from '../mcp/local/local-backend.js';
|
|
18
|
-
import { mountMCPEndpoints } from './mcp-http.js';
|
|
19
|
-
const buildGraph = async () => {
|
|
20
|
-
const nodes = [];
|
|
21
|
-
for (const table of NODE_TABLES) {
|
|
22
|
-
try {
|
|
23
|
-
let query = '';
|
|
24
|
-
if (table === 'File') {
|
|
25
|
-
query = `MATCH (n:File) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.content AS content`;
|
|
26
|
-
}
|
|
27
|
-
else if (table === 'Folder') {
|
|
28
|
-
query = `MATCH (n:Folder) RETURN n.id AS id, n.name AS name, n.filePath AS filePath`;
|
|
29
|
-
}
|
|
30
|
-
else if (table === 'Community') {
|
|
31
|
-
query = `MATCH (n:Community) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.cohesion AS cohesion, n.symbolCount AS symbolCount`;
|
|
32
|
-
}
|
|
33
|
-
else if (table === 'Process') {
|
|
34
|
-
query = `MATCH (n:Process) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.processType AS processType, n.stepCount AS stepCount, n.communities AS communities, n.entryPointId AS entryPointId, n.terminalId AS terminalId`;
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
query = `MATCH (n:${table}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.content AS content`;
|
|
38
|
-
}
|
|
39
|
-
const rows = await executeQuery(query);
|
|
40
|
-
for (const row of rows) {
|
|
41
|
-
nodes.push({
|
|
42
|
-
id: row.id ?? row[0],
|
|
43
|
-
label: table,
|
|
44
|
-
properties: {
|
|
45
|
-
name: row.name ?? row.label ?? row[1],
|
|
46
|
-
filePath: row.filePath ?? row[2],
|
|
47
|
-
startLine: row.startLine,
|
|
48
|
-
endLine: row.endLine,
|
|
49
|
-
content: row.content,
|
|
50
|
-
heuristicLabel: row.heuristicLabel,
|
|
51
|
-
cohesion: row.cohesion,
|
|
52
|
-
symbolCount: row.symbolCount,
|
|
53
|
-
processType: row.processType,
|
|
54
|
-
stepCount: row.stepCount,
|
|
55
|
-
communities: row.communities,
|
|
56
|
-
entryPointId: row.entryPointId,
|
|
57
|
-
terminalId: row.terminalId,
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// ignore empty tables
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const relationships = [];
|
|
67
|
-
const relRows = await executeQuery(`MATCH (a)-[r:CodeRelation]->(b) RETURN a.id AS sourceId, b.id AS targetId, r.type AS type, r.confidence AS confidence, r.reason AS reason, r.step AS step`);
|
|
68
|
-
for (const row of relRows) {
|
|
69
|
-
relationships.push({
|
|
70
|
-
id: `${row.sourceId}_${row.type}_${row.targetId}`,
|
|
71
|
-
type: row.type,
|
|
72
|
-
sourceId: row.sourceId,
|
|
73
|
-
targetId: row.targetId,
|
|
74
|
-
confidence: row.confidence,
|
|
75
|
-
reason: row.reason,
|
|
76
|
-
step: row.step,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return { nodes, relationships };
|
|
80
|
-
};
|
|
81
|
-
const statusFromError = (err) => {
|
|
82
|
-
const msg = String(err?.message ?? '');
|
|
83
|
-
if (msg.includes('No indexed repositories') || msg.includes('not found'))
|
|
84
|
-
return 404;
|
|
85
|
-
if (msg.includes('Multiple repositories'))
|
|
86
|
-
return 400;
|
|
87
|
-
return 500;
|
|
88
|
-
};
|
|
89
|
-
const requestedRepo = (req) => {
|
|
90
|
-
const fromQuery = typeof req.query.repo === 'string' ? req.query.repo : undefined;
|
|
91
|
-
if (fromQuery)
|
|
92
|
-
return fromQuery;
|
|
93
|
-
if (req.body && typeof req.body === 'object' && typeof req.body.repo === 'string') {
|
|
94
|
-
return req.body.repo;
|
|
95
|
-
}
|
|
96
|
-
return undefined;
|
|
97
|
-
};
|
|
98
|
-
export const createServer = async (port, host = '127.0.0.1') => {
|
|
99
|
-
const app = express();
|
|
100
|
-
// CORS: localhost origins + deployed site only
|
|
101
|
-
// Non-browser requests (curl, server-to-server) have no origin and are allowed
|
|
102
|
-
app.use(cors({
|
|
103
|
-
origin: (origin, callback) => {
|
|
104
|
-
if (!origin
|
|
105
|
-
|| origin.startsWith('http://localhost:')
|
|
106
|
-
|| origin.startsWith('http://127.0.0.1:')
|
|
107
|
-
|| origin === 'https://code-mapper.vercel.app') {
|
|
108
|
-
callback(null, true);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
callback(new Error('Not allowed by CORS'));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}));
|
|
115
|
-
app.use(express.json({ limit: '10mb' }));
|
|
116
|
-
// Initialize MCP backend (multi-repo, shared across all MCP sessions)
|
|
117
|
-
const backend = new LocalBackend();
|
|
118
|
-
await backend.init();
|
|
119
|
-
const cleanupMcp = mountMCPEndpoints(app, backend);
|
|
120
|
-
// Resolve a repo by name from the global registry, or default to first
|
|
121
|
-
const resolveRepo = async (repoName) => {
|
|
122
|
-
const repos = await listRegisteredRepos();
|
|
123
|
-
if (repos.length === 0)
|
|
124
|
-
return null;
|
|
125
|
-
if (repoName)
|
|
126
|
-
return repos.find(r => r.name === repoName) || null;
|
|
127
|
-
return repos[0]; // default to first
|
|
128
|
-
};
|
|
129
|
-
// List all registered repos
|
|
130
|
-
app.get('/api/repos', async (_req, res) => {
|
|
131
|
-
try {
|
|
132
|
-
const repos = await listRegisteredRepos();
|
|
133
|
-
res.json(repos.map(r => ({
|
|
134
|
-
name: r.name, path: r.path, indexedAt: r.indexedAt,
|
|
135
|
-
lastCommit: r.lastCommit, stats: r.stats,
|
|
136
|
-
})));
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
res.status(500).json({ error: err.message || 'Failed to list repos' });
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
// Get repo info
|
|
143
|
-
app.get('/api/repo', async (req, res) => {
|
|
144
|
-
try {
|
|
145
|
-
const entry = await resolveRepo(requestedRepo(req));
|
|
146
|
-
if (!entry) {
|
|
147
|
-
res.status(404).json({ error: 'Repository not found. Run: code-mapper analyze' });
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const meta = await loadMeta(entry.storagePath);
|
|
151
|
-
res.json({
|
|
152
|
-
name: entry.name,
|
|
153
|
-
repoPath: entry.path,
|
|
154
|
-
indexedAt: meta?.indexedAt ?? entry.indexedAt,
|
|
155
|
-
stats: meta?.stats ?? entry.stats ?? {},
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
res.status(500).json({ error: err.message || 'Failed to get repo info' });
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
// Get full graph
|
|
163
|
-
app.get('/api/graph', async (req, res) => {
|
|
164
|
-
try {
|
|
165
|
-
const entry = await resolveRepo(requestedRepo(req));
|
|
166
|
-
if (!entry) {
|
|
167
|
-
res.status(404).json({ error: 'Repository not found' });
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
171
|
-
const graph = await withLbugDb(lbugPath, async () => buildGraph());
|
|
172
|
-
res.json(graph);
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
res.status(500).json({ error: err.message || 'Failed to build graph' });
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
// Execute Cypher query
|
|
179
|
-
app.post('/api/query', async (req, res) => {
|
|
180
|
-
try {
|
|
181
|
-
const cypher = req.body.cypher;
|
|
182
|
-
if (!cypher) {
|
|
183
|
-
res.status(400).json({ error: 'Missing "cypher" in request body' });
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const entry = await resolveRepo(requestedRepo(req));
|
|
187
|
-
if (!entry) {
|
|
188
|
-
res.status(404).json({ error: 'Repository not found' });
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
192
|
-
const result = await withLbugDb(lbugPath, () => executeQuery(cypher));
|
|
193
|
-
res.json({ result });
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
res.status(500).json({ error: err.message || 'Query failed' });
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
// Search
|
|
200
|
-
app.post('/api/search', async (req, res) => {
|
|
201
|
-
try {
|
|
202
|
-
const query = (req.body.query ?? '').trim();
|
|
203
|
-
if (!query) {
|
|
204
|
-
res.status(400).json({ error: 'Missing "query" in request body' });
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const entry = await resolveRepo(requestedRepo(req));
|
|
208
|
-
if (!entry) {
|
|
209
|
-
res.status(404).json({ error: 'Repository not found' });
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
213
|
-
const parsedLimit = Number(req.body.limit ?? 10);
|
|
214
|
-
const limit = Number.isFinite(parsedLimit)
|
|
215
|
-
? Math.max(1, Math.min(100, Math.trunc(parsedLimit)))
|
|
216
|
-
: 10;
|
|
217
|
-
const results = await withLbugDb(lbugPath, async () => {
|
|
218
|
-
const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
|
|
219
|
-
if (isEmbedderReady()) {
|
|
220
|
-
const { semanticSearch } = await import('../core/embeddings/embedding-pipeline.js');
|
|
221
|
-
return hybridSearch(query, limit, executeQuery, semanticSearch);
|
|
222
|
-
}
|
|
223
|
-
// FTS-only fallback when embeddings aren't loaded
|
|
224
|
-
return searchFTSFromLbug(query, limit);
|
|
225
|
-
});
|
|
226
|
-
res.json({ results });
|
|
227
|
-
}
|
|
228
|
-
catch (err) {
|
|
229
|
-
res.status(500).json({ error: err.message || 'Search failed' });
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
// Read file (with path traversal guard)
|
|
233
|
-
app.get('/api/file', async (req, res) => {
|
|
234
|
-
try {
|
|
235
|
-
const entry = await resolveRepo(requestedRepo(req));
|
|
236
|
-
if (!entry) {
|
|
237
|
-
res.status(404).json({ error: 'Repository not found' });
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
const filePath = req.query.path;
|
|
241
|
-
if (!filePath) {
|
|
242
|
-
res.status(400).json({ error: 'Missing path' });
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
// Prevent path traversal: resolve and verify path stays within repo root
|
|
246
|
-
const repoRoot = path.resolve(entry.path);
|
|
247
|
-
const fullPath = path.resolve(repoRoot, filePath);
|
|
248
|
-
if (!fullPath.startsWith(repoRoot + path.sep) && fullPath !== repoRoot) {
|
|
249
|
-
res.status(403).json({ error: 'Path traversal denied' });
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const content = await fs.readFile(fullPath, 'utf-8');
|
|
253
|
-
res.json({ content });
|
|
254
|
-
}
|
|
255
|
-
catch (err) {
|
|
256
|
-
if (err.code === 'ENOENT') {
|
|
257
|
-
res.status(404).json({ error: 'File not found' });
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
res.status(500).json({ error: err.message || 'Failed to read file' });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
// List all processes
|
|
265
|
-
app.get('/api/processes', async (req, res) => {
|
|
266
|
-
try {
|
|
267
|
-
const result = await backend.queryProcesses(requestedRepo(req));
|
|
268
|
-
res.json(result);
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
res.status(statusFromError(err)).json({ error: err.message || 'Failed to query processes' });
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
// Process detail
|
|
275
|
-
app.get('/api/process', async (req, res) => {
|
|
276
|
-
try {
|
|
277
|
-
const name = String(req.query.name ?? '').trim();
|
|
278
|
-
if (!name) {
|
|
279
|
-
res.status(400).json({ error: 'Missing "name" query parameter' });
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
const result = await backend.queryProcessDetail(name, requestedRepo(req));
|
|
283
|
-
if (result?.error) {
|
|
284
|
-
res.status(404).json({ error: result.error });
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
res.json(result);
|
|
288
|
-
}
|
|
289
|
-
catch (err) {
|
|
290
|
-
res.status(statusFromError(err)).json({ error: err.message || 'Failed to query process detail' });
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
// List all clusters
|
|
294
|
-
app.get('/api/clusters', async (req, res) => {
|
|
295
|
-
try {
|
|
296
|
-
const result = await backend.queryClusters(requestedRepo(req));
|
|
297
|
-
res.json(result);
|
|
298
|
-
}
|
|
299
|
-
catch (err) {
|
|
300
|
-
res.status(statusFromError(err)).json({ error: err.message || 'Failed to query clusters' });
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
// Cluster detail
|
|
304
|
-
app.get('/api/cluster', async (req, res) => {
|
|
305
|
-
try {
|
|
306
|
-
const name = String(req.query.name ?? '').trim();
|
|
307
|
-
if (!name) {
|
|
308
|
-
res.status(400).json({ error: 'Missing "name" query parameter' });
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
const result = await backend.queryClusterDetail(name, requestedRepo(req));
|
|
312
|
-
if (result?.error) {
|
|
313
|
-
res.status(404).json({ error: result.error });
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
res.json(result);
|
|
317
|
-
}
|
|
318
|
-
catch (err) {
|
|
319
|
-
res.status(statusFromError(err)).json({ error: err.message || 'Failed to query cluster detail' });
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
// Global error handler
|
|
323
|
-
app.use((err, _req, res, _next) => {
|
|
324
|
-
console.error('Unhandled error:', err);
|
|
325
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
326
|
-
});
|
|
327
|
-
const server = app.listen(port, host, () => {
|
|
328
|
-
console.log(`Code Mapper server running on http://${host}:${port}`);
|
|
329
|
-
});
|
|
330
|
-
// Graceful shutdown
|
|
331
|
-
const shutdown = async () => {
|
|
332
|
-
server.close();
|
|
333
|
-
await cleanupMcp();
|
|
334
|
-
await closeLbug();
|
|
335
|
-
await backend.disconnect();
|
|
336
|
-
process.exit(0);
|
|
337
|
-
};
|
|
338
|
-
process.once('SIGINT', shutdown);
|
|
339
|
-
process.once('SIGTERM', shutdown);
|
|
340
|
-
};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/** @file mcp-http.ts
|
|
2
|
-
* @description Mounts Code Mapper MCP server on Express using StreamableHTTP transport
|
|
3
|
-
* Each client gets a stateful session; LocalBackend is shared (thread-safe)
|
|
4
|
-
* Sessions are evicted on close or after idle timeout */
|
|
5
|
-
import type { Express } from 'express';
|
|
6
|
-
import type { LocalBackend } from '../mcp/local/local-backend.js';
|
|
7
|
-
export declare function mountMCPEndpoints(app: Express, backend: LocalBackend): () => Promise<void>;
|