@duytransipher/gitnexus 1.4.6-sipher.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +73 -0
- package/README.md +261 -0
- package/dist/cli/ai-context.d.ts +23 -0
- package/dist/cli/ai-context.js +265 -0
- package/dist/cli/analyze.d.ts +12 -0
- package/dist/cli/analyze.js +345 -0
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/clean.d.ts +10 -0
- package/dist/cli/clean.js +60 -0
- package/dist/cli/eval-server.d.ts +37 -0
- package/dist/cli/eval-server.js +389 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +137 -0
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/list.d.ts +6 -0
- package/dist/cli/list.js +30 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +36 -0
- package/dist/cli/serve.d.ts +4 -0
- package/dist/cli/serve.js +6 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +367 -0
- package/dist/cli/sipher-patched.d.ts +2 -0
- package/dist/cli/sipher-patched.js +77 -0
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +549 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.js +36 -0
- package/dist/cli/tool.d.ts +60 -0
- package/dist/cli/tool.js +180 -0
- package/dist/cli/wiki.d.ts +15 -0
- package/dist/cli/wiki.js +365 -0
- package/dist/config/ignore-service.d.ts +26 -0
- package/dist/config/ignore-service.js +284 -0
- package/dist/config/supported-languages.d.ts +15 -0
- package/dist/config/supported-languages.js +16 -0
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +240 -0
- package/dist/core/embeddings/embedder.d.ts +60 -0
- package/dist/core/embeddings/embedder.js +251 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +51 -0
- package/dist/core/embeddings/embedding-pipeline.js +356 -0
- package/dist/core/embeddings/index.d.ts +9 -0
- package/dist/core/embeddings/index.js +9 -0
- package/dist/core/embeddings/text-generator.d.ts +24 -0
- package/dist/core/embeddings/text-generator.js +182 -0
- package/dist/core/embeddings/types.d.ts +87 -0
- package/dist/core/embeddings/types.js +32 -0
- package/dist/core/graph/graph.d.ts +2 -0
- package/dist/core/graph/graph.js +66 -0
- package/dist/core/graph/types.d.ts +66 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/ingestion/ast-cache.d.ts +11 -0
- package/dist/core/ingestion/ast-cache.js +35 -0
- package/dist/core/ingestion/call-processor.d.ts +23 -0
- package/dist/core/ingestion/call-processor.js +793 -0
- package/dist/core/ingestion/call-routing.d.ts +68 -0
- package/dist/core/ingestion/call-routing.js +129 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
- package/dist/core/ingestion/cluster-enricher.js +170 -0
- package/dist/core/ingestion/community-processor.d.ts +39 -0
- package/dist/core/ingestion/community-processor.js +312 -0
- package/dist/core/ingestion/constants.d.ts +16 -0
- package/dist/core/ingestion/constants.js +16 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +40 -0
- package/dist/core/ingestion/entry-point-scoring.js +353 -0
- package/dist/core/ingestion/export-detection.d.ts +18 -0
- package/dist/core/ingestion/export-detection.js +231 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +28 -0
- package/dist/core/ingestion/filesystem-walker.js +81 -0
- package/dist/core/ingestion/framework-detection.d.ts +54 -0
- package/dist/core/ingestion/framework-detection.js +411 -0
- package/dist/core/ingestion/heritage-processor.d.ts +28 -0
- package/dist/core/ingestion/heritage-processor.js +251 -0
- package/dist/core/ingestion/import-processor.d.ts +34 -0
- package/dist/core/ingestion/import-processor.js +398 -0
- package/dist/core/ingestion/language-config.d.ts +46 -0
- package/dist/core/ingestion/language-config.js +167 -0
- package/dist/core/ingestion/mro-processor.d.ts +45 -0
- package/dist/core/ingestion/mro-processor.js +369 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
- package/dist/core/ingestion/named-binding-extraction.js +363 -0
- package/dist/core/ingestion/parsing-processor.d.ts +19 -0
- package/dist/core/ingestion/parsing-processor.js +315 -0
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +401 -0
- package/dist/core/ingestion/process-processor.d.ts +51 -0
- package/dist/core/ingestion/process-processor.js +315 -0
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
- package/dist/core/ingestion/resolvers/csharp.js +109 -0
- package/dist/core/ingestion/resolvers/go.d.ts +19 -0
- package/dist/core/ingestion/resolvers/go.js +42 -0
- package/dist/core/ingestion/resolvers/index.d.ts +18 -0
- package/dist/core/ingestion/resolvers/index.js +13 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
- package/dist/core/ingestion/resolvers/jvm.js +87 -0
- package/dist/core/ingestion/resolvers/php.d.ts +15 -0
- package/dist/core/ingestion/resolvers/php.js +35 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
- package/dist/core/ingestion/resolvers/rust.js +73 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
- package/dist/core/ingestion/resolvers/standard.js +123 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
- package/dist/core/ingestion/resolvers/utils.js +122 -0
- package/dist/core/ingestion/structure-processor.d.ts +2 -0
- package/dist/core/ingestion/structure-processor.js +36 -0
- package/dist/core/ingestion/symbol-table.d.ts +63 -0
- package/dist/core/ingestion/symbol-table.js +85 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +15 -0
- package/dist/core/ingestion/tree-sitter-queries.js +888 -0
- package/dist/core/ingestion/type-env.d.ts +49 -0
- package/dist/core/ingestion/type-env.js +613 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +383 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +467 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
- package/dist/core/ingestion/type-extractors/index.js +31 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +681 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +549 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +455 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +456 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +145 -0
- package/dist/core/ingestion/type-extractors/shared.js +810 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +137 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
- package/dist/core/ingestion/type-extractors/types.js +1 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +494 -0
- package/dist/core/ingestion/utils.d.ts +138 -0
- package/dist/core/ingestion/utils.js +1290 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +122 -0
- package/dist/core/ingestion/workers/parse-worker.js +1126 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +16 -0
- package/dist/core/ingestion/workers/worker-pool.js +128 -0
- package/dist/core/lbug/csv-generator.d.ts +33 -0
- package/dist/core/lbug/csv-generator.js +366 -0
- package/dist/core/lbug/lbug-adapter.d.ts +103 -0
- package/dist/core/lbug/lbug-adapter.js +769 -0
- package/dist/core/lbug/schema.d.ts +53 -0
- package/dist/core/lbug/schema.js +430 -0
- package/dist/core/search/bm25-index.d.ts +23 -0
- package/dist/core/search/bm25-index.js +96 -0
- package/dist/core/search/hybrid-search.d.ts +49 -0
- package/dist/core/search/hybrid-search.js +118 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +5 -0
- package/dist/core/tree-sitter/parser-loader.js +63 -0
- package/dist/core/wiki/generator.d.ts +120 -0
- package/dist/core/wiki/generator.js +939 -0
- package/dist/core/wiki/graph-queries.d.ts +80 -0
- package/dist/core/wiki/graph-queries.js +238 -0
- package/dist/core/wiki/html-viewer.d.ts +10 -0
- package/dist/core/wiki/html-viewer.js +297 -0
- package/dist/core/wiki/llm-client.d.ts +43 -0
- package/dist/core/wiki/llm-client.js +186 -0
- package/dist/core/wiki/prompts.d.ts +53 -0
- package/dist/core/wiki/prompts.js +174 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +3 -0
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +200 -0
- package/dist/mcp/core/embedder.d.ts +27 -0
- package/dist/mcp/core/embedder.js +108 -0
- package/dist/mcp/core/lbug-adapter.d.ts +57 -0
- package/dist/mcp/core/lbug-adapter.js +455 -0
- package/dist/mcp/local/local-backend.d.ts +181 -0
- package/dist/mcp/local/local-backend.js +1722 -0
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +411 -0
- package/dist/mcp/server.d.ts +23 -0
- package/dist/mcp/server.js +296 -0
- package/dist/mcp/staleness.d.ts +15 -0
- package/dist/mcp/staleness.js +29 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +292 -0
- package/dist/server/api.d.ts +10 -0
- package/dist/server/api.js +344 -0
- package/dist/server/mcp-http.d.ts +13 -0
- package/dist/server/mcp-http.js +100 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +35 -0
- package/dist/storage/repo-manager.d.ts +138 -0
- package/dist/storage/repo-manager.js +299 -0
- package/dist/types/pipeline.d.ts +32 -0
- package/dist/types/pipeline.js +18 -0
- package/dist/unreal/bridge.d.ts +4 -0
- package/dist/unreal/bridge.js +113 -0
- package/dist/unreal/config.d.ts +6 -0
- package/dist/unreal/config.js +55 -0
- package/dist/unreal/types.d.ts +105 -0
- package/dist/unreal/types.js +1 -0
- package/hooks/claude/gitnexus-hook.cjs +238 -0
- package/hooks/claude/pre-tool-use.sh +79 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +100 -0
- package/scripts/ensure-cli-executable.cjs +21 -0
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/scripts/setup-unreal-gitnexus.ps1 +191 -0
- package/skills/gitnexus-cli.md +82 -0
- package/skills/gitnexus-debugging.md +89 -0
- package/skills/gitnexus-exploring.md +78 -0
- package/skills/gitnexus-guide.md +64 -0
- package/skills/gitnexus-impact-analysis.md +97 -0
- package/skills/gitnexus-pr-review.md +163 -0
- package/skills/gitnexus-refactoring.md +121 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedder Module (Read-Only)
|
|
3
|
+
*
|
|
4
|
+
* Singleton factory for transformers.js embedding pipeline.
|
|
5
|
+
* For MCP, we only need to compute query embeddings, not batch embed.
|
|
6
|
+
*/
|
|
7
|
+
import { pipeline, env } from '@huggingface/transformers';
|
|
8
|
+
// Model config
|
|
9
|
+
const MODEL_ID = 'Snowflake/snowflake-arctic-embed-xs';
|
|
10
|
+
const EMBEDDING_DIMS = 384;
|
|
11
|
+
// Module-level state for singleton pattern
|
|
12
|
+
let embedderInstance = null;
|
|
13
|
+
let isInitializing = false;
|
|
14
|
+
let initPromise = null;
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the embedding model (lazy, on first search)
|
|
17
|
+
*/
|
|
18
|
+
export const initEmbedder = async () => {
|
|
19
|
+
if (embedderInstance) {
|
|
20
|
+
return embedderInstance;
|
|
21
|
+
}
|
|
22
|
+
if (isInitializing && initPromise) {
|
|
23
|
+
return initPromise;
|
|
24
|
+
}
|
|
25
|
+
isInitializing = true;
|
|
26
|
+
initPromise = (async () => {
|
|
27
|
+
try {
|
|
28
|
+
env.allowLocalModels = false;
|
|
29
|
+
console.error('GitNexus: Loading embedding model (first search may take a moment)...');
|
|
30
|
+
// Try GPU first (DirectML on Windows, CUDA on Linux), fall back to CPU
|
|
31
|
+
const isWindows = process.platform === 'win32';
|
|
32
|
+
const gpuDevice = isWindows ? 'dml' : 'cuda';
|
|
33
|
+
const devicesToTry = [gpuDevice, 'cpu'];
|
|
34
|
+
for (const device of devicesToTry) {
|
|
35
|
+
try {
|
|
36
|
+
// Silence stdout and stderr during model load — ONNX Runtime and transformers.js
|
|
37
|
+
// may write progress/init messages that corrupt MCP stdio protocol or produce
|
|
38
|
+
// noisy warnings (e.g. node assignment to execution providers).
|
|
39
|
+
const origStdout = process.stdout.write;
|
|
40
|
+
const origStderr = process.stderr.write;
|
|
41
|
+
process.stdout.write = (() => true);
|
|
42
|
+
process.stderr.write = (() => true);
|
|
43
|
+
try {
|
|
44
|
+
embedderInstance = await pipeline('feature-extraction', MODEL_ID, {
|
|
45
|
+
device: device,
|
|
46
|
+
dtype: 'fp32',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
process.stdout.write = origStdout;
|
|
51
|
+
process.stderr.write = origStderr;
|
|
52
|
+
}
|
|
53
|
+
console.error(`GitNexus: Embedding model loaded (${device})`);
|
|
54
|
+
return embedderInstance;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
if (device === 'cpu')
|
|
58
|
+
throw new Error('Failed to load embedding model');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw new Error('No suitable device found');
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
isInitializing = false;
|
|
65
|
+
initPromise = null;
|
|
66
|
+
embedderInstance = null;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
isInitializing = false;
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
return initPromise;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Check if embedder is ready
|
|
77
|
+
*/
|
|
78
|
+
export const isEmbedderReady = () => embedderInstance !== null;
|
|
79
|
+
/**
|
|
80
|
+
* Embed a query text for semantic search
|
|
81
|
+
*/
|
|
82
|
+
export const embedQuery = async (query) => {
|
|
83
|
+
const embedder = await initEmbedder();
|
|
84
|
+
const result = await embedder(query, {
|
|
85
|
+
pooling: 'mean',
|
|
86
|
+
normalize: true,
|
|
87
|
+
});
|
|
88
|
+
return Array.from(result.data);
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Get embedding dimensions
|
|
92
|
+
*/
|
|
93
|
+
export const getEmbeddingDims = () => EMBEDDING_DIMS;
|
|
94
|
+
/**
|
|
95
|
+
* Cleanup embedder
|
|
96
|
+
*/
|
|
97
|
+
export const disposeEmbedder = async () => {
|
|
98
|
+
if (embedderInstance) {
|
|
99
|
+
try {
|
|
100
|
+
if ('dispose' in embedderInstance && typeof embedderInstance.dispose === 'function') {
|
|
101
|
+
await embedderInstance.dispose();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
embedderInstance = null;
|
|
106
|
+
initPromise = null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LadybugDB Adapter (Connection Pool)
|
|
3
|
+
*
|
|
4
|
+
* Manages a pool of LadybugDB databases keyed by repoId, each with
|
|
5
|
+
* multiple Connection objects for safe concurrent query execution.
|
|
6
|
+
*
|
|
7
|
+
* LadybugDB Connections are NOT thread-safe — a single Connection
|
|
8
|
+
* segfaults if concurrent .query() calls hit it simultaneously.
|
|
9
|
+
* This adapter provides a checkout/return connection pool so each
|
|
10
|
+
* concurrent query gets its own Connection from the same Database.
|
|
11
|
+
*
|
|
12
|
+
* @see https://docs.ladybugdb.com/concurrency — multiple Connections
|
|
13
|
+
* from the same Database is the officially supported concurrency pattern.
|
|
14
|
+
*/
|
|
15
|
+
import lbug from '@ladybugdb/core';
|
|
16
|
+
/** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
|
|
17
|
+
export declare const realStdoutWrite: any;
|
|
18
|
+
/**
|
|
19
|
+
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
|
20
|
+
* Retries on lock errors (e.g., when `gitnexus analyze` is running).
|
|
21
|
+
*
|
|
22
|
+
* Concurrent calls for the same repoId are deduplicated — the second caller
|
|
23
|
+
* awaits the first's in-progress init rather than starting a redundant one.
|
|
24
|
+
*/
|
|
25
|
+
export declare const initLbug: (repoId: string, dbPath: string) => Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Initialize a pool entry from a pre-existing Database object.
|
|
28
|
+
*
|
|
29
|
+
* Used in tests to avoid the writable→close→read-only cycle that crashes
|
|
30
|
+
* on macOS due to N-API destructor segfaults. The pool adapter reuses
|
|
31
|
+
* the core adapter's writable Database instead of opening a new read-only one.
|
|
32
|
+
*
|
|
33
|
+
* The Database is registered in the shared dbCache so closeOne() decrements
|
|
34
|
+
* the refCount correctly. If the Database is already cached (e.g. another
|
|
35
|
+
* repoId already injected it), the existing entry is reused.
|
|
36
|
+
*/
|
|
37
|
+
export declare function initLbugWithDb(repoId: string, existingDb: lbug.Database, dbPath: string): Promise<void>;
|
|
38
|
+
export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Execute a parameterized query on a specific repo's connection pool.
|
|
41
|
+
* Uses prepare/execute pattern to prevent Cypher injection.
|
|
42
|
+
*/
|
|
43
|
+
export declare const executeParameterized: (repoId: string, cypher: string, params: Record<string, any>) => Promise<any[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Close one or all repo pools.
|
|
46
|
+
* If repoId is provided, close only that repo's connections.
|
|
47
|
+
* If omitted, close all repos.
|
|
48
|
+
*/
|
|
49
|
+
export declare const closeLbug: (repoId?: string) => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Check if a specific repo's pool is active
|
|
52
|
+
*/
|
|
53
|
+
export declare const isLbugReady: (repoId: string) => boolean;
|
|
54
|
+
/** Regex to detect write operations in user-supplied Cypher queries */
|
|
55
|
+
export declare const CYPHER_WRITE_RE: RegExp;
|
|
56
|
+
/** Check if a Cypher query contains write operations */
|
|
57
|
+
export declare function isWriteQuery(query: string): boolean;
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LadybugDB Adapter (Connection Pool)
|
|
3
|
+
*
|
|
4
|
+
* Manages a pool of LadybugDB databases keyed by repoId, each with
|
|
5
|
+
* multiple Connection objects for safe concurrent query execution.
|
|
6
|
+
*
|
|
7
|
+
* LadybugDB Connections are NOT thread-safe — a single Connection
|
|
8
|
+
* segfaults if concurrent .query() calls hit it simultaneously.
|
|
9
|
+
* This adapter provides a checkout/return connection pool so each
|
|
10
|
+
* concurrent query gets its own Connection from the same Database.
|
|
11
|
+
*
|
|
12
|
+
* @see https://docs.ladybugdb.com/concurrency — multiple Connections
|
|
13
|
+
* from the same Database is the officially supported concurrency pattern.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'fs/promises';
|
|
16
|
+
import lbug from '@ladybugdb/core';
|
|
17
|
+
const pool = new Map();
|
|
18
|
+
const dbCache = new Map();
|
|
19
|
+
/** Max repos in the pool (LRU eviction) */
|
|
20
|
+
const MAX_POOL_SIZE = 5;
|
|
21
|
+
/** Idle timeout before closing a repo's connections */
|
|
22
|
+
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
23
|
+
/** Max connections per repo (caps concurrent queries per repo) */
|
|
24
|
+
const MAX_CONNS_PER_REPO = 8;
|
|
25
|
+
let idleTimer = null;
|
|
26
|
+
/** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
|
|
27
|
+
export const realStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
28
|
+
let stdoutSilenceCount = 0;
|
|
29
|
+
/** True while pre-warming connections — prevents watchdog from prematurely restoring stdout */
|
|
30
|
+
let preWarmActive = false;
|
|
31
|
+
/**
|
|
32
|
+
* Start the idle cleanup timer (runs every 60s)
|
|
33
|
+
*/
|
|
34
|
+
function ensureIdleTimer() {
|
|
35
|
+
if (idleTimer)
|
|
36
|
+
return;
|
|
37
|
+
idleTimer = setInterval(() => {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
for (const [repoId, entry] of pool) {
|
|
40
|
+
if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
|
|
41
|
+
closeOne(repoId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, 60_000);
|
|
45
|
+
if (idleTimer && typeof idleTimer === 'object' && 'unref' in idleTimer) {
|
|
46
|
+
idleTimer.unref();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Evict the least-recently-used repo if pool is at capacity
|
|
51
|
+
*/
|
|
52
|
+
function evictLRU() {
|
|
53
|
+
if (pool.size < MAX_POOL_SIZE)
|
|
54
|
+
return;
|
|
55
|
+
let oldestId = null;
|
|
56
|
+
let oldestTime = Infinity;
|
|
57
|
+
for (const [id, entry] of pool) {
|
|
58
|
+
if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
|
|
59
|
+
oldestTime = entry.lastUsed;
|
|
60
|
+
oldestId = id;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (oldestId) {
|
|
64
|
+
closeOne(oldestId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Remove a repo from the pool, close its connections, and release its
|
|
69
|
+
* shared Database ref. Only closes the Database when no other repoIds
|
|
70
|
+
* reference it (refCount === 0).
|
|
71
|
+
*/
|
|
72
|
+
function closeOne(repoId) {
|
|
73
|
+
const entry = pool.get(repoId);
|
|
74
|
+
if (!entry)
|
|
75
|
+
return;
|
|
76
|
+
entry.closed = true;
|
|
77
|
+
// Close available connections — fire-and-forget with .catch() to prevent
|
|
78
|
+
// unhandled rejections. Native close() returns Promise<void> but can crash
|
|
79
|
+
// the N-API destructor on macOS/Windows; deferring to process exit lets
|
|
80
|
+
// dangerouslyIgnoreUnhandledErrors absorb the crash.
|
|
81
|
+
for (const conn of entry.available) {
|
|
82
|
+
conn.close().catch(() => { });
|
|
83
|
+
}
|
|
84
|
+
entry.available.length = 0;
|
|
85
|
+
// Checked-out connections can't be closed here — they're in-flight.
|
|
86
|
+
// The checkin() function detects entry.closed and closes them on return.
|
|
87
|
+
// Only close the Database when no other repoIds reference it.
|
|
88
|
+
// External databases (injected via initLbugWithDb) are never closed here —
|
|
89
|
+
// the core adapter owns them and handles their lifecycle.
|
|
90
|
+
const shared = dbCache.get(entry.dbPath);
|
|
91
|
+
if (shared) {
|
|
92
|
+
shared.refCount--;
|
|
93
|
+
if (shared.refCount === 0) {
|
|
94
|
+
if (shared.external) {
|
|
95
|
+
// External databases are owned by the core adapter — don't close
|
|
96
|
+
// or remove from cache. Keep the entry so future initLbug() calls
|
|
97
|
+
// for the same dbPath reuse it instead of hitting a file lock.
|
|
98
|
+
shared.refCount = 0;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
shared.db.close().catch(() => { });
|
|
102
|
+
dbCache.delete(entry.dbPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
pool.delete(repoId);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Create a new Connection from a repo's Database.
|
|
110
|
+
* Silences stdout to prevent native module output from corrupting MCP stdio.
|
|
111
|
+
*/
|
|
112
|
+
function silenceStdout() {
|
|
113
|
+
if (stdoutSilenceCount++ === 0) {
|
|
114
|
+
process.stdout.write = (() => true);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function restoreStdout() {
|
|
118
|
+
if (--stdoutSilenceCount <= 0) {
|
|
119
|
+
stdoutSilenceCount = 0;
|
|
120
|
+
process.stdout.write = realStdoutWrite;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
|
|
124
|
+
// inside createConnection before restoreStdout runs).
|
|
125
|
+
setInterval(() => {
|
|
126
|
+
if (stdoutSilenceCount > 0 && !preWarmActive) {
|
|
127
|
+
stdoutSilenceCount = 0;
|
|
128
|
+
process.stdout.write = realStdoutWrite;
|
|
129
|
+
}
|
|
130
|
+
}, 1000).unref();
|
|
131
|
+
function createConnection(db) {
|
|
132
|
+
silenceStdout();
|
|
133
|
+
try {
|
|
134
|
+
return new lbug.Connection(db);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
restoreStdout();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Query timeout in milliseconds */
|
|
141
|
+
const QUERY_TIMEOUT_MS = 30_000;
|
|
142
|
+
/** Waiter queue timeout in milliseconds */
|
|
143
|
+
const WAITER_TIMEOUT_MS = 15_000;
|
|
144
|
+
const LOCK_RETRY_ATTEMPTS = 3;
|
|
145
|
+
const LOCK_RETRY_DELAY_MS = 2000;
|
|
146
|
+
/** Deduplicates concurrent initLbug calls for the same repoId */
|
|
147
|
+
const initPromises = new Map();
|
|
148
|
+
/**
|
|
149
|
+
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
|
150
|
+
* Retries on lock errors (e.g., when `gitnexus analyze` is running).
|
|
151
|
+
*
|
|
152
|
+
* Concurrent calls for the same repoId are deduplicated — the second caller
|
|
153
|
+
* awaits the first's in-progress init rather than starting a redundant one.
|
|
154
|
+
*/
|
|
155
|
+
export const initLbug = async (repoId, dbPath) => {
|
|
156
|
+
const existing = pool.get(repoId);
|
|
157
|
+
if (existing) {
|
|
158
|
+
existing.lastUsed = Date.now();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Deduplicate concurrent init calls for the same repoId —
|
|
162
|
+
// prevents double-init race when multiple parallel tool calls
|
|
163
|
+
// trigger initialization for the same repo simultaneously.
|
|
164
|
+
const pending = initPromises.get(repoId);
|
|
165
|
+
if (pending)
|
|
166
|
+
return pending;
|
|
167
|
+
const promise = doInitLbug(repoId, dbPath);
|
|
168
|
+
initPromises.set(repoId, promise);
|
|
169
|
+
try {
|
|
170
|
+
await promise;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
initPromises.delete(repoId);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Internal init — creates DB, pre-warms connections, loads FTS, then registers pool.
|
|
178
|
+
* Pool entry is registered LAST so concurrent executeQuery calls see either
|
|
179
|
+
* "not initialized" (and throw) or a fully ready pool — never a half-built one.
|
|
180
|
+
*/
|
|
181
|
+
async function doInitLbug(repoId, dbPath) {
|
|
182
|
+
// Check if database exists
|
|
183
|
+
try {
|
|
184
|
+
await fs.stat(dbPath);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
throw new Error(`LadybugDB not found at ${dbPath}. Run: gitnexus analyze`);
|
|
188
|
+
}
|
|
189
|
+
evictLRU();
|
|
190
|
+
// Reuse an existing native Database if another repoId already opened this path.
|
|
191
|
+
// This prevents buffer manager exhaustion from multiple mmap regions on the same file.
|
|
192
|
+
let shared = dbCache.get(dbPath);
|
|
193
|
+
if (!shared) {
|
|
194
|
+
// Open in read-only mode — MCP server never writes to the database.
|
|
195
|
+
// This allows multiple MCP server instances to read concurrently, and
|
|
196
|
+
// avoids lock conflicts when `gitnexus analyze` is writing.
|
|
197
|
+
let lastError = null;
|
|
198
|
+
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
|
199
|
+
silenceStdout();
|
|
200
|
+
try {
|
|
201
|
+
const db = new lbug.Database(dbPath, 0, // bufferManagerSize (default)
|
|
202
|
+
false, // enableCompression (default)
|
|
203
|
+
true);
|
|
204
|
+
restoreStdout();
|
|
205
|
+
shared = { db, refCount: 0, ftsLoaded: false };
|
|
206
|
+
dbCache.set(dbPath, shared);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
restoreStdout();
|
|
211
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
212
|
+
const isLockError = lastError.message.includes('Could not set lock')
|
|
213
|
+
|| lastError.message.includes('lock');
|
|
214
|
+
if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS)
|
|
215
|
+
break;
|
|
216
|
+
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!shared) {
|
|
220
|
+
throw new Error(`LadybugDB unavailable for ${repoId}. Another process may be rebuilding the index. ` +
|
|
221
|
+
`Retry later. (${lastError?.message || 'unknown error'})`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
shared.refCount++;
|
|
225
|
+
const db = shared.db;
|
|
226
|
+
// Pre-create the full pool upfront so createConnection() (which silences
|
|
227
|
+
// stdout) is never called lazily during active query execution.
|
|
228
|
+
// Mark preWarmActive so the watchdog timer doesn't interfere.
|
|
229
|
+
preWarmActive = true;
|
|
230
|
+
const available = [];
|
|
231
|
+
try {
|
|
232
|
+
for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
|
|
233
|
+
available.push(createConnection(db));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
preWarmActive = false;
|
|
238
|
+
}
|
|
239
|
+
// Load FTS extension once per shared Database.
|
|
240
|
+
// Done BEFORE pool registration so no concurrent checkout can grab
|
|
241
|
+
// the connection while the async FTS load is in progress.
|
|
242
|
+
if (!shared.ftsLoaded) {
|
|
243
|
+
try {
|
|
244
|
+
await available[0].query('LOAD EXTENSION fts');
|
|
245
|
+
shared.ftsLoaded = true;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Extension may not be installed — FTS queries will fail gracefully
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Register pool entry only after all connections are pre-warmed and FTS is
|
|
252
|
+
// loaded. Concurrent executeQuery calls see either "not initialized"
|
|
253
|
+
// (and throw cleanly) or a fully ready pool — never a half-built one.
|
|
254
|
+
pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath, closed: false });
|
|
255
|
+
ensureIdleTimer();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Initialize a pool entry from a pre-existing Database object.
|
|
259
|
+
*
|
|
260
|
+
* Used in tests to avoid the writable→close→read-only cycle that crashes
|
|
261
|
+
* on macOS due to N-API destructor segfaults. The pool adapter reuses
|
|
262
|
+
* the core adapter's writable Database instead of opening a new read-only one.
|
|
263
|
+
*
|
|
264
|
+
* The Database is registered in the shared dbCache so closeOne() decrements
|
|
265
|
+
* the refCount correctly. If the Database is already cached (e.g. another
|
|
266
|
+
* repoId already injected it), the existing entry is reused.
|
|
267
|
+
*/
|
|
268
|
+
export async function initLbugWithDb(repoId, existingDb, dbPath) {
|
|
269
|
+
const existing = pool.get(repoId);
|
|
270
|
+
if (existing) {
|
|
271
|
+
existing.lastUsed = Date.now();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Register in dbCache with external: true so other initLbug() calls
|
|
275
|
+
// for the same dbPath reuse this Database instead of trying to open
|
|
276
|
+
// a new one (which would fail with a file lock error).
|
|
277
|
+
// closeOne() respects the external flag and skips db.close().
|
|
278
|
+
let shared = dbCache.get(dbPath);
|
|
279
|
+
if (!shared) {
|
|
280
|
+
shared = { db: existingDb, refCount: 0, ftsLoaded: false, external: true };
|
|
281
|
+
dbCache.set(dbPath, shared);
|
|
282
|
+
}
|
|
283
|
+
shared.refCount++;
|
|
284
|
+
const available = [];
|
|
285
|
+
preWarmActive = true;
|
|
286
|
+
try {
|
|
287
|
+
for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
|
|
288
|
+
available.push(createConnection(existingDb));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
preWarmActive = false;
|
|
293
|
+
}
|
|
294
|
+
// Load FTS extension if not already loaded on this Database
|
|
295
|
+
try {
|
|
296
|
+
await available[0].query('LOAD EXTENSION fts');
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Extension may already be loaded or not installed
|
|
300
|
+
}
|
|
301
|
+
pool.set(repoId, {
|
|
302
|
+
db: existingDb,
|
|
303
|
+
available,
|
|
304
|
+
checkedOut: 0,
|
|
305
|
+
waiters: [],
|
|
306
|
+
lastUsed: Date.now(),
|
|
307
|
+
dbPath,
|
|
308
|
+
closed: false
|
|
309
|
+
});
|
|
310
|
+
ensureIdleTimer();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Checkout a connection from the pool.
|
|
314
|
+
* Returns an available connection, or creates a new one if under the cap.
|
|
315
|
+
* If all connections are busy and at cap, queues the caller until one is returned.
|
|
316
|
+
*/
|
|
317
|
+
function checkout(entry) {
|
|
318
|
+
// Fast path: grab an available connection
|
|
319
|
+
if (entry.available.length > 0) {
|
|
320
|
+
entry.checkedOut++;
|
|
321
|
+
return Promise.resolve(entry.available.pop());
|
|
322
|
+
}
|
|
323
|
+
// Pool was pre-warmed to MAX_CONNS_PER_REPO during init. If we're here
|
|
324
|
+
// with fewer total connections, something leaked — surface the bug rather
|
|
325
|
+
// than silently creating a connection (which would silence stdout mid-query).
|
|
326
|
+
const totalConns = entry.available.length + entry.checkedOut;
|
|
327
|
+
if (totalConns < MAX_CONNS_PER_REPO) {
|
|
328
|
+
throw new Error(`Connection pool integrity error: expected ${MAX_CONNS_PER_REPO} ` +
|
|
329
|
+
`connections but found ${totalConns} (${entry.available.length} available, ` +
|
|
330
|
+
`${entry.checkedOut} checked out)`);
|
|
331
|
+
}
|
|
332
|
+
// At capacity — queue the caller with a timeout.
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
const waiter = (conn) => {
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
resolve(conn);
|
|
337
|
+
};
|
|
338
|
+
const timer = setTimeout(() => {
|
|
339
|
+
const idx = entry.waiters.indexOf(waiter);
|
|
340
|
+
if (idx !== -1)
|
|
341
|
+
entry.waiters.splice(idx, 1);
|
|
342
|
+
reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
|
|
343
|
+
}, WAITER_TIMEOUT_MS);
|
|
344
|
+
entry.waiters.push(waiter);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Return a connection to the pool after use.
|
|
349
|
+
* If the pool entry was closed while the connection was checked out (e.g.
|
|
350
|
+
* LRU eviction), close the orphaned connection instead of returning it.
|
|
351
|
+
* If there are queued waiters, hand the connection directly to the next one
|
|
352
|
+
* instead of putting it back in the available array (avoids race conditions).
|
|
353
|
+
*/
|
|
354
|
+
function checkin(entry, conn) {
|
|
355
|
+
if (entry.closed) {
|
|
356
|
+
// Pool entry was deleted during checkout — close the orphaned connection
|
|
357
|
+
conn.close().catch(() => { });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (entry.waiters.length > 0) {
|
|
361
|
+
// Hand directly to the next waiter — no intermediate available state
|
|
362
|
+
const waiter = entry.waiters.shift();
|
|
363
|
+
waiter(conn);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
entry.checkedOut--;
|
|
367
|
+
entry.available.push(conn);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Execute a query on a specific repo's connection pool.
|
|
372
|
+
* Automatically checks out a connection, runs the query, and returns it.
|
|
373
|
+
*/
|
|
374
|
+
/** Race a promise against a timeout */
|
|
375
|
+
function withTimeout(promise, ms, label) {
|
|
376
|
+
let timer;
|
|
377
|
+
const timeout = new Promise((_, reject) => {
|
|
378
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
379
|
+
});
|
|
380
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
381
|
+
}
|
|
382
|
+
export const executeQuery = async (repoId, cypher) => {
|
|
383
|
+
const entry = pool.get(repoId);
|
|
384
|
+
if (!entry) {
|
|
385
|
+
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
386
|
+
}
|
|
387
|
+
if (isWriteQuery(cypher)) {
|
|
388
|
+
throw new Error('Write operations are not allowed. The pool adapter is read-only.');
|
|
389
|
+
}
|
|
390
|
+
entry.lastUsed = Date.now();
|
|
391
|
+
const conn = await checkout(entry);
|
|
392
|
+
try {
|
|
393
|
+
const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
|
|
394
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
395
|
+
const rows = await result.getAll();
|
|
396
|
+
return rows;
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
checkin(entry, conn);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
/**
|
|
403
|
+
* Execute a parameterized query on a specific repo's connection pool.
|
|
404
|
+
* Uses prepare/execute pattern to prevent Cypher injection.
|
|
405
|
+
*/
|
|
406
|
+
export const executeParameterized = async (repoId, cypher, params) => {
|
|
407
|
+
const entry = pool.get(repoId);
|
|
408
|
+
if (!entry) {
|
|
409
|
+
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
|
410
|
+
}
|
|
411
|
+
entry.lastUsed = Date.now();
|
|
412
|
+
const conn = await checkout(entry);
|
|
413
|
+
try {
|
|
414
|
+
const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
|
|
415
|
+
if (!stmt.isSuccess()) {
|
|
416
|
+
const errMsg = await stmt.getErrorMessage();
|
|
417
|
+
throw new Error(`Prepare failed: ${errMsg}`);
|
|
418
|
+
}
|
|
419
|
+
const queryResult = await withTimeout(conn.execute(stmt, params), QUERY_TIMEOUT_MS, 'Execute');
|
|
420
|
+
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
421
|
+
const rows = await result.getAll();
|
|
422
|
+
return rows;
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
checkin(entry, conn);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Close one or all repo pools.
|
|
430
|
+
* If repoId is provided, close only that repo's connections.
|
|
431
|
+
* If omitted, close all repos.
|
|
432
|
+
*/
|
|
433
|
+
export const closeLbug = async (repoId) => {
|
|
434
|
+
if (repoId) {
|
|
435
|
+
closeOne(repoId);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
for (const id of [...pool.keys()]) {
|
|
439
|
+
closeOne(id);
|
|
440
|
+
}
|
|
441
|
+
if (idleTimer) {
|
|
442
|
+
clearInterval(idleTimer);
|
|
443
|
+
idleTimer = null;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Check if a specific repo's pool is active
|
|
448
|
+
*/
|
|
449
|
+
export const isLbugReady = (repoId) => pool.has(repoId);
|
|
450
|
+
/** Regex to detect write operations in user-supplied Cypher queries */
|
|
451
|
+
export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
|
|
452
|
+
/** Check if a Cypher query contains write operations */
|
|
453
|
+
export function isWriteQuery(query) {
|
|
454
|
+
return CYPHER_WRITE_RE.test(query);
|
|
455
|
+
}
|