gitnexus 1.2.4 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js
CHANGED
|
@@ -53,7 +53,7 @@ program
|
|
|
53
53
|
.command('wiki [path]')
|
|
54
54
|
.description('Generate repository wiki from knowledge graph')
|
|
55
55
|
.option('-f, --force', 'Force full regeneration even if up to date')
|
|
56
|
-
.option('--model <model>', 'LLM model name (default:
|
|
56
|
+
.option('--model <model>', 'LLM model name (default: minimax/minimax-m2.5)')
|
|
57
57
|
.option('--base-url <url>', 'LLM API base URL (default: OpenAI)')
|
|
58
58
|
.option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
|
|
59
59
|
.option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
|
package/dist/cli/wiki.js
CHANGED
|
@@ -141,7 +141,7 @@ export const wikiCommand = async (inputPath, options) => {
|
|
|
141
141
|
let defaultModel;
|
|
142
142
|
if (choice === '2') {
|
|
143
143
|
baseUrl = 'https://openrouter.ai/api/v1';
|
|
144
|
-
defaultModel = '
|
|
144
|
+
defaultModel = 'minimax/minimax-m2.5';
|
|
145
145
|
}
|
|
146
146
|
else if (choice === '3') {
|
|
147
147
|
baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
|
|
@@ -44,6 +44,12 @@ export declare class WikiGenerator {
|
|
|
44
44
|
private onProgress;
|
|
45
45
|
private failedModules;
|
|
46
46
|
constructor(repoPath: string, storagePath: string, kuzuPath: string, llmConfig: LLMConfig, options?: WikiOptions, onProgress?: ProgressCallback);
|
|
47
|
+
private lastPercent;
|
|
48
|
+
/**
|
|
49
|
+
* Create streaming options that report LLM progress to the progress bar.
|
|
50
|
+
* Uses the last known percent so streaming doesn't reset the bar backwards.
|
|
51
|
+
*/
|
|
52
|
+
private streamOpts;
|
|
47
53
|
/**
|
|
48
54
|
* Main entry point. Runs the full pipeline or incremental update.
|
|
49
55
|
*/
|
|
@@ -41,7 +41,26 @@ export class WikiGenerator {
|
|
|
41
41
|
this.llmConfig = llmConfig;
|
|
42
42
|
this.maxTokensPerModule = options.maxTokensPerModule ?? DEFAULT_MAX_TOKENS_PER_MODULE;
|
|
43
43
|
this.concurrency = options.concurrency ?? 3;
|
|
44
|
-
|
|
44
|
+
const progressFn = onProgress || (() => { });
|
|
45
|
+
this.onProgress = (phase, percent, detail) => {
|
|
46
|
+
if (percent > 0)
|
|
47
|
+
this.lastPercent = percent;
|
|
48
|
+
progressFn(phase, percent, detail);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
lastPercent = 0;
|
|
52
|
+
/**
|
|
53
|
+
* Create streaming options that report LLM progress to the progress bar.
|
|
54
|
+
* Uses the last known percent so streaming doesn't reset the bar backwards.
|
|
55
|
+
*/
|
|
56
|
+
streamOpts(label, fixedPercent) {
|
|
57
|
+
return {
|
|
58
|
+
onChunk: (chars) => {
|
|
59
|
+
const tokens = Math.round(chars / 4);
|
|
60
|
+
const pct = fixedPercent ?? this.lastPercent;
|
|
61
|
+
this.onProgress('stream', pct, `${label} (${tokens} tok)`);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
45
64
|
}
|
|
46
65
|
/**
|
|
47
66
|
* Main entry point. Runs the full pipeline or incremental update.
|
|
@@ -153,8 +172,7 @@ export class WikiGenerator {
|
|
|
153
172
|
}
|
|
154
173
|
catch (err) {
|
|
155
174
|
this.failedModules.push(node.name);
|
|
156
|
-
|
|
157
|
-
reportProgress(node.name);
|
|
175
|
+
reportProgress(`Failed: ${node.name}`);
|
|
158
176
|
return 0;
|
|
159
177
|
}
|
|
160
178
|
});
|
|
@@ -172,8 +190,7 @@ export class WikiGenerator {
|
|
|
172
190
|
}
|
|
173
191
|
catch (err) {
|
|
174
192
|
this.failedModules.push(node.name);
|
|
175
|
-
|
|
176
|
-
reportProgress(node.name);
|
|
193
|
+
reportProgress(`Failed: ${node.name}`);
|
|
177
194
|
}
|
|
178
195
|
}
|
|
179
196
|
// Phase 3: Generate overview
|
|
@@ -216,7 +233,7 @@ export class WikiGenerator {
|
|
|
216
233
|
FILE_LIST: fileList,
|
|
217
234
|
DIRECTORY_TREE: dirTree,
|
|
218
235
|
});
|
|
219
|
-
const response = await callLLM(prompt, this.llmConfig, GROUPING_SYSTEM_PROMPT);
|
|
236
|
+
const response = await callLLM(prompt, this.llmConfig, GROUPING_SYSTEM_PROMPT, this.streamOpts('Grouping files', 15));
|
|
220
237
|
const grouping = this.parseGroupingResponse(response.content, files);
|
|
221
238
|
// Convert to tree nodes
|
|
222
239
|
const tree = [];
|
|
@@ -353,7 +370,7 @@ export class WikiGenerator {
|
|
|
353
370
|
INCOMING_CALLS: formatCallEdges(interCalls.incoming),
|
|
354
371
|
PROCESSES: formatProcesses(processes),
|
|
355
372
|
});
|
|
356
|
-
const response = await callLLM(prompt, this.llmConfig, MODULE_SYSTEM_PROMPT);
|
|
373
|
+
const response = await callLLM(prompt, this.llmConfig, MODULE_SYSTEM_PROMPT, this.streamOpts(node.name));
|
|
357
374
|
// Write page with front matter
|
|
358
375
|
const pageContent = `# ${node.name}\n\n${response.content}`;
|
|
359
376
|
await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
|
|
@@ -389,7 +406,7 @@ export class WikiGenerator {
|
|
|
389
406
|
CROSS_MODULE_CALLS: formatCallEdges(crossCalls),
|
|
390
407
|
CROSS_PROCESSES: formatProcesses(processes),
|
|
391
408
|
});
|
|
392
|
-
const response = await callLLM(prompt, this.llmConfig, PARENT_SYSTEM_PROMPT);
|
|
409
|
+
const response = await callLLM(prompt, this.llmConfig, PARENT_SYSTEM_PROMPT, this.streamOpts(node.name));
|
|
393
410
|
const pageContent = `# ${node.name}\n\n${response.content}`;
|
|
394
411
|
await fs.writeFile(path.join(this.wikiDir, `${node.slug}.md`), pageContent, 'utf-8');
|
|
395
412
|
}
|
|
@@ -425,7 +442,7 @@ export class WikiGenerator {
|
|
|
425
442
|
MODULE_EDGES: edgesText,
|
|
426
443
|
TOP_PROCESSES: formatProcesses(topProcesses),
|
|
427
444
|
});
|
|
428
|
-
const response = await callLLM(prompt, this.llmConfig, OVERVIEW_SYSTEM_PROMPT);
|
|
445
|
+
const response = await callLLM(prompt, this.llmConfig, OVERVIEW_SYSTEM_PROMPT, this.streamOpts('Generating overview', 88));
|
|
429
446
|
const pageContent = `# ${path.basename(this.repoPath)} — Wiki\n\n${response.content}`;
|
|
430
447
|
await fs.writeFile(path.join(this.wikiDir, 'overview.md'), pageContent, 'utf-8');
|
|
431
448
|
}
|
|
@@ -699,7 +716,7 @@ export class WikiGenerator {
|
|
|
699
716
|
// On rate limit, reduce concurrency temporarily
|
|
700
717
|
if (err.message?.includes('429')) {
|
|
701
718
|
activeConcurrency = Math.max(1, activeConcurrency - 1);
|
|
702
|
-
this.onProgress('modules',
|
|
719
|
+
this.onProgress('modules', this.lastPercent, `Rate limited — concurrency → ${activeConcurrency}`);
|
|
703
720
|
// Re-queue the item
|
|
704
721
|
idx--;
|
|
705
722
|
setTimeout(next, 5000);
|
|
@@ -29,8 +29,12 @@ export declare function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promis
|
|
|
29
29
|
* Estimate token count from text (rough heuristic: ~4 chars per token).
|
|
30
30
|
*/
|
|
31
31
|
export declare function estimateTokens(text: string): number;
|
|
32
|
+
export interface CallLLMOptions {
|
|
33
|
+
onChunk?: (charsReceived: number) => void;
|
|
34
|
+
}
|
|
32
35
|
/**
|
|
33
36
|
* Call an OpenAI-compatible LLM API.
|
|
34
|
-
*
|
|
37
|
+
* Uses streaming when onChunk callback is provided for real-time progress.
|
|
38
|
+
* Retries up to 3 times on transient failures (429, 5xx, network errors).
|
|
35
39
|
*/
|
|
36
|
-
export declare function callLLM(prompt: string, config: LLMConfig, systemPrompt?: string): Promise<LLMResponse>;
|
|
40
|
+
export declare function callLLM(prompt: string, config: LLMConfig, systemPrompt?: string, options?: CallLLMOptions): Promise<LLMResponse>;
|
|
@@ -25,11 +25,11 @@ export async function resolveLLMConfig(overrides) {
|
|
|
25
25
|
baseUrl: overrides?.baseUrl
|
|
26
26
|
|| process.env.GITNEXUS_LLM_BASE_URL
|
|
27
27
|
|| savedConfig.baseUrl
|
|
28
|
-
|| 'https://
|
|
28
|
+
|| 'https://openrouter.ai/api/v1',
|
|
29
29
|
model: overrides?.model
|
|
30
30
|
|| process.env.GITNEXUS_MODEL
|
|
31
31
|
|| savedConfig.model
|
|
32
|
-
|| '
|
|
32
|
+
|| 'minimax/minimax-m2.5',
|
|
33
33
|
maxTokens: overrides?.maxTokens ?? 16_384,
|
|
34
34
|
temperature: overrides?.temperature ?? 0,
|
|
35
35
|
};
|
|
@@ -42,21 +42,25 @@ export function estimateTokens(text) {
|
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Call an OpenAI-compatible LLM API.
|
|
45
|
-
*
|
|
45
|
+
* Uses streaming when onChunk callback is provided for real-time progress.
|
|
46
|
+
* Retries up to 3 times on transient failures (429, 5xx, network errors).
|
|
46
47
|
*/
|
|
47
|
-
export async function callLLM(prompt, config, systemPrompt) {
|
|
48
|
+
export async function callLLM(prompt, config, systemPrompt, options) {
|
|
48
49
|
const messages = [];
|
|
49
50
|
if (systemPrompt) {
|
|
50
51
|
messages.push({ role: 'system', content: systemPrompt });
|
|
51
52
|
}
|
|
52
53
|
messages.push({ role: 'user', content: prompt });
|
|
53
54
|
const url = `${config.baseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
55
|
+
const useStream = !!options?.onChunk;
|
|
54
56
|
const body = {
|
|
55
57
|
model: config.model,
|
|
56
58
|
messages,
|
|
57
59
|
max_tokens: config.maxTokens,
|
|
58
60
|
temperature: config.temperature,
|
|
59
61
|
};
|
|
62
|
+
if (useStream)
|
|
63
|
+
body.stream = true;
|
|
60
64
|
const MAX_RETRIES = 3;
|
|
61
65
|
let lastError = null;
|
|
62
66
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
@@ -85,6 +89,11 @@ export async function callLLM(prompt, config, systemPrompt) {
|
|
|
85
89
|
}
|
|
86
90
|
throw new Error(`LLM API error (${response.status}): ${errorText.slice(0, 500)}`);
|
|
87
91
|
}
|
|
92
|
+
// Streaming path
|
|
93
|
+
if (useStream && response.body) {
|
|
94
|
+
return await readSSEStream(response.body, options.onChunk);
|
|
95
|
+
}
|
|
96
|
+
// Non-streaming path
|
|
88
97
|
const json = await response.json();
|
|
89
98
|
const choice = json.choices?.[0];
|
|
90
99
|
if (!choice?.message?.content) {
|
|
@@ -108,6 +117,46 @@ export async function callLLM(prompt, config, systemPrompt) {
|
|
|
108
117
|
}
|
|
109
118
|
throw lastError || new Error('LLM call failed after retries');
|
|
110
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Read an SSE stream from an OpenAI-compatible streaming response.
|
|
122
|
+
*/
|
|
123
|
+
async function readSSEStream(body, onChunk) {
|
|
124
|
+
const decoder = new TextDecoder();
|
|
125
|
+
const reader = body.getReader();
|
|
126
|
+
let content = '';
|
|
127
|
+
let buffer = '';
|
|
128
|
+
while (true) {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done)
|
|
131
|
+
break;
|
|
132
|
+
buffer += decoder.decode(value, { stream: true });
|
|
133
|
+
const lines = buffer.split('\n');
|
|
134
|
+
buffer = lines.pop() || '';
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (!trimmed || !trimmed.startsWith('data: '))
|
|
138
|
+
continue;
|
|
139
|
+
const data = trimmed.slice(6);
|
|
140
|
+
if (data === '[DONE]')
|
|
141
|
+
continue;
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(data);
|
|
144
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
145
|
+
if (delta) {
|
|
146
|
+
content += delta;
|
|
147
|
+
onChunk(content.length);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Skip malformed SSE chunks
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!content) {
|
|
156
|
+
throw new Error('LLM returned empty streaming response');
|
|
157
|
+
}
|
|
158
|
+
return { content };
|
|
159
|
+
}
|
|
111
160
|
function sleep(ms) {
|
|
112
161
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
113
162
|
}
|
package/package.json
CHANGED