@codragraph/cli 2.1.1 → 2.1.4
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 +12 -9
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.js +19 -2
- package/dist/cli/index.js +2 -1
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +3 -1
- package/dist/cli/setup.js +36 -19
- package/dist/cli/status.d.ts +13 -0
- package/dist/cli/status.js +99 -0
- package/dist/config/ignore-service.js +2 -0
- package/dist/core/graphstore/cgdb-row-source.js +3 -2
- package/dist/core/group/bridge-db.js +42 -10
- package/dist/core/run-analyze.d.ts +20 -0
- package/dist/core/run-analyze.js +201 -0
- package/dist/core/search/hybrid-search.js +11 -3
- package/dist/mcp/resources.js +2 -2
- package/dist/server/api.d.ts +14 -2
- package/dist/server/api.js +90 -7
- package/dist/server/mcp-http.d.ts +22 -0
- package/dist/server/mcp-http.js +21 -2
- package/dist/server/web-dashboard.d.ts +28 -0
- package/dist/server/web-dashboard.js +61 -0
- package/dist/web/assets/agent-D5lb0zXz.js +1089 -0
- package/dist/web/assets/architectureDiagram-EMZXCZ2Q-CZtc99v_.js +36 -0
- package/dist/web/assets/blockDiagram-IGV67L2C-BtoUp-6Y.js +132 -0
- package/dist/web/assets/c4Diagram-DFAF54RM-C4Hl3J2U.js +10 -0
- package/dist/web/assets/chunk-3GS5O3IE-DkUjU0WD.js +231 -0
- package/dist/web/assets/chunk-3YCYZ6SJ-CQkVgT_z.js +1 -0
- package/dist/web/assets/chunk-7RZVMHOQ-BitYcNVR.js +338 -0
- package/dist/web/assets/chunk-AEOMTBSW-BgTIXPsY.js +1 -0
- package/dist/web/assets/chunk-H3VCZNTA-Cx5XV_aC.js +13 -0
- package/dist/web/assets/chunk-HN6EAY2L-BBnyTNdB.js +1 -0
- package/dist/web/assets/chunk-KSICW3F5-BYzvDLNI.js +15 -0
- package/dist/web/assets/chunk-O5ABG6QK-dHwHzA6n.js +1 -0
- package/dist/web/assets/chunk-PK6DOVAG-CvsEnugt.js +206 -0
- package/dist/web/assets/chunk-RWUO3TPN-BgRTY0_k.js +1 -0
- package/dist/web/assets/chunk-TBF5ZNIQ-DL5stGM1.js +1 -0
- package/dist/web/assets/chunk-TU3PZOEN-RLyvLcv-.js +1 -0
- package/dist/web/assets/classDiagram-PPOCWD7C-DTr8QIOf.js +1 -0
- package/dist/web/assets/classDiagram-v2-23LJLIIU-DTr8QIOf.js +1 -0
- package/dist/web/assets/context-builder-22jU3V56.js +16 -0
- package/dist/web/assets/cose-bilkent-PNC4W37J-DVhePRYg.js +1 -0
- package/dist/web/assets/dagre-E77IOHMT-Dzx0A6ZU.js +4 -0
- package/dist/web/assets/diagram-H7BISOXX-CC9pRew1.js +43 -0
- package/dist/web/assets/diagram-JC5VWROH-Bau_i9tf.js +24 -0
- package/dist/web/assets/diagram-LXUTUG65-D9_FM2Gt.js +10 -0
- package/dist/web/assets/diagram-WEHSV5V5-BMlayouL.js +24 -0
- package/dist/web/assets/erDiagram-GCSMX5X6-C3dhDFA8.js +85 -0
- package/dist/web/assets/flowDiagram-OTCZ4VVT-CWSFWmhr.js +162 -0
- package/dist/web/assets/ganttDiagram-MUNLMDZQ-D3a67Yol.js +292 -0
- package/dist/web/assets/gitGraphDiagram-3HKGZ4G3-7jmry-vM.js +106 -0
- package/dist/web/assets/index-BgeqpYgd.js +1415 -0
- package/dist/web/assets/index-CT0GtFLZ.css +1 -0
- package/dist/web/assets/infoDiagram-MN7RKWGX-G7lhP0Ib.js +2 -0
- package/dist/web/assets/ishikawaDiagram-YMYX4NHK-DUoJvNP2.js +70 -0
- package/dist/web/assets/journeyDiagram-SO5T7YLQ-RMFPNNqz.js +139 -0
- package/dist/web/assets/kanban-definition-LJHFXRCJ-BzpDs1K9.js +89 -0
- package/dist/web/assets/katex-GD7MH7QM-DBQvrix-.js +261 -0
- package/dist/web/assets/mindmap-definition-2EUWGEK5-Bk0O4roa.js +96 -0
- package/dist/web/assets/pieDiagram-3IATQBI2-DKU7kpgS.js +30 -0
- package/dist/web/assets/quadrantDiagram-E256RVCF-BY0TGWCS.js +7 -0
- package/dist/web/assets/requirementDiagram-M5DCFWZL-DLHOVTSv.js +84 -0
- package/dist/web/assets/sankeyDiagram-L3NBLAOT-DVMj5rX2.js +10 -0
- package/dist/web/assets/sequenceDiagram-ZOUHS735-CJC73bV-.js +157 -0
- package/dist/web/assets/stateDiagram-MLPALWAM-BCFyESls.js +1 -0
- package/dist/web/assets/stateDiagram-v2-B5LQ5ZB2-DahzzIca.js +1 -0
- package/dist/web/assets/timeline-definition-5SPVSISX-TRSDRgPw.js +120 -0
- package/dist/web/assets/vennDiagram-IE5QUKF5-DNy7HRBM.js +34 -0
- package/dist/web/assets/wardley-RL74JXVD-BCRCBASE-B-eZEzf9.js +161 -0
- package/dist/web/assets/wardleyDiagram-XU3VSMPF-BP-r1xzR.js +20 -0
- package/dist/web/assets/xychartDiagram-ZHJ5623Y-Dr9r7a35.js +7 -0
- package/dist/web/codragraph-logo-512.png +0 -0
- package/dist/web/codragraph-logo.png +0 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/index.html +36 -0
- package/hooks/claude/codragraph-hook.cjs +24 -9
- package/hooks/claude/pre-tool-use.sh +6 -1
- package/package.json +3 -1
- package/scripts/build.js +62 -4
- package/scripts/patch-tree-sitter-swift.cjs +0 -1
- package/skills/codragraph-cli.md +1 -1
- package/vendor/leiden/index.cjs +272 -285
- package/vendor/leiden/utils.cjs +264 -274
- package/dist/_shared/lbug/schema-constants.d.ts +0 -16
- package/dist/_shared/lbug/schema-constants.d.ts.map +0 -1
- package/dist/_shared/lbug/schema-constants.js +0 -67
- package/dist/_shared/lbug/schema-constants.js.map +0 -1
- package/dist/core/graphstore/lbug-row-source.d.ts +0 -19
- package/dist/core/graphstore/lbug-row-source.js +0 -141
- package/dist/core/lbug/content-read.d.ts +0 -46
- package/dist/core/lbug/content-read.js +0 -64
- package/dist/core/lbug/csv-generator.d.ts +0 -29
- package/dist/core/lbug/csv-generator.js +0 -492
- package/dist/core/lbug/lbug-adapter.d.ts +0 -176
- package/dist/core/lbug/lbug-adapter.js +0 -1320
- package/dist/core/lbug/pool-adapter.d.ts +0 -93
- package/dist/core/lbug/pool-adapter.js +0 -550
- package/dist/core/lbug/schema.d.ts +0 -62
- package/dist/core/lbug/schema.js +0 -502
- package/dist/mcp/core/lbug-adapter.d.ts +0 -5
- package/dist/mcp/core/lbug-adapter.js +0 -5
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LadybugDB connection pool (core). Used by MCP, sync, search, wiki, etc.
|
|
3
|
-
*
|
|
4
|
-
* LadybugDB Adapter (Connection Pool)
|
|
5
|
-
*
|
|
6
|
-
* Manages a pool of LadybugDB databases keyed by repoId, each with
|
|
7
|
-
* multiple Connection objects for safe concurrent query execution.
|
|
8
|
-
*
|
|
9
|
-
* LadybugDB Connections are NOT thread-safe — a single Connection
|
|
10
|
-
* segfaults if concurrent .query() calls hit it simultaneously.
|
|
11
|
-
* This adapter provides a checkout/return connection pool so each
|
|
12
|
-
* concurrent query gets its own Connection from the same Database.
|
|
13
|
-
*
|
|
14
|
-
* @see https://docs.ladybugdb.com/concurrency — multiple Connections
|
|
15
|
-
* from the same Database is the officially supported concurrency pattern.
|
|
16
|
-
*/
|
|
17
|
-
import lbug from '@ladybugdb/core';
|
|
18
|
-
/**
|
|
19
|
-
* Listeners notified when a pool entry is torn down (LRU eviction, idle
|
|
20
|
-
* timeout, explicit close). Used by upper layers (e.g. the BM25 search
|
|
21
|
-
* module) to invalidate per-repo caches that must not outlive the pool
|
|
22
|
-
* entry that produced them.
|
|
23
|
-
*
|
|
24
|
-
* Listeners run synchronously inside `closeOne` after the pool entry has
|
|
25
|
-
* been removed; throwing listeners are isolated so one bad listener does
|
|
26
|
-
* not prevent others from firing or break teardown.
|
|
27
|
-
*/
|
|
28
|
-
type PoolCloseListener = (repoId: string) => void;
|
|
29
|
-
/**
|
|
30
|
-
* Subscribe to pool-close events. Returns a disposer that removes the
|
|
31
|
-
* listener (handy for tests).
|
|
32
|
-
*/
|
|
33
|
-
export declare function addPoolCloseListener(listener: PoolCloseListener): () => void;
|
|
34
|
-
/** Saved real stdout/stderr write — used to silence native module output without race conditions */
|
|
35
|
-
export declare const realStdoutWrite: any;
|
|
36
|
-
export declare const realStderrWrite: any;
|
|
37
|
-
/**
|
|
38
|
-
* Touch a repo to reset its idle timeout.
|
|
39
|
-
* Call this during long-running operations to prevent the connection from being closed.
|
|
40
|
-
*/
|
|
41
|
-
export declare const touchRepo: (repoId: string) => void;
|
|
42
|
-
/**
|
|
43
|
-
* Silence stdout by replacing process.stdout.write with a no-op.
|
|
44
|
-
* Uses a reference counter so nested silence/restore pairs are safe.
|
|
45
|
-
* Exported so other modules (e.g. embedder) use the same mechanism instead
|
|
46
|
-
* of independently patching stdout, which causes restore-order conflicts.
|
|
47
|
-
*/
|
|
48
|
-
export declare function silenceStdout(): void;
|
|
49
|
-
export declare function restoreStdout(): void;
|
|
50
|
-
/**
|
|
51
|
-
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
|
52
|
-
* Retries on lock errors (e.g., when `codragraph analyze` is running).
|
|
53
|
-
*
|
|
54
|
-
* Concurrent calls for the same repoId are deduplicated — the second caller
|
|
55
|
-
* awaits the first's in-progress init rather than starting a redundant one.
|
|
56
|
-
*/
|
|
57
|
-
export declare const initLbug: (repoId: string, dbPath: string) => Promise<void>;
|
|
58
|
-
/**
|
|
59
|
-
* Initialize a pool entry from a pre-existing Database object.
|
|
60
|
-
*
|
|
61
|
-
* Used in tests to avoid the writable→close→read-only cycle that crashes
|
|
62
|
-
* on macOS due to N-API destructor segfaults. The pool adapter reuses
|
|
63
|
-
* the core adapter's writable Database instead of opening a new read-only one.
|
|
64
|
-
*
|
|
65
|
-
* The Database is registered in the shared dbCache so closeOne() decrements
|
|
66
|
-
* the refCount correctly. If the Database is already cached (e.g. another
|
|
67
|
-
* repoId already injected it), the existing entry is reused.
|
|
68
|
-
*/
|
|
69
|
-
export declare function initLbugWithDb(repoId: string, existingDb: lbug.Database, dbPath: string): Promise<void>;
|
|
70
|
-
export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
|
|
71
|
-
/**
|
|
72
|
-
* Execute a parameterized query on a specific repo's connection pool.
|
|
73
|
-
* Uses prepare/execute pattern to prevent Cypher injection.
|
|
74
|
-
*/
|
|
75
|
-
export declare const executeParameterized: (repoId: string, cypher: string, params: Record<string, any>) => Promise<any[]>;
|
|
76
|
-
/**
|
|
77
|
-
* Close one or all repo pools.
|
|
78
|
-
* If repoId is provided, close only that repo's connections.
|
|
79
|
-
* If omitted, close all repos.
|
|
80
|
-
*/
|
|
81
|
-
export declare const closeLbug: (repoId?: string) => Promise<void>;
|
|
82
|
-
/**
|
|
83
|
-
* Check if a specific repo's pool is active
|
|
84
|
-
*/
|
|
85
|
-
export declare const isLbugReady: (repoId: string) => boolean;
|
|
86
|
-
/** Regex to detect write operations in user-supplied Cypher queries.
|
|
87
|
-
* Note: CALL is NOT blocked — it's used for read-only FTS (CALL QUERY_FTS_INDEX)
|
|
88
|
-
* and vector search (CALL QUERY_VECTOR_INDEX). The database is opened in
|
|
89
|
-
* read-only mode as defense-in-depth against write procedures. */
|
|
90
|
-
export declare const CYPHER_WRITE_RE: RegExp;
|
|
91
|
-
/** Check if a Cypher query contains write operations */
|
|
92
|
-
export declare function isWriteQuery(query: string): boolean;
|
|
93
|
-
export {};
|
|
@@ -1,550 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LadybugDB connection pool (core). Used by MCP, sync, search, wiki, etc.
|
|
3
|
-
*
|
|
4
|
-
* LadybugDB Adapter (Connection Pool)
|
|
5
|
-
*
|
|
6
|
-
* Manages a pool of LadybugDB databases keyed by repoId, each with
|
|
7
|
-
* multiple Connection objects for safe concurrent query execution.
|
|
8
|
-
*
|
|
9
|
-
* LadybugDB Connections are NOT thread-safe — a single Connection
|
|
10
|
-
* segfaults if concurrent .query() calls hit it simultaneously.
|
|
11
|
-
* This adapter provides a checkout/return connection pool so each
|
|
12
|
-
* concurrent query gets its own Connection from the same Database.
|
|
13
|
-
*
|
|
14
|
-
* @see https://docs.ladybugdb.com/concurrency — multiple Connections
|
|
15
|
-
* from the same Database is the officially supported concurrency pattern.
|
|
16
|
-
*/
|
|
17
|
-
import fs from 'fs/promises';
|
|
18
|
-
import lbug from '@ladybugdb/core';
|
|
19
|
-
const pool = new Map();
|
|
20
|
-
const poolCloseListeners = new Set();
|
|
21
|
-
/**
|
|
22
|
-
* Subscribe to pool-close events. Returns a disposer that removes the
|
|
23
|
-
* listener (handy for tests).
|
|
24
|
-
*/
|
|
25
|
-
export function addPoolCloseListener(listener) {
|
|
26
|
-
poolCloseListeners.add(listener);
|
|
27
|
-
return () => {
|
|
28
|
-
poolCloseListeners.delete(listener);
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
const dbCache = new Map();
|
|
32
|
-
/** Max repos in the pool (LRU eviction) */
|
|
33
|
-
const MAX_POOL_SIZE = 5;
|
|
34
|
-
/** Idle timeout before closing a repo's connections */
|
|
35
|
-
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
36
|
-
/** Max connections per repo (caps concurrent queries per repo) */
|
|
37
|
-
const MAX_CONNS_PER_REPO = 8;
|
|
38
|
-
let idleTimer = null;
|
|
39
|
-
/** Saved real stdout/stderr write — used to silence native module output without race conditions */
|
|
40
|
-
export const realStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
41
|
-
export const realStderrWrite = process.stderr.write.bind(process.stderr);
|
|
42
|
-
let stdoutSilenceCount = 0;
|
|
43
|
-
/** True while pre-warming connections — prevents watchdog from prematurely restoring stdout */
|
|
44
|
-
let preWarmActive = false;
|
|
45
|
-
/**
|
|
46
|
-
* Start the idle cleanup timer (runs every 60s)
|
|
47
|
-
*/
|
|
48
|
-
function ensureIdleTimer() {
|
|
49
|
-
if (idleTimer)
|
|
50
|
-
return;
|
|
51
|
-
idleTimer = setInterval(() => {
|
|
52
|
-
const now = Date.now();
|
|
53
|
-
for (const [repoId, entry] of pool) {
|
|
54
|
-
if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
|
|
55
|
-
closeOne(repoId);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}, 60_000);
|
|
59
|
-
if (idleTimer && typeof idleTimer === 'object' && 'unref' in idleTimer) {
|
|
60
|
-
idleTimer.unref();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Touch a repo to reset its idle timeout.
|
|
65
|
-
* Call this during long-running operations to prevent the connection from being closed.
|
|
66
|
-
*/
|
|
67
|
-
export const touchRepo = (repoId) => {
|
|
68
|
-
const entry = pool.get(repoId);
|
|
69
|
-
if (entry) {
|
|
70
|
-
entry.lastUsed = Date.now();
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
/**
|
|
74
|
-
* Evict the least-recently-used repo if pool is at capacity
|
|
75
|
-
*/
|
|
76
|
-
function evictLRU() {
|
|
77
|
-
if (pool.size < MAX_POOL_SIZE)
|
|
78
|
-
return;
|
|
79
|
-
let oldestId = null;
|
|
80
|
-
let oldestTime = Infinity;
|
|
81
|
-
for (const [id, entry] of pool) {
|
|
82
|
-
if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
|
|
83
|
-
oldestTime = entry.lastUsed;
|
|
84
|
-
oldestId = id;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (oldestId) {
|
|
88
|
-
closeOne(oldestId);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Remove a repo from the pool, close its connections, and release its
|
|
93
|
-
* shared Database ref. Only closes the Database when no other repoIds
|
|
94
|
-
* reference it (refCount === 0).
|
|
95
|
-
*/
|
|
96
|
-
function closeOne(repoId) {
|
|
97
|
-
const entry = pool.get(repoId);
|
|
98
|
-
if (!entry)
|
|
99
|
-
return;
|
|
100
|
-
entry.closed = true;
|
|
101
|
-
// Close available connections — fire-and-forget with .catch() to prevent
|
|
102
|
-
// unhandled rejections. Native close() returns Promise<void> but can crash
|
|
103
|
-
// the N-API destructor on macOS/Windows; deferring to process exit lets
|
|
104
|
-
// dangerouslyIgnoreUnhandledErrors absorb the crash.
|
|
105
|
-
for (const conn of entry.available) {
|
|
106
|
-
conn.close().catch(() => { });
|
|
107
|
-
}
|
|
108
|
-
entry.available.length = 0;
|
|
109
|
-
// Checked-out connections can't be closed here — they're in-flight.
|
|
110
|
-
// The checkin() function detects entry.closed and closes them on return.
|
|
111
|
-
// Only close the Database when no other repoIds reference it.
|
|
112
|
-
// External databases (injected via initLbugWithDb) are never closed here —
|
|
113
|
-
// the core adapter owns them and handles their lifecycle.
|
|
114
|
-
const shared = dbCache.get(entry.dbPath);
|
|
115
|
-
if (shared) {
|
|
116
|
-
shared.refCount--;
|
|
117
|
-
if (shared.refCount === 0) {
|
|
118
|
-
if (shared.external) {
|
|
119
|
-
// External databases are owned by the core adapter — don't close
|
|
120
|
-
// or remove from cache. Keep the entry so future initLbug() calls
|
|
121
|
-
// for the same dbPath reuse it instead of hitting a file lock.
|
|
122
|
-
shared.refCount = 0;
|
|
123
|
-
shared.ftsLoaded = false;
|
|
124
|
-
shared.vectorLoaded = false;
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
shared.db.close().catch(() => { });
|
|
128
|
-
dbCache.delete(entry.dbPath);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
pool.delete(repoId);
|
|
133
|
-
// Notify listeners AFTER the pool entry is gone so any cache-invalidation
|
|
134
|
-
// they perform is consistent with `isLbugReady(repoId) === false`.
|
|
135
|
-
for (const listener of poolCloseListeners) {
|
|
136
|
-
try {
|
|
137
|
-
listener(repoId);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Isolate listener failures — teardown must complete.
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Create a new Connection from a repo's Database.
|
|
146
|
-
* Silences stdout to prevent native module output from corrupting MCP stdio.
|
|
147
|
-
*/
|
|
148
|
-
let activeQueryCount = 0;
|
|
149
|
-
/**
|
|
150
|
-
* Silence stdout by replacing process.stdout.write with a no-op.
|
|
151
|
-
* Uses a reference counter so nested silence/restore pairs are safe.
|
|
152
|
-
* Exported so other modules (e.g. embedder) use the same mechanism instead
|
|
153
|
-
* of independently patching stdout, which causes restore-order conflicts.
|
|
154
|
-
*/
|
|
155
|
-
export function silenceStdout() {
|
|
156
|
-
if (stdoutSilenceCount++ === 0) {
|
|
157
|
-
process.stdout.write = (() => true);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
export function restoreStdout() {
|
|
161
|
-
if (--stdoutSilenceCount <= 0) {
|
|
162
|
-
stdoutSilenceCount = 0;
|
|
163
|
-
process.stdout.write = realStdoutWrite;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
|
|
167
|
-
// inside createConnection before restoreStdout runs).
|
|
168
|
-
// Exempts active queries and pre-warm — these legitimately hold silence for
|
|
169
|
-
// longer than 1 second (queries can take up to QUERY_TIMEOUT_MS = 30s).
|
|
170
|
-
setInterval(() => {
|
|
171
|
-
if (stdoutSilenceCount > 0 && !preWarmActive && activeQueryCount === 0) {
|
|
172
|
-
stdoutSilenceCount = 0;
|
|
173
|
-
process.stdout.write = realStdoutWrite;
|
|
174
|
-
}
|
|
175
|
-
}, 1000).unref();
|
|
176
|
-
function createConnection(db) {
|
|
177
|
-
silenceStdout();
|
|
178
|
-
try {
|
|
179
|
-
return new lbug.Connection(db);
|
|
180
|
-
}
|
|
181
|
-
finally {
|
|
182
|
-
restoreStdout();
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/** Query timeout in milliseconds */
|
|
186
|
-
const QUERY_TIMEOUT_MS = 30_000;
|
|
187
|
-
/** Waiter queue timeout in milliseconds */
|
|
188
|
-
const WAITER_TIMEOUT_MS = 15_000;
|
|
189
|
-
const LOCK_RETRY_ATTEMPTS = 3;
|
|
190
|
-
const LOCK_RETRY_DELAY_MS = 2000;
|
|
191
|
-
/** Deduplicates concurrent initLbug calls for the same repoId */
|
|
192
|
-
const initPromises = new Map();
|
|
193
|
-
/**
|
|
194
|
-
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
|
195
|
-
* Retries on lock errors (e.g., when `codragraph analyze` is running).
|
|
196
|
-
*
|
|
197
|
-
* Concurrent calls for the same repoId are deduplicated — the second caller
|
|
198
|
-
* awaits the first's in-progress init rather than starting a redundant one.
|
|
199
|
-
*/
|
|
200
|
-
export const initLbug = async (repoId, dbPath) => {
|
|
201
|
-
const existing = pool.get(repoId);
|
|
202
|
-
if (existing) {
|
|
203
|
-
existing.lastUsed = Date.now();
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
// Deduplicate concurrent init calls for the same repoId —
|
|
207
|
-
// prevents double-init race when multiple parallel tool calls
|
|
208
|
-
// trigger initialization for the same repo simultaneously.
|
|
209
|
-
const pending = initPromises.get(repoId);
|
|
210
|
-
if (pending)
|
|
211
|
-
return pending;
|
|
212
|
-
const promise = doInitLbug(repoId, dbPath);
|
|
213
|
-
initPromises.set(repoId, promise);
|
|
214
|
-
try {
|
|
215
|
-
await promise;
|
|
216
|
-
}
|
|
217
|
-
finally {
|
|
218
|
-
initPromises.delete(repoId);
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
/**
|
|
222
|
-
* Internal init — creates DB, pre-warms connections, loads FTS, then registers pool.
|
|
223
|
-
* Pool entry is registered LAST so concurrent executeQuery calls see either
|
|
224
|
-
* "not initialized" (and throw) or a fully ready pool — never a half-built one.
|
|
225
|
-
*/
|
|
226
|
-
async function doInitLbug(repoId, dbPath) {
|
|
227
|
-
// Check if database exists
|
|
228
|
-
try {
|
|
229
|
-
await fs.stat(dbPath);
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
throw new Error(`LadybugDB not found at ${dbPath}. Run: codragraph analyze`);
|
|
233
|
-
}
|
|
234
|
-
evictLRU();
|
|
235
|
-
// Reuse an existing native Database if another repoId already opened this path.
|
|
236
|
-
// This prevents buffer manager exhaustion from multiple mmap regions on the same file.
|
|
237
|
-
let shared = dbCache.get(dbPath);
|
|
238
|
-
if (!shared) {
|
|
239
|
-
// Open in read-only mode — MCP server never writes to the database.
|
|
240
|
-
// This allows multiple MCP server instances to read concurrently, and
|
|
241
|
-
// avoids lock conflicts when `codragraph analyze` is writing.
|
|
242
|
-
let lastError = null;
|
|
243
|
-
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
|
244
|
-
silenceStdout();
|
|
245
|
-
try {
|
|
246
|
-
const db = new lbug.Database(dbPath, 0, // bufferManagerSize (default)
|
|
247
|
-
false, // enableCompression (default)
|
|
248
|
-
true);
|
|
249
|
-
restoreStdout();
|
|
250
|
-
shared = { db, refCount: 0, ftsLoaded: false, vectorLoaded: false };
|
|
251
|
-
dbCache.set(dbPath, shared);
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
catch (err) {
|
|
255
|
-
restoreStdout();
|
|
256
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
257
|
-
const isLockError = lastError.message.includes('Could not set lock') || lastError.message.includes('lock');
|
|
258
|
-
if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS)
|
|
259
|
-
break;
|
|
260
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (!shared) {
|
|
264
|
-
throw new Error(`LadybugDB unavailable for ${repoId}. Another process may be rebuilding the index. ` +
|
|
265
|
-
`Retry later. (${lastError?.message || 'unknown error'})`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
shared.refCount++;
|
|
269
|
-
const db = shared.db;
|
|
270
|
-
// Pre-create the full pool upfront so createConnection() (which silences
|
|
271
|
-
// stdout) is never called lazily during active query execution.
|
|
272
|
-
// Mark preWarmActive so the watchdog timer doesn't interfere.
|
|
273
|
-
preWarmActive = true;
|
|
274
|
-
const available = [];
|
|
275
|
-
try {
|
|
276
|
-
for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
|
|
277
|
-
available.push(createConnection(db));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
finally {
|
|
281
|
-
preWarmActive = false;
|
|
282
|
-
}
|
|
283
|
-
// Load FTS + VECTOR extensions on EVERY connection in the pool.
|
|
284
|
-
//
|
|
285
|
-
// CRITICAL: LadybugDB's extension state is per-connection on macOS (and
|
|
286
|
-
// possibly other platforms) — loading on `available[0]` does NOT propagate
|
|
287
|
-
// to the other connections. Since checkout pops from the end of the array,
|
|
288
|
-
// queries would otherwise hit unloaded connections and `QUERY_FTS_INDEX`
|
|
289
|
-
// would silently return 0 rows (catch in queryFTSViaExecutor swallows the
|
|
290
|
-
// "extension not loaded" error). That broke every search on macOS.
|
|
291
|
-
//
|
|
292
|
-
// Load on all connections concurrently, but BEFORE the pool is registered
|
|
293
|
-
// so no checkout can race the load.
|
|
294
|
-
await loadExtensionsOnConnections(available, shared);
|
|
295
|
-
// Register pool entry only after all connections are pre-warmed and FTS is
|
|
296
|
-
// loaded. Concurrent executeQuery calls see either "not initialized"
|
|
297
|
-
// (and throw cleanly) or a fully ready pool — never a half-built one.
|
|
298
|
-
pool.set(repoId, {
|
|
299
|
-
db,
|
|
300
|
-
available,
|
|
301
|
-
checkedOut: 0,
|
|
302
|
-
waiters: [],
|
|
303
|
-
lastUsed: Date.now(),
|
|
304
|
-
dbPath,
|
|
305
|
-
closed: false,
|
|
306
|
-
});
|
|
307
|
-
ensureIdleTimer();
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Load FTS + VECTOR extensions on every connection in the pool.
|
|
311
|
-
* Per-connection load is required because LadybugDB's extension state is
|
|
312
|
-
* not always shared across connections of the same Database (observed on
|
|
313
|
-
* macOS native bindings — silent FTS failures otherwise).
|
|
314
|
-
*/
|
|
315
|
-
async function loadExtensionsOnConnections(conns, shared) {
|
|
316
|
-
// FTS: every connection needs `LOAD EXTENSION fts` independently.
|
|
317
|
-
await Promise.all(conns.map(async (c) => {
|
|
318
|
-
try {
|
|
319
|
-
await c.query('LOAD EXTENSION fts');
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
// already-loaded / not-installed — FTS queries fail gracefully if missing
|
|
323
|
-
}
|
|
324
|
-
}));
|
|
325
|
-
shared.ftsLoaded = true;
|
|
326
|
-
// VECTOR: install once on the Database (idempotent), then LOAD on every
|
|
327
|
-
// connection. INSTALL is a no-op after the first call but we run it on
|
|
328
|
-
// conn[0] to guarantee the package is present before fanning out LOADs.
|
|
329
|
-
try {
|
|
330
|
-
await conns[0].query('INSTALL VECTOR');
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
/* not available — semantic search will be a no-op */
|
|
334
|
-
}
|
|
335
|
-
await Promise.all(conns.map(async (c) => {
|
|
336
|
-
try {
|
|
337
|
-
await c.query('LOAD EXTENSION VECTOR');
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
/* not available — semantic search will be a no-op */
|
|
341
|
-
}
|
|
342
|
-
}));
|
|
343
|
-
shared.vectorLoaded = true;
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Initialize a pool entry from a pre-existing Database object.
|
|
347
|
-
*
|
|
348
|
-
* Used in tests to avoid the writable→close→read-only cycle that crashes
|
|
349
|
-
* on macOS due to N-API destructor segfaults. The pool adapter reuses
|
|
350
|
-
* the core adapter's writable Database instead of opening a new read-only one.
|
|
351
|
-
*
|
|
352
|
-
* The Database is registered in the shared dbCache so closeOne() decrements
|
|
353
|
-
* the refCount correctly. If the Database is already cached (e.g. another
|
|
354
|
-
* repoId already injected it), the existing entry is reused.
|
|
355
|
-
*/
|
|
356
|
-
export async function initLbugWithDb(repoId, existingDb, dbPath) {
|
|
357
|
-
const existing = pool.get(repoId);
|
|
358
|
-
if (existing) {
|
|
359
|
-
existing.lastUsed = Date.now();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
// Register in dbCache with external: true so other initLbug() calls
|
|
363
|
-
// for the same dbPath reuse this Database instead of trying to open
|
|
364
|
-
// a new one (which would fail with a file lock error).
|
|
365
|
-
// closeOne() respects the external flag and skips db.close().
|
|
366
|
-
let shared = dbCache.get(dbPath);
|
|
367
|
-
if (!shared) {
|
|
368
|
-
shared = { db: existingDb, refCount: 0, ftsLoaded: false, vectorLoaded: false, external: true };
|
|
369
|
-
dbCache.set(dbPath, shared);
|
|
370
|
-
}
|
|
371
|
-
shared.refCount++;
|
|
372
|
-
const available = [];
|
|
373
|
-
preWarmActive = true;
|
|
374
|
-
try {
|
|
375
|
-
for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
|
|
376
|
-
available.push(createConnection(existingDb));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
finally {
|
|
380
|
-
preWarmActive = false;
|
|
381
|
-
}
|
|
382
|
-
// Load FTS + VECTOR on every connection (see loadExtensionsOnConnections
|
|
383
|
-
// for why per-connection load is required on macOS native bindings).
|
|
384
|
-
await loadExtensionsOnConnections(available, shared);
|
|
385
|
-
pool.set(repoId, {
|
|
386
|
-
db: existingDb,
|
|
387
|
-
available,
|
|
388
|
-
checkedOut: 0,
|
|
389
|
-
waiters: [],
|
|
390
|
-
lastUsed: Date.now(),
|
|
391
|
-
dbPath,
|
|
392
|
-
closed: false,
|
|
393
|
-
});
|
|
394
|
-
ensureIdleTimer();
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Checkout a connection from the pool.
|
|
398
|
-
* Returns an available connection, or creates a new one if under the cap.
|
|
399
|
-
* If all connections are busy and at cap, queues the caller until one is returned.
|
|
400
|
-
*/
|
|
401
|
-
function checkout(entry) {
|
|
402
|
-
// Fast path: grab an available connection
|
|
403
|
-
if (entry.available.length > 0) {
|
|
404
|
-
entry.checkedOut++;
|
|
405
|
-
return Promise.resolve(entry.available.pop());
|
|
406
|
-
}
|
|
407
|
-
// Pool was pre-warmed to MAX_CONNS_PER_REPO during init. If we're here
|
|
408
|
-
// with fewer total connections, something leaked — surface the bug rather
|
|
409
|
-
// than silently creating a connection (which would silence stdout mid-query).
|
|
410
|
-
const totalConns = entry.available.length + entry.checkedOut;
|
|
411
|
-
if (totalConns < MAX_CONNS_PER_REPO) {
|
|
412
|
-
throw new Error(`Connection pool integrity error: expected ${MAX_CONNS_PER_REPO} ` +
|
|
413
|
-
`connections but found ${totalConns} (${entry.available.length} available, ` +
|
|
414
|
-
`${entry.checkedOut} checked out)`);
|
|
415
|
-
}
|
|
416
|
-
// At capacity — queue the caller with a timeout.
|
|
417
|
-
return new Promise((resolve, reject) => {
|
|
418
|
-
const waiter = (conn) => {
|
|
419
|
-
clearTimeout(timer);
|
|
420
|
-
resolve(conn);
|
|
421
|
-
};
|
|
422
|
-
const timer = setTimeout(() => {
|
|
423
|
-
const idx = entry.waiters.indexOf(waiter);
|
|
424
|
-
if (idx !== -1)
|
|
425
|
-
entry.waiters.splice(idx, 1);
|
|
426
|
-
reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
|
|
427
|
-
}, WAITER_TIMEOUT_MS);
|
|
428
|
-
entry.waiters.push(waiter);
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Return a connection to the pool after use.
|
|
433
|
-
* If the pool entry was closed while the connection was checked out (e.g.
|
|
434
|
-
* LRU eviction), close the orphaned connection instead of returning it.
|
|
435
|
-
* If there are queued waiters, hand the connection directly to the next one
|
|
436
|
-
* instead of putting it back in the available array (avoids race conditions).
|
|
437
|
-
*/
|
|
438
|
-
function checkin(entry, conn) {
|
|
439
|
-
if (entry.closed) {
|
|
440
|
-
// Pool entry was deleted during checkout — close the orphaned connection
|
|
441
|
-
conn.close().catch(() => { });
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
if (entry.waiters.length > 0) {
|
|
445
|
-
// Hand directly to the next waiter — no intermediate available state
|
|
446
|
-
const waiter = entry.waiters.shift();
|
|
447
|
-
waiter(conn);
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
entry.checkedOut--;
|
|
451
|
-
entry.available.push(conn);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* Execute a query on a specific repo's connection pool.
|
|
456
|
-
* Automatically checks out a connection, runs the query, and returns it.
|
|
457
|
-
*/
|
|
458
|
-
/** Race a promise against a timeout */
|
|
459
|
-
function withTimeout(promise, ms, label) {
|
|
460
|
-
let timer;
|
|
461
|
-
const timeout = new Promise((_, reject) => {
|
|
462
|
-
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
463
|
-
});
|
|
464
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
465
|
-
}
|
|
466
|
-
export const executeQuery = async (repoId, cypher) => {
|
|
467
|
-
const entry = pool.get(repoId);
|
|
468
|
-
if (!entry) {
|
|
469
|
-
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
470
|
-
}
|
|
471
|
-
if (isWriteQuery(cypher)) {
|
|
472
|
-
throw new Error('Write operations are not allowed. The pool adapter is read-only.');
|
|
473
|
-
}
|
|
474
|
-
entry.lastUsed = Date.now();
|
|
475
|
-
const conn = await checkout(entry);
|
|
476
|
-
silenceStdout();
|
|
477
|
-
activeQueryCount++;
|
|
478
|
-
try {
|
|
479
|
-
const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
|
|
480
|
-
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
481
|
-
const rows = await result.getAll();
|
|
482
|
-
return rows;
|
|
483
|
-
}
|
|
484
|
-
finally {
|
|
485
|
-
activeQueryCount--;
|
|
486
|
-
restoreStdout();
|
|
487
|
-
checkin(entry, conn);
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
/**
|
|
491
|
-
* Execute a parameterized query on a specific repo's connection pool.
|
|
492
|
-
* Uses prepare/execute pattern to prevent Cypher injection.
|
|
493
|
-
*/
|
|
494
|
-
export const executeParameterized = async (repoId, cypher, params) => {
|
|
495
|
-
const entry = pool.get(repoId);
|
|
496
|
-
if (!entry) {
|
|
497
|
-
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
498
|
-
}
|
|
499
|
-
entry.lastUsed = Date.now();
|
|
500
|
-
const conn = await checkout(entry);
|
|
501
|
-
silenceStdout();
|
|
502
|
-
activeQueryCount++;
|
|
503
|
-
try {
|
|
504
|
-
const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
|
|
505
|
-
if (!stmt.isSuccess()) {
|
|
506
|
-
const errMsg = await stmt.getErrorMessage();
|
|
507
|
-
throw new Error(`Prepare failed: ${errMsg}`);
|
|
508
|
-
}
|
|
509
|
-
const queryResult = await withTimeout(conn.execute(stmt, params), QUERY_TIMEOUT_MS, 'Execute');
|
|
510
|
-
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
511
|
-
const rows = await result.getAll();
|
|
512
|
-
return rows;
|
|
513
|
-
}
|
|
514
|
-
finally {
|
|
515
|
-
activeQueryCount--;
|
|
516
|
-
restoreStdout();
|
|
517
|
-
checkin(entry, conn);
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
/**
|
|
521
|
-
* Close one or all repo pools.
|
|
522
|
-
* If repoId is provided, close only that repo's connections.
|
|
523
|
-
* If omitted, close all repos.
|
|
524
|
-
*/
|
|
525
|
-
export const closeLbug = async (repoId) => {
|
|
526
|
-
if (repoId) {
|
|
527
|
-
closeOne(repoId);
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
for (const id of [...pool.keys()]) {
|
|
531
|
-
closeOne(id);
|
|
532
|
-
}
|
|
533
|
-
if (idleTimer) {
|
|
534
|
-
clearInterval(idleTimer);
|
|
535
|
-
idleTimer = null;
|
|
536
|
-
}
|
|
537
|
-
};
|
|
538
|
-
/**
|
|
539
|
-
* Check if a specific repo's pool is active
|
|
540
|
-
*/
|
|
541
|
-
export const isLbugReady = (repoId) => pool.has(repoId);
|
|
542
|
-
/** Regex to detect write operations in user-supplied Cypher queries.
|
|
543
|
-
* Note: CALL is NOT blocked — it's used for read-only FTS (CALL QUERY_FTS_INDEX)
|
|
544
|
-
* and vector search (CALL QUERY_VECTOR_INDEX). The database is opened in
|
|
545
|
-
* read-only mode as defense-in-depth against write procedures. */
|
|
546
|
-
export const CYPHER_WRITE_RE = /(?<!:)\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH|FOREACH|INSTALL|LOAD)\b/i;
|
|
547
|
-
/** Check if a Cypher query contains write operations */
|
|
548
|
-
export function isWriteQuery(query) {
|
|
549
|
-
return CYPHER_WRITE_RE.test(query);
|
|
550
|
-
}
|