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: gpt-4o-mini)')
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 = 'openai/gpt-4o-mini';
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
- this.onProgress = onProgress || (() => { });
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
- this.onProgress('modules', 0, `Failed: ${node.name} — ${err.message?.slice(0, 80)}`);
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
- this.onProgress('modules', 0, `Failed: ${node.name} — ${err.message?.slice(0, 80)}`);
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', 0, `Rate limited — reducing concurrency to ${activeConcurrency}`);
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
- * Retries once on transient failures (5xx, network errors).
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://api.openai.com/v1',
28
+ || 'https://openrouter.ai/api/v1',
29
29
  model: overrides?.model
30
30
  || process.env.GITNEXUS_MODEL
31
31
  || savedConfig.model
32
- || 'gpt-4o-mini',
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
- * Retries once on transient failures (5xx, network errors).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",