minovative-mind-cli 1.0.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.
Files changed (53) hide show
  1. package/README.md +418 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/chat.d.ts +7 -0
  7. package/dist/commands/chat.js +30 -0
  8. package/dist/commands/login.d.ts +5 -0
  9. package/dist/commands/login.js +18 -0
  10. package/dist/commands/logout.d.ts +5 -0
  11. package/dist/commands/logout.js +12 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/services/agent-tools.d.ts +36 -0
  15. package/dist/services/agent-tools.js +764 -0
  16. package/dist/services/agent.d.ts +21 -0
  17. package/dist/services/agent.js +648 -0
  18. package/dist/services/ai.d.ts +60 -0
  19. package/dist/services/ai.js +331 -0
  20. package/dist/services/auth.d.ts +3 -0
  21. package/dist/services/auth.js +183 -0
  22. package/dist/services/changeLogger.d.ts +23 -0
  23. package/dist/services/changeLogger.js +57 -0
  24. package/dist/services/contextAgent.d.ts +20 -0
  25. package/dist/services/contextAgent.js +440 -0
  26. package/dist/services/proxyClient.d.ts +21 -0
  27. package/dist/services/proxyClient.js +119 -0
  28. package/dist/services/verificationService.d.ts +10 -0
  29. package/dist/services/verificationService.js +148 -0
  30. package/dist/utils/atomicWrite.d.ts +6 -0
  31. package/dist/utils/atomicWrite.js +29 -0
  32. package/dist/utils/config.d.ts +17 -0
  33. package/dist/utils/config.js +17 -0
  34. package/dist/utils/contextPrompts.d.ts +3 -0
  35. package/dist/utils/contextPrompts.js +34 -0
  36. package/dist/utils/dependencyTracer.d.ts +48 -0
  37. package/dist/utils/dependencyTracer.js +647 -0
  38. package/dist/utils/excludedExtensions.d.ts +8 -0
  39. package/dist/utils/excludedExtensions.js +125 -0
  40. package/dist/utils/fuzzyMatch.d.ts +21 -0
  41. package/dist/utils/fuzzyMatch.js +121 -0
  42. package/dist/utils/logger.d.ts +8 -0
  43. package/dist/utils/logger.js +17 -0
  44. package/dist/utils/pathSecurity.d.ts +10 -0
  45. package/dist/utils/pathSecurity.js +26 -0
  46. package/dist/utils/symbolExtractor.d.ts +6 -0
  47. package/dist/utils/symbolExtractor.js +249 -0
  48. package/dist/utils/syntaxValidator.d.ts +5 -0
  49. package/dist/utils/syntaxValidator.js +81 -0
  50. package/dist/utils/systemPrompts.d.ts +5 -0
  51. package/dist/utils/systemPrompts.js +119 -0
  52. package/oclif.manifest.json +69 -0
  53. package/package.json +81 -0
@@ -0,0 +1,440 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { createContextAgentSession, createIntentRouterSession, createWebSearchAgentSession } from './ai.js';
5
+ import { listDirectory, grepSearch, readFile, traceDependencies, findRecentChanges } from './agent-tools.js';
6
+ import { debugLog } from '../utils/logger.js';
7
+ import { buildDependencyGraph } from '../utils/dependencyTracer.js';
8
+ async function detectProjectType(workspaceRoot) {
9
+ const types = [];
10
+ const fileExists = async (fileName) => {
11
+ try {
12
+ await fs.access(path.join(workspaceRoot, fileName));
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ };
19
+ // Node.js Ecosystem
20
+ if (await fileExists('package.json')) {
21
+ types.push('Node.js');
22
+ try {
23
+ const pkgJsonStr = await fs.readFile(path.join(workspaceRoot, 'package.json'), 'utf-8');
24
+ const pkgJson = JSON.parse(pkgJsonStr);
25
+ const deps = { ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) };
26
+ if (deps['@oclif/core'])
27
+ types.push('oclif CLI');
28
+ if (deps['next'])
29
+ types.push('Next.js');
30
+ if (deps['nuxt'])
31
+ types.push('Nuxt.js');
32
+ if (deps['react'])
33
+ types.push('React');
34
+ if (deps['react-native'] || deps['expo'])
35
+ types.push('React Native');
36
+ if (deps['vue'])
37
+ types.push('Vue.js');
38
+ if (deps['svelte'])
39
+ types.push('Svelte');
40
+ if (deps['@angular/core'])
41
+ types.push('Angular');
42
+ if (deps['@remix-run/react'])
43
+ types.push('Remix');
44
+ if (deps['express'])
45
+ types.push('Express');
46
+ if (deps['@nestjs/core'])
47
+ types.push('NestJS');
48
+ if (deps['vite'])
49
+ types.push('Vite');
50
+ if (deps['tailwindcss'])
51
+ types.push('Tailwind CSS');
52
+ if (deps['firebase'])
53
+ types.push('Firebase');
54
+ }
55
+ catch { }
56
+ if (await fileExists('tsconfig.json'))
57
+ types.push('TypeScript');
58
+ }
59
+ // Python Ecosystem
60
+ if (await fileExists('pyproject.toml') || await fileExists('requirements.txt') || await fileExists('Pipfile')) {
61
+ types.push('Python');
62
+ try {
63
+ const reqs = await fileExists('requirements.txt') ? await fs.readFile(path.join(workspaceRoot, 'requirements.txt'), 'utf-8') : '';
64
+ const toml = await fileExists('pyproject.toml') ? await fs.readFile(path.join(workspaceRoot, 'pyproject.toml'), 'utf-8') : '';
65
+ const combined = (reqs + toml).toLowerCase();
66
+ if (combined.includes('django'))
67
+ types.push('Django');
68
+ if (combined.includes('flask'))
69
+ types.push('Flask');
70
+ if (combined.includes('fastapi'))
71
+ types.push('FastAPI');
72
+ }
73
+ catch { }
74
+ }
75
+ // Rust
76
+ if (await fileExists('Cargo.toml'))
77
+ types.push('Rust');
78
+ // Go
79
+ if (await fileExists('go.mod'))
80
+ types.push('Go');
81
+ // Ruby
82
+ if (await fileExists('Gemfile')) {
83
+ types.push('Ruby');
84
+ try {
85
+ const gemfile = await fs.readFile(path.join(workspaceRoot, 'Gemfile'), 'utf-8');
86
+ if (gemfile.includes('rails'))
87
+ types.push('Ruby on Rails');
88
+ }
89
+ catch { }
90
+ }
91
+ // Java / Kotlin / Android
92
+ if (await fileExists('build.gradle') || await fileExists('build.gradle.kts') || await fileExists('pom.xml')) {
93
+ if (await fileExists('app/src/main/AndroidManifest.xml')) {
94
+ types.push('Android');
95
+ }
96
+ else {
97
+ types.push('Java/Kotlin');
98
+ }
99
+ }
100
+ // iOS / macOS
101
+ if (await fileExists('Package.swift') || await fileExists('Podfile')) {
102
+ types.push('Swift/iOS');
103
+ }
104
+ // PHP
105
+ if (await fileExists('composer.json')) {
106
+ types.push('PHP');
107
+ try {
108
+ const composer = await fs.readFile(path.join(workspaceRoot, 'composer.json'), 'utf-8');
109
+ if (composer.includes('laravel/framework'))
110
+ types.push('Laravel');
111
+ }
112
+ catch { }
113
+ }
114
+ // C# / .NET
115
+ try {
116
+ const files = await fs.readdir(workspaceRoot);
117
+ if (files.some(f => f.endsWith('.sln') || f.endsWith('.csproj'))) {
118
+ types.push('C# / .NET');
119
+ }
120
+ }
121
+ catch { }
122
+ // Flutter
123
+ if (await fileExists('pubspec.yaml'))
124
+ types.push('Flutter / Dart');
125
+ // Docker
126
+ if (await fileExists('Dockerfile') || await fileExists('docker-compose.yml'))
127
+ types.push('Docker');
128
+ if (types.length === 0) {
129
+ return 'Unknown Project Type';
130
+ }
131
+ return types.join(' / ');
132
+ }
133
+ export async function routeIntent(userRequest, chatHistory = '') {
134
+ try {
135
+ const session = createIntentRouterSession();
136
+ let prompt = `User Request: "${userRequest}"`;
137
+ if (chatHistory) {
138
+ prompt = `Previous Conversation Context:\n${chatHistory}\n\n${prompt}`;
139
+ }
140
+ const result = await session.sendMessage(prompt);
141
+ const text = result.response.text()?.trim() || '{}';
142
+ const parsed = JSON.parse(text);
143
+ debugLog(`Intent Router Parsed: ${JSON.stringify(parsed)}`);
144
+ return {
145
+ needsContext: parsed.context === 'SEARCH',
146
+ targetAgent: parsed.agent === 'CHAT' ? 'CHAT' : 'EXECUTE',
147
+ };
148
+ }
149
+ catch (e) {
150
+ debugLog(`Intent Router failed to parse JSON, falling back to EXECUTE. Error: ${String(e)}`);
151
+ // Fallback to searching if the router fails
152
+ return { needsContext: true, targetAgent: 'EXECUTE' };
153
+ }
154
+ }
155
+ export async function gatherContext(workspaceRoot, userRequest, chatHistory = '', inputHandler, abortSignal, onProgress) {
156
+ // Always skip slash commands for zero latency
157
+ if (userRequest.startsWith('/')) {
158
+ return { contextResult: null, targetAgent: 'EXECUTE', chainedMessages: [] };
159
+ }
160
+ // Use the AI Intent Router to decide if we need to search
161
+ const { needsContext, targetAgent } = await routeIntent(userRequest, chatHistory);
162
+ debugLog(`GatherContext Route: needsContext=${needsContext}, targetAgent=${targetAgent}`);
163
+ if (!needsContext) {
164
+ return { contextResult: null, targetAgent, chainedMessages: [] };
165
+ }
166
+ const projectTreeResult = await listDirectory(workspaceRoot, '.', 10);
167
+ let projectTree = projectTreeResult.output;
168
+ if (projectTree.length > 30000) {
169
+ projectTree = projectTree.substring(0, 30000) + '\n... (Project tree truncated due to size)';
170
+ }
171
+ const projectType = await detectProjectType(workspaceRoot);
172
+ const session = createContextAgentSession();
173
+ const relevantFiles = new Map();
174
+ let summary = 'No relevant context found.';
175
+ let webSearchSummary = '';
176
+ let chainedMessages = [];
177
+ // Initial prompt
178
+ let currentMessage = `User Request: "${userRequest}"\n\nProject Type: ${projectType}\n\nProject Structure:\n${projectTree}`;
179
+ if (chatHistory) {
180
+ currentMessage = `Previous Conversation Context:\n${chatHistory}\n\n` + currentMessage;
181
+ }
182
+ currentMessage += `\n\nStart investigating to find relevant files.`;
183
+ const MAX_TURNS = 8; // From Tier 3 budget
184
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
185
+ await inputHandler.waitForPrompt();
186
+ const queuedMsg = inputHandler.getAndClear();
187
+ let additionalText = undefined;
188
+ if (queuedMsg) {
189
+ chainedMessages.push(queuedMsg);
190
+ additionalText = `[USER INTERRUPTION] The user sent the following message during your investigation:\n"${queuedMsg}"\n\nPlease incorporate this into your investigation. Adjust your summary to account for this, but do not completely lose focus on your original goal.`;
191
+ if (onProgress)
192
+ onProgress(`Forwarding queued message to AI...`);
193
+ else
194
+ console.log(pc.dim(` [Context Agent] Forwarding queued message to AI...`));
195
+ }
196
+ let result;
197
+ try {
198
+ result = await session.sendMessage(currentMessage, additionalText, abortSignal);
199
+ }
200
+ catch (e) {
201
+ if (e.name === 'AbortError' || e.message?.includes('abort')) {
202
+ break;
203
+ }
204
+ throw e;
205
+ }
206
+ const grounding = result.response.groundingMetadata?.();
207
+ if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
208
+ const searchMsg = `Google Search: ${grounding.webSearchQueries.map((q) => `"${q}"`).join(', ')}`;
209
+ if (onProgress)
210
+ onProgress(searchMsg);
211
+ else
212
+ console.log(pc.dim(` [Context Agent] ${searchMsg}`));
213
+ }
214
+ const functionCalls = result.response.functionCalls();
215
+ if (!functionCalls || functionCalls.length === 0) {
216
+ // Model returned text instead of calling finish_investigation, just take the text as summary
217
+ summary = result.response.text();
218
+ break;
219
+ }
220
+ let isFinished = false;
221
+ const functionResponses = [];
222
+ for (const call of functionCalls) {
223
+ const args = call.args;
224
+ let logMsg = ` [Context Agent] Executing ${call.name}`;
225
+ if (call.name === 'finish_investigation') {
226
+ const filesToRead = args.relevantFiles || [];
227
+ logMsg = ` [Context Agent] Finished investigation (Selected ${filesToRead.length} files)`;
228
+ }
229
+ else if (call.name === 'select_files') {
230
+ const filesToRead = args.files || [];
231
+ logMsg = ` [Context Agent] Selected ${filesToRead.length} files to read`;
232
+ }
233
+ else if (call.name === 'search_codebase') {
234
+ logMsg = ` [Context Agent] Searching codebase for: "${args.pattern}"`;
235
+ }
236
+ else if (call.name === 'read_file') {
237
+ const range = args.startLine || args.endLine ? ` (lines ${args.startLine || 1}-${args.endLine || 'end'})` : '';
238
+ const targets = args.targetElements ? ` [elements: ${args.targetElements.join(', ')}]` : '';
239
+ logMsg = ` [Context Agent] Reading file: ${args.filePath}${range}${targets}`;
240
+ }
241
+ else if (call.name === 'list_directory') {
242
+ logMsg = ` [Context Agent] Listing directory: ${args.dirPath}`;
243
+ }
244
+ else if (call.name === 'perform_web_search') {
245
+ logMsg = ` [Context Agent] Searching the web for: "${args.query}"`;
246
+ }
247
+ else if (call.name === 'find_dependencies') {
248
+ logMsg = ` [Context Agent] Tracing dependencies for: ${args.filePath}`;
249
+ }
250
+ else if (call.name === 'find_recent_changes') {
251
+ logMsg = ` [Context Agent] Looking for recently modified files`;
252
+ }
253
+ if (onProgress) {
254
+ onProgress(logMsg.trim().replace(/^\\[Context Agent\\] /, ''));
255
+ }
256
+ else {
257
+ console.log(pc.dim(logMsg));
258
+ }
259
+ if (call.name === 'finish_investigation') {
260
+ summary = args.summary || '';
261
+ const filesToRead = args.relevantFiles || [];
262
+ debugLog(`Context Agent finished. Selected files: ${JSON.stringify(filesToRead)}`);
263
+ for (const filePath of filesToRead) {
264
+ if (!relevantFiles.has(filePath)) {
265
+ const readResult = await readFile(workspaceRoot, filePath);
266
+ if (!readResult.error) {
267
+ relevantFiles.set(filePath, readResult.output);
268
+ }
269
+ }
270
+ }
271
+ isFinished = true;
272
+ // ── Auto-trace reverse dependencies ──
273
+ // When the Context Agent finalizes its investigation, we automatically
274
+ // discover files that DEPEND ON the selected files. This ensures the
275
+ // Execution Agent won't break imports when modifying/deleting/renaming.
276
+ const MAX_TOTAL_FILES = 15;
277
+ try {
278
+ const graph = await buildDependencyGraph(workspaceRoot);
279
+ const autoDiscovered = new Set();
280
+ for (const filePath of filesToRead) {
281
+ const reverseDeps = graph.getImportedBy(filePath);
282
+ for (const dep of reverseDeps) {
283
+ if (!filesToRead.includes(dep) && !autoDiscovered.has(dep)) {
284
+ autoDiscovered.add(dep);
285
+ }
286
+ }
287
+ }
288
+ // Merge auto-discovered dependents, respecting the file cap
289
+ const remaining = MAX_TOTAL_FILES - relevantFiles.size;
290
+ let added = 0;
291
+ for (const dep of autoDiscovered) {
292
+ if (added >= remaining)
293
+ break;
294
+ if (!relevantFiles.has(dep)) {
295
+ const readResult = await readFile(workspaceRoot, dep);
296
+ if (!readResult.error) {
297
+ relevantFiles.set(dep, readResult.output);
298
+ added++;
299
+ }
300
+ }
301
+ }
302
+ if (added > 0) {
303
+ const depMsg = `Auto-traced ${added} reverse dependent(s) into context`;
304
+ if (onProgress)
305
+ onProgress(depMsg);
306
+ else
307
+ console.log(pc.dim(` [Context Agent] ${depMsg}`));
308
+ }
309
+ }
310
+ catch {
311
+ // Dependency tracing is best-effort — don't block investigation
312
+ }
313
+ functionResponses.push({
314
+ functionResponse: {
315
+ name: call.name,
316
+ response: { output: 'Investigation finished.' },
317
+ },
318
+ });
319
+ break;
320
+ }
321
+ else if (call.name === 'select_files') {
322
+ const filesToRead = args.files || [];
323
+ let output = '';
324
+ for (const filePath of filesToRead) {
325
+ const readResult = await readFile(workspaceRoot, filePath);
326
+ if (!readResult.error) {
327
+ relevantFiles.set(filePath, readResult.output);
328
+ output += `\n--- File: ${filePath} ---\n${readResult.output}\n`;
329
+ }
330
+ else {
331
+ output += `\n--- File: ${filePath} ---\nError: ${readResult.error}\n`;
332
+ }
333
+ }
334
+ functionResponses.push({
335
+ functionResponse: {
336
+ name: call.name,
337
+ response: { output: output || 'No files read.' },
338
+ },
339
+ });
340
+ }
341
+ else if (call.name === 'list_directory') {
342
+ const listRes = await listDirectory(workspaceRoot, args.dirPath, args.maxDepth || 1);
343
+ functionResponses.push({
344
+ functionResponse: {
345
+ name: call.name,
346
+ response: {
347
+ output: listRes.output,
348
+ ...(listRes.error ? { error: listRes.error } : {}),
349
+ },
350
+ },
351
+ });
352
+ }
353
+ else if (call.name === 'search_codebase') {
354
+ const grepRes = await grepSearch(workspaceRoot, args.pattern, args.fileGlob);
355
+ functionResponses.push({
356
+ functionResponse: {
357
+ name: call.name,
358
+ response: { output: grepRes.error ? grepRes.error : grepRes.output },
359
+ },
360
+ });
361
+ }
362
+ else if (call.name === 'read_file') {
363
+ const readRes = await readFile(workspaceRoot, args.filePath, args.startLine, args.endLine, args.targetElements);
364
+ if (!readRes.error) {
365
+ relevantFiles.set(args.filePath, readRes.output);
366
+ }
367
+ functionResponses.push({
368
+ functionResponse: {
369
+ name: call.name,
370
+ response: { output: readRes.error ? readRes.error : readRes.output },
371
+ },
372
+ });
373
+ }
374
+ else if (call.name === 'perform_web_search') {
375
+ try {
376
+ const webSession = createWebSearchAgentSession();
377
+ const webResult = await webSession.sendMessage(`Please search the web for the following query and summarize your findings:\n"${args.query}"`);
378
+ const grounding = webResult.response.groundingMetadata?.();
379
+ if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
380
+ const webMsg = `Web Search: ${grounding.webSearchQueries.map((q) => `"${q}"`).join(', ')}`;
381
+ if (onProgress)
382
+ onProgress(webMsg);
383
+ else
384
+ console.log(pc.dim(` [Web Search] ${webMsg}`));
385
+ }
386
+ const searchSummary = webResult.response.text()?.trim() || 'No relevant information found.';
387
+ webSearchSummary += `\nQuery: ${args.query}\nFindings:\n${searchSummary}\n`;
388
+ functionResponses.push({
389
+ functionResponse: {
390
+ name: call.name,
391
+ response: { output: searchSummary },
392
+ },
393
+ });
394
+ }
395
+ catch (e) {
396
+ functionResponses.push({
397
+ functionResponse: {
398
+ name: call.name,
399
+ response: { error: e.message || 'Failed to search the web' },
400
+ },
401
+ });
402
+ }
403
+ }
404
+ else if (call.name === 'find_dependencies') {
405
+ const depResult = await traceDependencies(workspaceRoot, args.filePath, args.direction, args.maxDepth);
406
+ functionResponses.push({
407
+ functionResponse: {
408
+ name: call.name,
409
+ response: { output: depResult.error ? depResult.error : depResult.output },
410
+ },
411
+ });
412
+ }
413
+ else if (call.name === 'find_recent_changes') {
414
+ const recentRes = await findRecentChanges(workspaceRoot, args.dirPath, args.minutes, args.maxDepth);
415
+ functionResponses.push({
416
+ functionResponse: {
417
+ name: call.name,
418
+ response: { output: recentRes.error ? recentRes.error : recentRes.output },
419
+ },
420
+ });
421
+ }
422
+ }
423
+ if (isFinished) {
424
+ break;
425
+ }
426
+ // Prepare next turn
427
+ currentMessage = functionResponses;
428
+ }
429
+ return {
430
+ contextResult: {
431
+ projectTree,
432
+ projectType,
433
+ relevantFiles,
434
+ summary,
435
+ webSearchSummary,
436
+ },
437
+ targetAgent,
438
+ chainedMessages,
439
+ };
440
+ }
@@ -0,0 +1,21 @@
1
+ import type { Content, Tool, ToolConfig, FunctionCall } from '@google/generative-ai';
2
+ export interface ProxyUsageMetadata {
3
+ promptTokens: number;
4
+ candidatesTokens: number;
5
+ cachedTokens?: number;
6
+ creditsUsed: number;
7
+ remainingBalance: number;
8
+ }
9
+ export declare class ProxyClient {
10
+ private readonly PROXY_URL;
11
+ generateFunctionCallViaProxy(idToken: string, modelName: string, contents: Content[], tools?: Tool[], toolConfig?: ToolConfig, systemInstruction?: string | Content, generationConfig?: any, streamCallbacks?: {
12
+ onChunk: (chunk: string) => void;
13
+ }, abortSignal?: AbortSignal): Promise<{
14
+ functionCall: FunctionCall | null;
15
+ functionCalls: FunctionCall[];
16
+ thought?: string;
17
+ parts?: any[];
18
+ usageMetadata?: ProxyUsageMetadata;
19
+ groundingMetadata?: any;
20
+ }>;
21
+ }
@@ -0,0 +1,119 @@
1
+ export class ProxyClient {
2
+ PROXY_URL = 'https://generatecontent-6obg3e4zwa-uc.a.run.app';
3
+ async generateFunctionCallViaProxy(idToken, modelName, contents, tools, toolConfig, systemInstruction, generationConfig, streamCallbacks, abortSignal) {
4
+ const response = await fetch(this.PROXY_URL, {
5
+ method: 'POST',
6
+ headers: {
7
+ 'Content-Type': 'application/json',
8
+ 'X-Firebase-Auth': `Bearer ${idToken}`,
9
+ },
10
+ body: JSON.stringify({
11
+ model: modelName,
12
+ contents,
13
+ tools,
14
+ toolConfig,
15
+ systemInstruction,
16
+ generationConfig,
17
+ }),
18
+ signal: abortSignal,
19
+ });
20
+ if (response.status === 401) {
21
+ let details = '';
22
+ try {
23
+ const text = await response.text();
24
+ try {
25
+ const errorData = JSON.parse(text);
26
+ details = errorData.details || errorData.error || text;
27
+ }
28
+ catch (e) {
29
+ details = text;
30
+ }
31
+ }
32
+ catch (e) {
33
+ details = 'Unknown error reading body';
34
+ }
35
+ throw new Error(`Authentication failed: ${details}. Please login again.`);
36
+ }
37
+ if (response.status === 402) {
38
+ throw new Error('Insufficient credits. Please visit minovativemind.dev to purchase more credits.');
39
+ }
40
+ if (!response.ok) {
41
+ const errorData = await response.json().catch(() => ({}));
42
+ throw new Error(`Proxy error ${response.status}: ${errorData.error || response.statusText}`);
43
+ }
44
+ if (!response.body) {
45
+ throw new Error('No response body received from proxy');
46
+ }
47
+ // Node 18+ fetch body is a ReadableStream which is async iterable but type might mismatch.
48
+ // Let's read chunks manually
49
+ const reader = response.body.getReader();
50
+ const decoder = new TextDecoder();
51
+ let buffer = '';
52
+ let functionCall = null;
53
+ const functionCalls = [];
54
+ let thought = '';
55
+ let parts = undefined;
56
+ let usageMetadata = undefined;
57
+ let groundingMetadata = undefined;
58
+ try {
59
+ while (true) {
60
+ const { done, value } = await reader.read();
61
+ if (done)
62
+ break;
63
+ buffer += decoder.decode(value, { stream: true });
64
+ const lines = buffer.split('\n\n');
65
+ buffer = lines.pop() || '';
66
+ for (const line of lines) {
67
+ if (!line.startsWith('data: '))
68
+ continue;
69
+ const dataStr = line.slice(6);
70
+ try {
71
+ const data = JSON.parse(dataStr);
72
+ if (data.type === 'functionCall' && data.functionCall) {
73
+ functionCall = data.functionCall;
74
+ functionCalls.push(data.functionCall);
75
+ }
76
+ else if (data.type === 'thought' && data.thought) {
77
+ thought += data.thought;
78
+ if (streamCallbacks?.onChunk)
79
+ streamCallbacks.onChunk(data.thought);
80
+ }
81
+ else if (data.type === 'chunk' && data.text) {
82
+ thought += data.text;
83
+ if (streamCallbacks?.onChunk)
84
+ streamCallbacks.onChunk(data.text);
85
+ }
86
+ else if (data.type === 'parts' && data.parts) {
87
+ parts = data.parts;
88
+ }
89
+ else if (data.type === 'done') {
90
+ if (data.usage) {
91
+ usageMetadata = data.usage;
92
+ }
93
+ if (data.groundingMetadata) {
94
+ groundingMetadata = data.groundingMetadata;
95
+ }
96
+ }
97
+ else if (data.type === 'error') {
98
+ throw new Error(`Proxy generation error: ${data.message}`);
99
+ }
100
+ }
101
+ catch (parseError) {
102
+ console.error('Failed to parse SSE data:', dataStr, parseError);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ finally {
108
+ reader.releaseLock();
109
+ }
110
+ return {
111
+ functionCall,
112
+ functionCalls,
113
+ thought: thought.trim(),
114
+ parts,
115
+ usageMetadata,
116
+ groundingMetadata,
117
+ };
118
+ }
119
+ }
@@ -0,0 +1,10 @@
1
+ export interface VerificationResult {
2
+ success: boolean;
3
+ command: string;
4
+ output: string;
5
+ errors: string[];
6
+ }
7
+ export declare function detectVerificationCommand(workspaceRoot: string): Promise<string | null>;
8
+ export declare function runVerification(workspaceRoot: string): Promise<VerificationResult | null>;
9
+ export declare function formatVerificationForModel(result: VerificationResult): string;
10
+ export declare function verifyChangedFiles(workspaceRoot: string, filePaths: string[]): Promise<string | null>;