gitnexus 1.4.9 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/cli/ai-context.d.ts +4 -1
- package/dist/cli/ai-context.js +19 -11
- package/dist/cli/analyze.d.ts +6 -0
- package/dist/cli/analyze.js +105 -251
- package/dist/cli/eval-server.js +20 -11
- package/dist/cli/index-repo.js +20 -22
- package/dist/cli/index.js +8 -7
- package/dist/cli/mcp.js +1 -1
- package/dist/cli/serve.js +29 -1
- package/dist/cli/setup.js +9 -9
- package/dist/cli/skill-gen.js +15 -9
- package/dist/cli/wiki.d.ts +2 -0
- package/dist/cli/wiki.js +141 -26
- package/dist/config/ignore-service.js +102 -22
- package/dist/config/supported-languages.d.ts +8 -42
- package/dist/config/supported-languages.js +8 -43
- package/dist/core/augmentation/engine.js +19 -7
- package/dist/core/embeddings/embedder.js +19 -15
- package/dist/core/embeddings/embedding-pipeline.js +6 -6
- package/dist/core/embeddings/http-client.js +3 -3
- package/dist/core/embeddings/text-generator.js +9 -24
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/embeddings/types.js +1 -7
- package/dist/core/graph/graph.js +6 -2
- package/dist/core/graph/types.d.ts +9 -59
- package/dist/core/ingestion/ast-cache.js +3 -3
- package/dist/core/ingestion/call-processor.d.ts +20 -2
- package/dist/core/ingestion/call-processor.js +347 -144
- package/dist/core/ingestion/call-routing.js +10 -4
- package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
- package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
- package/dist/core/ingestion/call-sites/java.d.ts +9 -0
- package/dist/core/ingestion/call-sites/java.js +30 -0
- package/dist/core/ingestion/cluster-enricher.js +6 -8
- package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
- package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
- package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
- package/dist/core/ingestion/cobol-processor.js +102 -56
- package/dist/core/ingestion/community-processor.js +21 -15
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
- package/dist/core/ingestion/entry-point-scoring.js +5 -6
- package/dist/core/ingestion/export-detection.js +32 -9
- package/dist/core/ingestion/field-extractor.d.ts +1 -1
- package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
- package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
- package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
- package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
- package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
- package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
- package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
- package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
- package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
- package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
- package/dist/core/ingestion/field-extractors/generic.js +6 -0
- package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
- package/dist/core/ingestion/field-extractors/typescript.js +1 -1
- package/dist/core/ingestion/field-types.d.ts +4 -2
- package/dist/core/ingestion/filesystem-walker.js +3 -3
- package/dist/core/ingestion/framework-detection.d.ts +1 -1
- package/dist/core/ingestion/framework-detection.js +355 -85
- package/dist/core/ingestion/heritage-processor.d.ts +24 -0
- package/dist/core/ingestion/heritage-processor.js +99 -8
- package/dist/core/ingestion/import-processor.js +44 -15
- package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
- package/dist/core/ingestion/import-resolvers/dart.js +1 -1
- package/dist/core/ingestion/import-resolvers/go.js +4 -2
- package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
- package/dist/core/ingestion/import-resolvers/php.js +4 -4
- package/dist/core/ingestion/import-resolvers/python.js +1 -1
- package/dist/core/ingestion/import-resolvers/rust.js +9 -3
- package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/standard.js +6 -5
- package/dist/core/ingestion/import-resolvers/swift.js +2 -1
- package/dist/core/ingestion/import-resolvers/utils.js +26 -7
- package/dist/core/ingestion/language-config.js +5 -4
- package/dist/core/ingestion/language-provider.d.ts +7 -2
- package/dist/core/ingestion/languages/c-cpp.js +106 -21
- package/dist/core/ingestion/languages/cobol.js +1 -1
- package/dist/core/ingestion/languages/csharp.js +96 -19
- package/dist/core/ingestion/languages/dart.js +23 -7
- package/dist/core/ingestion/languages/go.js +1 -1
- package/dist/core/ingestion/languages/index.d.ts +1 -1
- package/dist/core/ingestion/languages/index.js +2 -3
- package/dist/core/ingestion/languages/java.js +4 -1
- package/dist/core/ingestion/languages/kotlin.js +60 -13
- package/dist/core/ingestion/languages/php.js +102 -25
- package/dist/core/ingestion/languages/python.js +28 -5
- package/dist/core/ingestion/languages/ruby.js +56 -14
- package/dist/core/ingestion/languages/rust.js +55 -11
- package/dist/core/ingestion/languages/swift.js +112 -27
- package/dist/core/ingestion/languages/typescript.js +95 -19
- package/dist/core/ingestion/markdown-processor.js +5 -5
- package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
- package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
- package/dist/core/ingestion/method-extractors/generic.js +137 -0
- package/dist/core/ingestion/method-types.d.ts +61 -0
- package/dist/core/ingestion/method-types.js +2 -0
- package/dist/core/ingestion/mro-processor.d.ts +1 -1
- package/dist/core/ingestion/mro-processor.js +12 -8
- package/dist/core/ingestion/named-binding-processor.js +2 -2
- package/dist/core/ingestion/named-bindings/rust.js +3 -1
- package/dist/core/ingestion/parsing-processor.js +74 -24
- package/dist/core/ingestion/pipeline.d.ts +2 -1
- package/dist/core/ingestion/pipeline.js +208 -102
- package/dist/core/ingestion/process-processor.js +12 -10
- package/dist/core/ingestion/resolution-context.js +3 -3
- package/dist/core/ingestion/route-extractors/middleware.js +31 -7
- package/dist/core/ingestion/route-extractors/php.js +2 -1
- package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
- package/dist/core/ingestion/structure-processor.d.ts +1 -1
- package/dist/core/ingestion/structure-processor.js +4 -4
- package/dist/core/ingestion/symbol-table.d.ts +1 -1
- package/dist/core/ingestion/symbol-table.js +22 -6
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +1 -1
- package/dist/core/ingestion/type-env.d.ts +2 -2
- package/dist/core/ingestion/type-env.js +75 -50
- package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
- package/dist/core/ingestion/type-extractors/csharp.js +24 -14
- package/dist/core/ingestion/type-extractors/dart.js +6 -8
- package/dist/core/ingestion/type-extractors/go.js +7 -6
- package/dist/core/ingestion/type-extractors/jvm.js +10 -21
- package/dist/core/ingestion/type-extractors/php.js +26 -13
- package/dist/core/ingestion/type-extractors/python.js +11 -15
- package/dist/core/ingestion/type-extractors/ruby.js +8 -3
- package/dist/core/ingestion/type-extractors/rust.js +6 -8
- package/dist/core/ingestion/type-extractors/shared.js +134 -50
- package/dist/core/ingestion/type-extractors/swift.js +16 -13
- package/dist/core/ingestion/type-extractors/typescript.js +23 -15
- package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
- package/dist/core/ingestion/utils/ast-helpers.js +72 -35
- package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
- package/dist/core/ingestion/utils/call-analysis.js +96 -49
- package/dist/core/ingestion/utils/event-loop.js +1 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
- package/dist/core/ingestion/workers/parse-worker.js +364 -84
- package/dist/core/ingestion/workers/worker-pool.js +5 -10
- package/dist/core/lbug/csv-generator.js +54 -15
- package/dist/core/lbug/lbug-adapter.d.ts +5 -0
- package/dist/core/lbug/lbug-adapter.js +86 -23
- package/dist/core/lbug/schema.d.ts +3 -6
- package/dist/core/lbug/schema.js +6 -30
- package/dist/core/run-analyze.d.ts +49 -0
- package/dist/core/run-analyze.js +257 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -1
- package/dist/core/wiki/cursor-client.js +2 -7
- package/dist/core/wiki/generator.js +38 -23
- package/dist/core/wiki/graph-queries.js +10 -10
- package/dist/core/wiki/html-viewer.js +7 -3
- package/dist/core/wiki/llm-client.d.ts +23 -2
- package/dist/core/wiki/llm-client.js +96 -26
- package/dist/core/wiki/prompts.js +7 -6
- package/dist/mcp/core/embedder.js +1 -1
- package/dist/mcp/core/lbug-adapter.d.ts +4 -1
- package/dist/mcp/core/lbug-adapter.js +17 -7
- package/dist/mcp/local/local-backend.js +247 -95
- package/dist/mcp/resources.js +14 -6
- package/dist/mcp/server.js +13 -5
- package/dist/mcp/staleness.js +5 -1
- package/dist/mcp/tools.js +100 -23
- package/dist/server/analyze-job.d.ts +53 -0
- package/dist/server/analyze-job.js +146 -0
- package/dist/server/analyze-worker.d.ts +13 -0
- package/dist/server/analyze-worker.js +59 -0
- package/dist/server/api.js +795 -44
- package/dist/server/git-clone.d.ts +25 -0
- package/dist/server/git-clone.js +91 -0
- package/dist/storage/git.js +1 -3
- package/dist/storage/repo-manager.d.ts +5 -2
- package/dist/storage/repo-manager.js +4 -4
- package/dist/types/pipeline.d.ts +1 -21
- package/dist/types/pipeline.js +1 -18
- package/hooks/claude/gitnexus-hook.cjs +52 -22
- package/package.json +13 -13
- package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
- package/dist/core/ingestion/utils/language-detection.js +0 -70
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Analysis Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Extracts the core analysis pipeline from the CLI analyze command into a
|
|
5
|
+
* reusable function that can be called from both the CLI and a server-side
|
|
6
|
+
* worker process.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: This module must NEVER call process.exit(). The caller (CLI
|
|
9
|
+
* wrapper or server worker) is responsible for process lifecycle.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
14
|
+
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings, } from './lbug/lbug-adapter.js';
|
|
15
|
+
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, cleanupOldKuzuFiles, } from '../storage/repo-manager.js';
|
|
16
|
+
import { getCurrentCommit, hasGitDir } from '../storage/git.js';
|
|
17
|
+
import { generateAIContextFiles } from '../cli/ai-context.js';
|
|
18
|
+
/** Threshold: auto-skip embeddings for repos with more nodes than this */
|
|
19
|
+
const EMBEDDING_NODE_LIMIT = 50_000;
|
|
20
|
+
export const PHASE_LABELS = {
|
|
21
|
+
extracting: 'Scanning files',
|
|
22
|
+
structure: 'Building structure',
|
|
23
|
+
parsing: 'Parsing code',
|
|
24
|
+
imports: 'Resolving imports',
|
|
25
|
+
calls: 'Tracing calls',
|
|
26
|
+
heritage: 'Extracting inheritance',
|
|
27
|
+
communities: 'Detecting communities',
|
|
28
|
+
processes: 'Detecting processes',
|
|
29
|
+
complete: 'Pipeline complete',
|
|
30
|
+
lbug: 'Loading into LadybugDB',
|
|
31
|
+
fts: 'Creating search indexes',
|
|
32
|
+
embeddings: 'Generating embeddings',
|
|
33
|
+
done: 'Done',
|
|
34
|
+
};
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Main orchestrator
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/**
|
|
39
|
+
* Run the full GitNexus analysis pipeline.
|
|
40
|
+
*
|
|
41
|
+
* This is the shared core extracted from the CLI `analyze` command. It
|
|
42
|
+
* handles: pipeline execution, LadybugDB loading, FTS indexing, embedding
|
|
43
|
+
* generation, metadata persistence, and AI context file generation.
|
|
44
|
+
*
|
|
45
|
+
* The function communicates progress and log messages exclusively through
|
|
46
|
+
* the {@link AnalyzeCallbacks} interface — it never writes to stdout/stderr
|
|
47
|
+
* directly and never calls `process.exit()`.
|
|
48
|
+
*/
|
|
49
|
+
export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
50
|
+
const log = (msg) => callbacks.onLog?.(msg);
|
|
51
|
+
const progress = (phase, percent, message) => callbacks.onProgress(phase, percent, message);
|
|
52
|
+
const { storagePath, lbugPath } = getStoragePaths(repoPath);
|
|
53
|
+
// Clean up stale KuzuDB files from before the LadybugDB migration.
|
|
54
|
+
const kuzuResult = await cleanupOldKuzuFiles(storagePath);
|
|
55
|
+
if (kuzuResult.found && kuzuResult.needsReindex) {
|
|
56
|
+
log('Migrating from KuzuDB to LadybugDB — rebuilding index...');
|
|
57
|
+
}
|
|
58
|
+
const repoHasGit = hasGitDir(repoPath);
|
|
59
|
+
const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
|
|
60
|
+
const existingMeta = await loadMeta(storagePath);
|
|
61
|
+
// ── Early-return: already up to date ──────────────────────────────
|
|
62
|
+
if (existingMeta && !options.force && existingMeta.lastCommit === currentCommit) {
|
|
63
|
+
// Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
|
|
64
|
+
if (currentCommit !== '') {
|
|
65
|
+
return {
|
|
66
|
+
repoName: path.basename(repoPath),
|
|
67
|
+
repoPath,
|
|
68
|
+
stats: existingMeta.stats ?? {},
|
|
69
|
+
alreadyUpToDate: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── Cache embeddings from existing index before rebuild ────────────
|
|
74
|
+
let cachedEmbeddingNodeIds = new Set();
|
|
75
|
+
let cachedEmbeddings = [];
|
|
76
|
+
if (options.embeddings && existingMeta && !options.force) {
|
|
77
|
+
try {
|
|
78
|
+
progress('embeddings', 0, 'Caching embeddings...');
|
|
79
|
+
await initLbug(lbugPath);
|
|
80
|
+
const cached = await loadCachedEmbeddings();
|
|
81
|
+
cachedEmbeddingNodeIds = cached.embeddingNodeIds;
|
|
82
|
+
cachedEmbeddings = cached.embeddings;
|
|
83
|
+
await closeLbug();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
try {
|
|
87
|
+
await closeLbug();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* swallow */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ── Phase 1: Full Pipeline (0–60%) ────────────────────────────────
|
|
95
|
+
const pipelineResult = await runPipelineFromRepo(repoPath, (p) => {
|
|
96
|
+
const phaseLabel = PHASE_LABELS[p.phase] || p.phase;
|
|
97
|
+
const scaled = Math.round(p.percent * 0.6);
|
|
98
|
+
progress(p.phase, scaled, phaseLabel);
|
|
99
|
+
});
|
|
100
|
+
// ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
|
|
101
|
+
progress('lbug', 60, 'Loading into LadybugDB...');
|
|
102
|
+
await closeLbug();
|
|
103
|
+
const lbugFiles = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.lock`];
|
|
104
|
+
for (const f of lbugFiles) {
|
|
105
|
+
try {
|
|
106
|
+
await fs.rm(f, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* swallow */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
await initLbug(lbugPath);
|
|
113
|
+
try {
|
|
114
|
+
// All work after initLbug is wrapped in try/finally to ensure closeLbug()
|
|
115
|
+
// is called even if an error occurs — the module-level singleton DB handle
|
|
116
|
+
// must be released to avoid blocking subsequent invocations.
|
|
117
|
+
let lbugMsgCount = 0;
|
|
118
|
+
await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
|
|
119
|
+
lbugMsgCount++;
|
|
120
|
+
const pct = Math.min(84, 60 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 24));
|
|
121
|
+
progress('lbug', pct, msg);
|
|
122
|
+
});
|
|
123
|
+
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
|
124
|
+
progress('fts', 85, 'Creating search indexes...');
|
|
125
|
+
try {
|
|
126
|
+
await createFTSIndex('File', 'file_fts', ['name', 'content']);
|
|
127
|
+
await createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
|
128
|
+
await createFTSIndex('Class', 'class_fts', ['name', 'content']);
|
|
129
|
+
await createFTSIndex('Method', 'method_fts', ['name', 'content']);
|
|
130
|
+
await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Non-fatal — FTS is best-effort
|
|
134
|
+
}
|
|
135
|
+
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
136
|
+
if (cachedEmbeddings.length > 0) {
|
|
137
|
+
const cachedDims = cachedEmbeddings[0].embedding.length;
|
|
138
|
+
const { EMBEDDING_DIMS } = await import('./lbug/schema.js');
|
|
139
|
+
if (cachedDims !== EMBEDDING_DIMS) {
|
|
140
|
+
// Dimensions changed (e.g. switched embedding model) — discard cache and re-embed all
|
|
141
|
+
log(`Embedding dimensions changed (${cachedDims}d -> ${EMBEDDING_DIMS}d), discarding cache`);
|
|
142
|
+
cachedEmbeddings = [];
|
|
143
|
+
cachedEmbeddingNodeIds = new Set();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
progress('embeddings', 88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
|
|
147
|
+
const EMBED_BATCH = 200;
|
|
148
|
+
for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
|
|
149
|
+
const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
|
|
150
|
+
const paramsList = batch.map((e) => ({ nodeId: e.nodeId, embedding: e.embedding }));
|
|
151
|
+
try {
|
|
152
|
+
await executeWithReusedStatement(`CREATE (e:CodeEmbedding {nodeId: $nodeId, embedding: $embedding})`, paramsList);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* some may fail if node was removed, that's fine */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
|
|
161
|
+
const stats = await getLbugStats();
|
|
162
|
+
let embeddingSkipped = true;
|
|
163
|
+
if (options.embeddings) {
|
|
164
|
+
if (stats.nodes <= EMBEDDING_NODE_LIMIT) {
|
|
165
|
+
embeddingSkipped = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!embeddingSkipped) {
|
|
169
|
+
const { isHttpMode } = await import('./embeddings/http-client.js');
|
|
170
|
+
const httpMode = isHttpMode();
|
|
171
|
+
progress('embeddings', 90, httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...');
|
|
172
|
+
const { runEmbeddingPipeline } = await import('./embeddings/embedding-pipeline.js');
|
|
173
|
+
await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (p) => {
|
|
174
|
+
const scaled = 90 + Math.round((p.percent / 100) * 8);
|
|
175
|
+
const label = p.phase === 'loading-model'
|
|
176
|
+
? httpMode
|
|
177
|
+
? 'Connecting to embedding endpoint...'
|
|
178
|
+
: 'Loading embedding model...'
|
|
179
|
+
: `Embedding ${p.nodesProcessed || 0}/${p.totalNodes || '?'}`;
|
|
180
|
+
progress('embeddings', scaled, label);
|
|
181
|
+
}, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
|
|
182
|
+
}
|
|
183
|
+
// ── Phase 5: Finalize (98–100%) ───────────────────────────────────
|
|
184
|
+
progress('done', 98, 'Saving metadata...');
|
|
185
|
+
// Count embeddings in the index (cached + newly generated)
|
|
186
|
+
let embeddingCount = 0;
|
|
187
|
+
try {
|
|
188
|
+
const embResult = await executeQuery(`MATCH (e:CodeEmbedding) RETURN count(e) AS cnt`);
|
|
189
|
+
embeddingCount = embResult?.[0]?.cnt ?? 0;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* table may not exist if embeddings never ran */
|
|
193
|
+
}
|
|
194
|
+
const meta = {
|
|
195
|
+
repoPath,
|
|
196
|
+
lastCommit: currentCommit,
|
|
197
|
+
indexedAt: new Date().toISOString(),
|
|
198
|
+
stats: {
|
|
199
|
+
files: pipelineResult.totalFileCount,
|
|
200
|
+
nodes: stats.nodes,
|
|
201
|
+
edges: stats.edges,
|
|
202
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
203
|
+
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
204
|
+
embeddings: embeddingCount,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
await saveMeta(storagePath, meta);
|
|
208
|
+
await registerRepo(repoPath, meta);
|
|
209
|
+
// Only attempt to update .gitignore when a .git directory is present.
|
|
210
|
+
if (hasGitDir(repoPath)) {
|
|
211
|
+
await addToGitignore(repoPath);
|
|
212
|
+
}
|
|
213
|
+
const projectName = path.basename(repoPath);
|
|
214
|
+
// ── Generate AI context files (best-effort) ───────────────────────
|
|
215
|
+
let aggregatedClusterCount = 0;
|
|
216
|
+
if (pipelineResult.communityResult?.communities) {
|
|
217
|
+
const groups = new Map();
|
|
218
|
+
for (const c of pipelineResult.communityResult.communities) {
|
|
219
|
+
const label = c.heuristicLabel || c.label || 'Unknown';
|
|
220
|
+
groups.set(label, (groups.get(label) || 0) + c.symbolCount);
|
|
221
|
+
}
|
|
222
|
+
aggregatedClusterCount = Array.from(groups.values()).filter((count) => count >= 5).length;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
226
|
+
files: pipelineResult.totalFileCount,
|
|
227
|
+
nodes: stats.nodes,
|
|
228
|
+
edges: stats.edges,
|
|
229
|
+
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
230
|
+
clusters: aggregatedClusterCount,
|
|
231
|
+
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
232
|
+
}, undefined, { skipAgentsMd: options.skipAgentsMd });
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Best-effort — don't fail the entire analysis for context file issues
|
|
236
|
+
}
|
|
237
|
+
// ── Close LadybugDB ──────────────────────────────────────────────
|
|
238
|
+
await closeLbug();
|
|
239
|
+
progress('done', 100, 'Done');
|
|
240
|
+
return {
|
|
241
|
+
repoName: projectName,
|
|
242
|
+
repoPath,
|
|
243
|
+
stats: meta.stats,
|
|
244
|
+
pipelineResult,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
// Ensure LadybugDB is closed even on error
|
|
249
|
+
try {
|
|
250
|
+
await closeLbug();
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* swallow */
|
|
254
|
+
}
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
|
-
import { SupportedLanguages } from '
|
|
2
|
+
import { SupportedLanguages } from 'gitnexus-shared';
|
|
3
3
|
export declare const isLanguageAvailable: (language: SupportedLanguages) => boolean;
|
|
4
4
|
export declare const loadParser: () => Promise<Parser>;
|
|
5
5
|
export declare const loadLanguage: (language: SupportedLanguages, filePath?: string) => Promise<void>;
|
|
@@ -11,7 +11,7 @@ import Rust from 'tree-sitter-rust';
|
|
|
11
11
|
import PHP from 'tree-sitter-php';
|
|
12
12
|
import Ruby from 'tree-sitter-ruby';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
-
import { SupportedLanguages } from '
|
|
14
|
+
import { SupportedLanguages } from 'gitnexus-shared';
|
|
15
15
|
// tree-sitter-swift and tree-sitter-dart are optionalDependencies — may not be installed
|
|
16
16
|
const _require = createRequire(import.meta.url);
|
|
17
17
|
let Swift = null;
|
|
@@ -57,13 +57,8 @@ export async function callCursorLLM(prompt, config, systemPrompt, options) {
|
|
|
57
57
|
// Always use text format to get clean output without agent narration/thinking.
|
|
58
58
|
// stream-json captures assistant messages which include "Let me explore..." narration
|
|
59
59
|
// that pollutes the actual content when using thinking models.
|
|
60
|
-
const fullPrompt = systemPrompt
|
|
61
|
-
|
|
62
|
-
: prompt;
|
|
63
|
-
const args = [
|
|
64
|
-
'-p',
|
|
65
|
-
'--output-format', 'text',
|
|
66
|
-
];
|
|
60
|
+
const fullPrompt = systemPrompt ? `${systemPrompt}\n\n---\n\n${prompt}` : prompt;
|
|
61
|
+
const args = ['-p', '--output-format', 'text'];
|
|
67
62
|
if (config.model) {
|
|
68
63
|
args.push('--model', config.model);
|
|
69
64
|
}
|
|
@@ -15,7 +15,7 @@ import { execSync, execFileSync } from 'child_process';
|
|
|
15
15
|
import { initWikiDb, closeWikiDb, touchWikiDb, getFilesWithExports, getAllFiles, getIntraModuleCallEdges, getInterModuleCallEdges, getProcessesForFiles, getAllProcesses, getInterModuleEdgesForOverview, } from './graph-queries.js';
|
|
16
16
|
import { generateHTMLViewer } from './html-viewer.js';
|
|
17
17
|
import { callLLM, estimateTokens, } from './llm-client.js';
|
|
18
|
-
import { callCursorLLM, resolveCursorConfig
|
|
18
|
+
import { callCursorLLM, resolveCursorConfig } from './cursor-client.js';
|
|
19
19
|
import { GROUPING_SYSTEM_PROMPT, GROUPING_USER_PROMPT, MODULE_SYSTEM_PROMPT, MODULE_USER_PROMPT, PARENT_SYSTEM_PROMPT, PARENT_USER_PROMPT, OVERVIEW_SYSTEM_PROMPT, OVERVIEW_USER_PROMPT, fillTemplate, formatFileListForGrouping, formatDirectoryTree, formatCallEdges, formatProcesses, } from './prompts.js';
|
|
20
20
|
import { shouldIgnorePath } from '../../config/ignore-service.js';
|
|
21
21
|
// ─── Constants ────────────────────────────────────────────────────────
|
|
@@ -72,7 +72,7 @@ export class WikiGenerator {
|
|
|
72
72
|
if (hasFixedStart) {
|
|
73
73
|
// For fixed phases (like grouping), show incremental progress
|
|
74
74
|
const progress = Math.min(1, tokens / expectedTokens);
|
|
75
|
-
const pct = Math.round(startPercent +
|
|
75
|
+
const pct = Math.round(startPercent + progress * percentRange);
|
|
76
76
|
this.onProgress('stream', pct, `${label} (${tokens} tok)`);
|
|
77
77
|
}
|
|
78
78
|
else {
|
|
@@ -155,7 +155,7 @@ export class WikiGenerator {
|
|
|
155
155
|
async ensureHTMLViewer() {
|
|
156
156
|
// Only generate if there are markdown pages to bundle
|
|
157
157
|
const dirEntries = await fs.readdir(this.wikiDir).catch(() => []);
|
|
158
|
-
const hasMd = dirEntries.some(f => f.endsWith('.md'));
|
|
158
|
+
const hasMd = dirEntries.some((f) => f.endsWith('.md'));
|
|
159
159
|
if (!hasMd)
|
|
160
160
|
return;
|
|
161
161
|
this.onProgress('html', 98, 'Building HTML viewer...');
|
|
@@ -170,13 +170,13 @@ export class WikiGenerator {
|
|
|
170
170
|
const filesWithExports = await getFilesWithExports();
|
|
171
171
|
const allFiles = await getAllFiles();
|
|
172
172
|
// Filter to source files only
|
|
173
|
-
const sourceFiles = allFiles.filter(f => !shouldIgnorePath(f));
|
|
173
|
+
const sourceFiles = allFiles.filter((f) => !shouldIgnorePath(f));
|
|
174
174
|
if (sourceFiles.length === 0) {
|
|
175
175
|
throw new Error('No source files found in the knowledge graph. Nothing to document.');
|
|
176
176
|
}
|
|
177
177
|
// Build enriched file list (merge exports into all source files)
|
|
178
|
-
const exportMap = new Map(filesWithExports.map(f => [f.filePath, f]));
|
|
179
|
-
const enrichedFiles = sourceFiles.map(fp => {
|
|
178
|
+
const exportMap = new Map(filesWithExports.map((f) => [f.filePath, f]));
|
|
179
|
+
const enrichedFiles = sourceFiles.map((fp) => {
|
|
180
180
|
return exportMap.get(fp) || { filePath: fp, symbols: [] };
|
|
181
181
|
});
|
|
182
182
|
this.onProgress('gather', 10, `Found ${sourceFiles.length} source files`);
|
|
@@ -187,7 +187,12 @@ export class WikiGenerator {
|
|
|
187
187
|
if (this.options.reviewOnly) {
|
|
188
188
|
await this.saveModuleTree(moduleTree);
|
|
189
189
|
this.onProgress('review', 30, 'Module tree ready for review');
|
|
190
|
-
const reviewResult = {
|
|
190
|
+
const reviewResult = {
|
|
191
|
+
pagesGenerated: 0,
|
|
192
|
+
mode: 'full',
|
|
193
|
+
failedModules: [],
|
|
194
|
+
moduleTree,
|
|
195
|
+
};
|
|
191
196
|
return reviewResult;
|
|
192
197
|
}
|
|
193
198
|
// Phase 2: Generate module pages (parallel with concurrency limit)
|
|
@@ -287,7 +292,7 @@ export class WikiGenerator {
|
|
|
287
292
|
}
|
|
288
293
|
this.onProgress('grouping', 15, 'Grouping files into modules (LLM)...');
|
|
289
294
|
const fileList = formatFileListForGrouping(files);
|
|
290
|
-
const dirTree = formatDirectoryTree(files.map(f => f.filePath));
|
|
295
|
+
const dirTree = formatDirectoryTree(files.map((f) => f.filePath));
|
|
291
296
|
const prompt = fillTemplate(GROUPING_USER_PROMPT, {
|
|
292
297
|
FILE_LIST: fileList,
|
|
293
298
|
DIRECTORY_TREE: dirTree,
|
|
@@ -340,13 +345,13 @@ export class WikiGenerator {
|
|
|
340
345
|
return this.fallbackGrouping(files);
|
|
341
346
|
}
|
|
342
347
|
// Validate — ensure all files are assigned
|
|
343
|
-
const allFilePaths = new Set(files.map(f => f.filePath));
|
|
348
|
+
const allFilePaths = new Set(files.map((f) => f.filePath));
|
|
344
349
|
const assignedFiles = new Set();
|
|
345
350
|
const validGrouping = {};
|
|
346
351
|
for (const [mod, paths] of Object.entries(parsed)) {
|
|
347
352
|
if (!Array.isArray(paths))
|
|
348
353
|
continue;
|
|
349
|
-
const validPaths = paths.filter(p => {
|
|
354
|
+
const validPaths = paths.filter((p) => {
|
|
350
355
|
if (allFilePaths.has(p) && !assignedFiles.has(p)) {
|
|
351
356
|
assignedFiles.add(p);
|
|
352
357
|
return true;
|
|
@@ -358,15 +363,11 @@ export class WikiGenerator {
|
|
|
358
363
|
}
|
|
359
364
|
}
|
|
360
365
|
// Assign unassigned files to a "Miscellaneous" module
|
|
361
|
-
const unassigned = files
|
|
362
|
-
.map(f => f.filePath)
|
|
363
|
-
.filter(fp => !assignedFiles.has(fp));
|
|
366
|
+
const unassigned = files.map((f) => f.filePath).filter((fp) => !assignedFiles.has(fp));
|
|
364
367
|
if (unassigned.length > 0) {
|
|
365
368
|
validGrouping['Other'] = unassigned;
|
|
366
369
|
}
|
|
367
|
-
return Object.keys(validGrouping).length > 0
|
|
368
|
-
? validGrouping
|
|
369
|
-
: this.fallbackGrouping(files);
|
|
370
|
+
return Object.keys(validGrouping).length > 0 ? validGrouping : this.fallbackGrouping(files);
|
|
370
371
|
}
|
|
371
372
|
/**
|
|
372
373
|
* Fallback grouping by top-level directory when LLM parsing fails.
|
|
@@ -403,7 +404,7 @@ export class WikiGenerator {
|
|
|
403
404
|
group.push(fp);
|
|
404
405
|
}
|
|
405
406
|
// Check if basenames are unique; if not, use the full subDir path
|
|
406
|
-
const basenames = Array.from(subGroups.keys()).map(s => path.basename(s));
|
|
407
|
+
const basenames = Array.from(subGroups.keys()).map((s) => path.basename(s));
|
|
407
408
|
const hasCollisions = new Set(basenames).size < basenames.length;
|
|
408
409
|
return Array.from(subGroups.entries()).map(([subDir, subFiles]) => {
|
|
409
410
|
const label = hasCollisions ? subDir.replace(/\//g, '-') : path.basename(subDir);
|
|
@@ -469,7 +470,7 @@ export class WikiGenerator {
|
|
|
469
470
|
}
|
|
470
471
|
}
|
|
471
472
|
// Get cross-child call edges
|
|
472
|
-
const allChildFiles = node.children.flatMap(c => c.files);
|
|
473
|
+
const allChildFiles = node.children.flatMap((c) => c.files);
|
|
473
474
|
const crossCalls = await getIntraModuleCallEdges(allChildFiles);
|
|
474
475
|
const processes = await getProcessesForFiles(allChildFiles, 3);
|
|
475
476
|
const prompt = fillTemplate(PARENT_USER_PROMPT, {
|
|
@@ -506,7 +507,7 @@ export class WikiGenerator {
|
|
|
506
507
|
// Read project config
|
|
507
508
|
const projectInfo = await this.readProjectInfo();
|
|
508
509
|
const edgesText = moduleEdges.length > 0
|
|
509
|
-
? moduleEdges.map(e => `${e.from} → ${e.to} (${e.count} calls)`).join('\n')
|
|
510
|
+
? moduleEdges.map((e) => `${e.from} → ${e.to} (${e.count} calls)`).join('\n')
|
|
510
511
|
: 'No inter-module call edges detected';
|
|
511
512
|
const prompt = fillTemplate(OVERVIEW_USER_PROMPT, {
|
|
512
513
|
PROJECT_INFO: projectInfo,
|
|
@@ -644,7 +645,10 @@ export class WikiGenerator {
|
|
|
644
645
|
*/
|
|
645
646
|
isCommitReachable(fromCommit, toCommit) {
|
|
646
647
|
try {
|
|
647
|
-
execFileSync('git', ['merge-base', '--is-ancestor', fromCommit, toCommit], {
|
|
648
|
+
execFileSync('git', ['merge-base', '--is-ancestor', fromCommit, toCommit], {
|
|
649
|
+
cwd: this.repoPath,
|
|
650
|
+
stdio: 'ignore',
|
|
651
|
+
});
|
|
648
652
|
return true;
|
|
649
653
|
}
|
|
650
654
|
catch {
|
|
@@ -658,7 +662,11 @@ export class WikiGenerator {
|
|
|
658
662
|
return null; // Signal that we can't compute diff (divergent branches)
|
|
659
663
|
}
|
|
660
664
|
try {
|
|
661
|
-
const output = execFileSync('git', ['diff', `${fromCommit}..${toCommit}`, '--name-only'], {
|
|
665
|
+
const output = execFileSync('git', ['diff', `${fromCommit}..${toCommit}`, '--name-only'], {
|
|
666
|
+
cwd: this.repoPath,
|
|
667
|
+
})
|
|
668
|
+
.toString()
|
|
669
|
+
.trim();
|
|
662
670
|
return output ? output.split('\n').filter(Boolean) : [];
|
|
663
671
|
}
|
|
664
672
|
catch {
|
|
@@ -700,7 +708,14 @@ export class WikiGenerator {
|
|
|
700
708
|
return total;
|
|
701
709
|
}
|
|
702
710
|
async readProjectInfo() {
|
|
703
|
-
const candidates = [
|
|
711
|
+
const candidates = [
|
|
712
|
+
'package.json',
|
|
713
|
+
'Cargo.toml',
|
|
714
|
+
'pyproject.toml',
|
|
715
|
+
'go.mod',
|
|
716
|
+
'pom.xml',
|
|
717
|
+
'build.gradle',
|
|
718
|
+
];
|
|
704
719
|
const lines = [`Project: ${path.basename(this.repoPath)}`];
|
|
705
720
|
for (const file of candidates) {
|
|
706
721
|
const fullPath = path.join(this.repoPath, file);
|
|
@@ -742,7 +757,7 @@ export class WikiGenerator {
|
|
|
742
757
|
const result = {};
|
|
743
758
|
for (const node of tree) {
|
|
744
759
|
if (node.children && node.children.length > 0) {
|
|
745
|
-
result[node.name] = node.children.flatMap(c => c.files);
|
|
760
|
+
result[node.name] = node.children.flatMap((c) => c.files);
|
|
746
761
|
for (const child of node.children) {
|
|
747
762
|
result[child.name] = child.files;
|
|
748
763
|
}
|
|
@@ -57,7 +57,7 @@ export async function getAllFiles() {
|
|
|
57
57
|
RETURN f.filePath AS filePath
|
|
58
58
|
ORDER BY f.filePath
|
|
59
59
|
`);
|
|
60
|
-
return rows.map(r => r.filePath || r[0]);
|
|
60
|
+
return rows.map((r) => r.filePath || r[0]);
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Get inter-file call edges (calls between different files).
|
|
@@ -69,7 +69,7 @@ export async function getInterFileCallEdges() {
|
|
|
69
69
|
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
70
70
|
b.filePath AS toFile, b.name AS toName
|
|
71
71
|
`);
|
|
72
|
-
return rows.map(r => ({
|
|
72
|
+
return rows.map((r) => ({
|
|
73
73
|
fromFile: r.fromFile || r[0],
|
|
74
74
|
fromName: r.fromName || r[1],
|
|
75
75
|
toFile: r.toFile || r[2],
|
|
@@ -82,14 +82,14 @@ export async function getInterFileCallEdges() {
|
|
|
82
82
|
export async function getIntraModuleCallEdges(filePaths) {
|
|
83
83
|
if (filePaths.length === 0)
|
|
84
84
|
return [];
|
|
85
|
-
const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
85
|
+
const fileList = filePaths.map((f) => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
86
86
|
const rows = await executeQuery(REPO_ID, `
|
|
87
87
|
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
88
88
|
WHERE a.filePath IN [${fileList}] AND b.filePath IN [${fileList}]
|
|
89
89
|
RETURN DISTINCT a.filePath AS fromFile, a.name AS fromName,
|
|
90
90
|
b.filePath AS toFile, b.name AS toName
|
|
91
91
|
`);
|
|
92
|
-
return rows.map(r => ({
|
|
92
|
+
return rows.map((r) => ({
|
|
93
93
|
fromFile: r.fromFile || r[0],
|
|
94
94
|
fromName: r.fromName || r[1],
|
|
95
95
|
toFile: r.toFile || r[2],
|
|
@@ -102,7 +102,7 @@ export async function getIntraModuleCallEdges(filePaths) {
|
|
|
102
102
|
export async function getInterModuleCallEdges(filePaths) {
|
|
103
103
|
if (filePaths.length === 0)
|
|
104
104
|
return { outgoing: [], incoming: [] };
|
|
105
|
-
const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
105
|
+
const fileList = filePaths.map((f) => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
106
106
|
const outRows = await executeQuery(REPO_ID, `
|
|
107
107
|
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
|
108
108
|
WHERE a.filePath IN [${fileList}] AND NOT b.filePath IN [${fileList}]
|
|
@@ -118,13 +118,13 @@ export async function getInterModuleCallEdges(filePaths) {
|
|
|
118
118
|
LIMIT 30
|
|
119
119
|
`);
|
|
120
120
|
return {
|
|
121
|
-
outgoing: outRows.map(r => ({
|
|
121
|
+
outgoing: outRows.map((r) => ({
|
|
122
122
|
fromFile: r.fromFile || r[0],
|
|
123
123
|
fromName: r.fromName || r[1],
|
|
124
124
|
toFile: r.toFile || r[2],
|
|
125
125
|
toName: r.toName || r[3],
|
|
126
126
|
})),
|
|
127
|
-
incoming: inRows.map(r => ({
|
|
127
|
+
incoming: inRows.map((r) => ({
|
|
128
128
|
fromFile: r.fromFile || r[0],
|
|
129
129
|
fromName: r.fromName || r[1],
|
|
130
130
|
toFile: r.toFile || r[2],
|
|
@@ -139,7 +139,7 @@ export async function getInterModuleCallEdges(filePaths) {
|
|
|
139
139
|
export async function getProcessesForFiles(filePaths, limit = 5) {
|
|
140
140
|
if (filePaths.length === 0)
|
|
141
141
|
return [];
|
|
142
|
-
const fileList = filePaths.map(f => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
142
|
+
const fileList = filePaths.map((f) => `'${f.replace(/'/g, "''")}'`).join(', ');
|
|
143
143
|
// Find processes that have steps in the given files
|
|
144
144
|
const procRows = await executeQuery(REPO_ID, `
|
|
145
145
|
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
@@ -166,7 +166,7 @@ export async function getProcessesForFiles(filePaths, limit = 5) {
|
|
|
166
166
|
label,
|
|
167
167
|
type,
|
|
168
168
|
stepCount,
|
|
169
|
-
steps: stepRows.map(s => ({
|
|
169
|
+
steps: stepRows.map((s) => ({
|
|
170
170
|
step: s.step || s[3] || 0,
|
|
171
171
|
name: s.name || s[0],
|
|
172
172
|
filePath: s.filePath || s[1],
|
|
@@ -203,7 +203,7 @@ export async function getAllProcesses(limit = 20) {
|
|
|
203
203
|
label,
|
|
204
204
|
type,
|
|
205
205
|
stepCount,
|
|
206
|
-
steps: stepRows.map(s => ({
|
|
206
|
+
steps: stepRows.map((s) => ({
|
|
207
207
|
step: s.step || s[3] || 0,
|
|
208
208
|
name: s.name || s[0],
|
|
209
209
|
filePath: s.filePath || s[1],
|
|
@@ -16,18 +16,22 @@ export async function generateHTMLViewer(wikiDir, projectName) {
|
|
|
16
16
|
const raw = await fs.readFile(path.join(wikiDir, 'module_tree.json'), 'utf-8');
|
|
17
17
|
moduleTree = JSON.parse(raw);
|
|
18
18
|
}
|
|
19
|
-
catch {
|
|
19
|
+
catch {
|
|
20
|
+
/* will show empty nav */
|
|
21
|
+
}
|
|
20
22
|
// Load meta
|
|
21
23
|
let meta = null;
|
|
22
24
|
try {
|
|
23
25
|
const raw = await fs.readFile(path.join(wikiDir, 'meta.json'), 'utf-8');
|
|
24
26
|
meta = JSON.parse(raw);
|
|
25
27
|
}
|
|
26
|
-
catch {
|
|
28
|
+
catch {
|
|
29
|
+
/* no meta */
|
|
30
|
+
}
|
|
27
31
|
// Read all markdown files into a { slug: content } map
|
|
28
32
|
const pages = {};
|
|
29
33
|
const dirEntries = await fs.readdir(wikiDir);
|
|
30
|
-
for (const f of dirEntries.filter(f => f.endsWith('.md'))) {
|
|
34
|
+
for (const f of dirEntries.filter((f) => f.endsWith('.md'))) {
|
|
31
35
|
const content = await fs.readFile(path.join(wikiDir, f), 'utf-8');
|
|
32
36
|
pages[f.replace(/\.md$/, '')] = content;
|
|
33
37
|
}
|
|
@@ -6,14 +6,19 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Config priority: CLI flags > env vars > defaults
|
|
8
8
|
*/
|
|
9
|
-
export type LLMProvider = 'openai' | 'cursor';
|
|
9
|
+
export type LLMProvider = 'openai' | 'openrouter' | 'azure' | 'custom' | 'cursor';
|
|
10
10
|
export interface LLMConfig {
|
|
11
11
|
apiKey: string;
|
|
12
12
|
baseUrl: string;
|
|
13
13
|
model: string;
|
|
14
14
|
maxTokens: number;
|
|
15
15
|
temperature: number;
|
|
16
|
-
|
|
16
|
+
/** Provider type — controls auth header behaviour */
|
|
17
|
+
provider?: 'openai' | 'openrouter' | 'azure' | 'custom' | 'cursor';
|
|
18
|
+
/** Azure api-version query param (e.g. '2024-10-21'). Appended to URL when set. */
|
|
19
|
+
apiVersion?: string;
|
|
20
|
+
/** When true, strips sampling params and uses max_completion_tokens instead of max_tokens */
|
|
21
|
+
isReasoningModel?: boolean;
|
|
17
22
|
}
|
|
18
23
|
export interface LLMResponse {
|
|
19
24
|
content: string;
|
|
@@ -31,6 +36,22 @@ export declare function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promis
|
|
|
31
36
|
* Estimate token count from text (rough heuristic: ~4 chars per token).
|
|
32
37
|
*/
|
|
33
38
|
export declare function estimateTokens(text: string): number;
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if the given base URL is an Azure OpenAI endpoint.
|
|
41
|
+
* Uses proper hostname matching to avoid spoofed URLs like
|
|
42
|
+
* "https://myresource.openai.azure.com.evil.com/v1".
|
|
43
|
+
*/
|
|
44
|
+
export declare function isAzureProvider(baseUrl: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Returns true if the model name matches a known reasoning model pattern,
|
|
47
|
+
* or if the explicit override is true.
|
|
48
|
+
* Pass override=false to force non-reasoning even for o-series names.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isReasoningModel(model: string, override?: boolean): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Build the full chat completions URL, appending ?api-version when provided.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildRequestUrl(baseUrl: string, apiVersion: string | undefined): string;
|
|
34
55
|
export interface CallLLMOptions {
|
|
35
56
|
onChunk?: (charsReceived: number) => void;
|
|
36
57
|
}
|