vibecodingmachine-core 2026.3.9-907 → 2026.3.10-1547

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/auth/access-denied.html +119 -119
  3. package/src/auth/shared-auth-storage.js +267 -267
  4. package/src/autonomous-mode/feature-implementer.cjs +70 -70
  5. package/src/autonomous-mode/feature-implementer.js +425 -425
  6. package/src/beta-request.js +160 -160
  7. package/src/chat-management/chat-manager.cjs +71 -71
  8. package/src/chat-management/chat-manager.js +342 -342
  9. package/src/compliance/compliance-prompt.js +183 -183
  10. package/src/ide-integration/aider-cli-manager.cjs +850 -850
  11. package/src/ide-integration/applescript-manager.cjs +3215 -3215
  12. package/src/ide-integration/applescript-utils.js +314 -314
  13. package/src/ide-integration/cdp-manager.cjs +221 -221
  14. package/src/ide-integration/claude-code-cli-manager.cjs +456 -456
  15. package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
  16. package/src/ide-integration/continue-cli-manager.js +431 -431
  17. package/src/ide-integration/provider-manager.cjs +595 -595
  18. package/src/ide-integration/quota-detector.cjs +399 -399
  19. package/src/ide-integration/windows-automation-manager.js +532 -4
  20. package/src/ide-integration/windows-ide-manager.js +12 -3
  21. package/src/index.cjs +142 -142
  22. package/src/llm/direct-llm-manager.cjs +1299 -1299
  23. package/src/localization/index.js +147 -147
  24. package/src/quota-management/index.js +108 -108
  25. package/src/requirement-numbering.js +164 -164
  26. package/src/sync/aws-setup.js +445 -445
  27. package/src/ui/ButtonComponents.js +247 -247
  28. package/src/ui/ChatInterface.js +499 -499
  29. package/src/ui/StateManager.js +259 -259
  30. package/src/utils/audit-logger.cjs +116 -116
  31. package/src/utils/config-helpers.cjs +94 -94
  32. package/src/utils/config-helpers.js +94 -94
  33. package/src/utils/env-helpers.js +54 -54
  34. package/src/utils/error-reporter.js +117 -117
  35. package/src/utils/gcloud-auth.cjs +394 -394
  36. package/src/utils/git-branch-manager.js +278 -278
  37. package/src/utils/logger.cjs +193 -193
  38. package/src/utils/logger.js +191 -191
  39. package/src/utils/repo-helpers.cjs +120 -120
  40. package/src/utils/repo-helpers.js +120 -120
  41. package/src/utils/update-checker.js +246 -246
  42. package/src/utils/version-checker.js +170 -170
@@ -1,1299 +1,1299 @@
1
- /**
2
- * Direct LLM API Manager - Call LLM APIs directly without IDE CLI tools
3
- * Supports: Ollama (local), Anthropic, Groq, AWS Bedrock
4
- */
5
-
6
- const https = require('https');
7
- const http = require('http');
8
- const quotaManagement = require('../quota-management');
9
-
10
- class DirectLLMManager {
11
- constructor(sharedProviderManager = null) {
12
- this.logger = console;
13
- // Use shared ProviderManager if provided, otherwise create new instance
14
- // IMPORTANT: Pass shared instance to maintain rate limit state across calls
15
- if (sharedProviderManager) {
16
- this.providerManager = sharedProviderManager;
17
- } else {
18
- try {
19
- const ProviderManager = require('../ide-integration/provider-manager.cjs');
20
- this.providerManager = new ProviderManager();
21
- } catch (err) {
22
- this.providerManager = null;
23
- }
24
- }
25
- }
26
-
27
- /**
28
- * Detect and save rate limit from error message
29
- * @param {string} provider - Provider name
30
- * @param {string} model - Model name
31
- * @param {string} errorMessage - Error message from API
32
- */
33
- detectAndSaveRateLimit(provider, model, errorMessage) {
34
- if (!this.providerManager) return;
35
-
36
- // Check for rate limit indicators
37
- const isRateLimit = (errorMessage.includes('rate limit') ||
38
- errorMessage.includes('Rate limit') ||
39
- errorMessage.includes('too many requests') ||
40
- errorMessage.includes('429') ||
41
- errorMessage.includes('quota') ||
42
- errorMessage.includes('Weekly limit reached') ||
43
- errorMessage.includes('Daily limit reached') ||
44
- errorMessage.includes('limit reached')) &&
45
- !errorMessage.startsWith('Quota limit reached'); // Don't re-mark our own internal exceeded messages
46
-
47
- if (isRateLimit) {
48
- this.providerManager.markRateLimited(provider, model, errorMessage);
49
- }
50
- }
51
-
52
- /**
53
- * Call Ollama API directly (local)
54
- * @param {string} model - Model name (e.g., "qwen2.5-coder:32b")
55
- * @param {string} prompt - Prompt to send
56
- * @param {Object} options - Options (onChunk, onComplete, onError)
57
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
58
- */
59
- async callOllama(model, prompt, options = {}) {
60
- const { onChunk, onComplete, onError, temperature = 0.2 } = options;
61
-
62
- return new Promise((resolve) => {
63
- let fullResponse = '';
64
-
65
- const postData = JSON.stringify({
66
- model: model,
67
- prompt: prompt,
68
- stream: true,
69
- options: {
70
- temperature: temperature
71
- }
72
- });
73
-
74
- const req = http.request({
75
- hostname: 'localhost',
76
- port: 11434,
77
- path: '/api/generate',
78
- method: 'POST',
79
- headers: {
80
- 'Content-Type': 'application/json',
81
- 'Content-Length': Buffer.byteLength(postData)
82
- }
83
- }, (res) => {
84
- let buffer = '';
85
-
86
- res.on('data', (chunk) => {
87
- buffer += chunk.toString();
88
- const lines = buffer.split('\n');
89
- buffer = lines.pop(); // Keep incomplete line in buffer
90
-
91
- for (const line of lines) {
92
- if (!line.trim()) continue;
93
-
94
- try {
95
- const data = JSON.parse(line);
96
- if (data.response) {
97
- fullResponse += data.response;
98
- if (onChunk) onChunk(data.response);
99
- }
100
-
101
- if (data.done) {
102
- if (onComplete) onComplete(fullResponse);
103
- resolve({
104
- success: true,
105
- response: fullResponse,
106
- model: data.model,
107
- context: data.context
108
- });
109
- }
110
- } catch (err) {
111
- // Ignore JSON parse errors for partial chunks
112
- }
113
- }
114
- });
115
-
116
- res.on('end', () => {
117
- if (!fullResponse) {
118
- const error = 'No response received from Ollama';
119
- if (onError) onError(error);
120
- resolve({ success: false, error });
121
- }
122
- });
123
- });
124
-
125
- req.on('error', (error) => {
126
- const errorMsg = `Ollama API error: ${error.message}`;
127
- if (onError) onError(errorMsg);
128
- resolve({ success: false, error: errorMsg });
129
- });
130
-
131
- req.write(postData);
132
- req.end();
133
- });
134
- }
135
-
136
- /**
137
- * Call Anthropic API directly
138
- * @param {string} model - Model name (e.g., "claude-sonnet-4-20250514")
139
- * @param {string} prompt - Prompt to send
140
- * @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
141
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
142
- */
143
- async callAnthropic(model, prompt, options = {}) {
144
- const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
145
-
146
- if (!apiKey) {
147
- const error = 'Anthropic API key required';
148
- if (onError) onError(error);
149
- return { success: false, error };
150
- }
151
-
152
- return new Promise((resolve) => {
153
- let fullResponse = '';
154
-
155
- const postData = JSON.stringify({
156
- model: model,
157
- max_tokens: maxTokens,
158
- temperature: temperature,
159
- messages: [
160
- { role: 'user', content: prompt }
161
- ],
162
- stream: true
163
- });
164
-
165
- const req = https.request({
166
- hostname: 'api.anthropic.com',
167
- path: '/v1/messages',
168
- method: 'POST',
169
- headers: {
170
- 'Content-Type': 'application/json',
171
- 'x-api-key': apiKey,
172
- 'anthropic-version': '2023-06-01',
173
- 'Content-Length': Buffer.byteLength(postData)
174
- }
175
- }, (res) => {
176
- let buffer = '';
177
-
178
- res.on('data', (chunk) => {
179
- buffer += chunk.toString();
180
- const lines = buffer.split('\n');
181
- buffer = lines.pop();
182
-
183
- for (const line of lines) {
184
- if (!line.trim() || !line.startsWith('data: ')) continue;
185
-
186
- try {
187
- const jsonStr = line.slice(6); // Remove "data: " prefix
188
- if (jsonStr === '[DONE]') continue;
189
-
190
- const data = JSON.parse(jsonStr);
191
-
192
- if (data.type === 'content_block_delta' && data.delta?.text) {
193
- fullResponse += data.delta.text;
194
- if (onChunk) onChunk(data.delta.text);
195
- } else if (data.type === 'message_stop') {
196
- if (onComplete) onComplete(fullResponse);
197
- resolve({
198
- success: true,
199
- response: fullResponse,
200
- model: model
201
- });
202
- }
203
- } catch (err) {
204
- // Ignore JSON parse errors
205
- }
206
- }
207
- });
208
-
209
- res.on('end', () => {
210
- if (!fullResponse) {
211
- const error = 'No response received from Anthropic';
212
- if (onError) onError(error);
213
- resolve({ success: false, error });
214
- }
215
- });
216
- });
217
-
218
- req.on('error', (error) => {
219
- const errorMsg = `Anthropic API error: ${error.message}`;
220
- if (onError) onError(errorMsg);
221
- resolve({ success: false, error: errorMsg });
222
- });
223
-
224
- req.write(postData);
225
- req.end();
226
- });
227
- }
228
-
229
- /**
230
- * Call Groq API directly
231
- * @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
232
- * @param {string} prompt - Prompt to send
233
- * @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
234
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
235
- */
236
- async callGroq(model, prompt, options = {}) {
237
- const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
238
-
239
- if (!apiKey) {
240
- const error = 'Groq API key required';
241
- if (onError) onError(error);
242
- return { success: false, error };
243
- }
244
-
245
- return new Promise((resolve) => {
246
- let fullResponse = '';
247
-
248
- const postData = JSON.stringify({
249
- model: model,
250
- messages: [
251
- { role: 'user', content: prompt }
252
- ],
253
- temperature: temperature,
254
- max_tokens: maxTokens,
255
- stream: true
256
- });
257
-
258
- const req = https.request({
259
- hostname: 'api.groq.com',
260
- path: '/openai/v1/chat/completions',
261
- method: 'POST',
262
- headers: {
263
- 'Content-Type': 'application/json',
264
- 'Authorization': `Bearer ${apiKey}`,
265
- 'Content-Length': Buffer.byteLength(postData)
266
- }
267
- }, (res) => {
268
- let buffer = '';
269
- let statusCode = res.statusCode;
270
-
271
- // Check for rate limit or error status codes
272
- if (statusCode === 429 || statusCode >= 400) {
273
- let errorBody = '';
274
- res.on('data', (chunk) => {
275
- errorBody += chunk.toString();
276
- });
277
- res.on('end', () => {
278
- const errorMsg = `Groq API error (${statusCode}): ${errorBody || 'No error details'}`;
279
- this.detectAndSaveRateLimit('groq', model, errorMsg);
280
- if (onError) onError(errorMsg);
281
- resolve({ success: false, error: errorMsg });
282
- });
283
- return;
284
- }
285
-
286
- res.on('data', (chunk) => {
287
- buffer += chunk.toString();
288
- const lines = buffer.split('\n');
289
- buffer = lines.pop();
290
-
291
- for (const line of lines) {
292
- if (!line.trim() || !line.startsWith('data: ')) continue;
293
-
294
- try {
295
- const jsonStr = line.slice(6);
296
- if (jsonStr === '[DONE]') {
297
- if (onComplete) onComplete(fullResponse);
298
- resolve({
299
- success: true,
300
- response: fullResponse,
301
- model: model
302
- });
303
- return;
304
- }
305
-
306
- const data = JSON.parse(jsonStr);
307
- const content = data.choices?.[0]?.delta?.content;
308
-
309
- if (content) {
310
- fullResponse += content;
311
- if (onChunk) onChunk(content);
312
- }
313
- } catch (err) {
314
- // Ignore JSON parse errors
315
- }
316
- }
317
- });
318
-
319
- res.on('end', () => {
320
- if (fullResponse) {
321
- if (onComplete) onComplete(fullResponse);
322
- resolve({ success: true, response: fullResponse, model });
323
- } else {
324
- const error = buffer || 'No response received from Groq';
325
- this.detectAndSaveRateLimit('groq', model, error);
326
- if (onError) onError(error);
327
- resolve({ success: false, error });
328
- }
329
- });
330
- });
331
-
332
- req.on('error', (error) => {
333
- const errorMsg = `Groq API error: ${error.message}`;
334
- this.detectAndSaveRateLimit('groq', model, errorMsg);
335
- if (onError) onError(errorMsg);
336
- resolve({ success: false, error: errorMsg });
337
- });
338
-
339
- req.write(postData);
340
- req.end();
341
- });
342
- }
343
-
344
- /**
345
- * Call AWS Bedrock API directly
346
- * @param {string} model - Model ID (e.g., "anthropic.claude-sonnet-4-v1")
347
- * @param {string} prompt - Prompt to send
348
- * @param {Object} options - Options (region, accessKeyId, secretAccessKey, onChunk, onComplete, onError)
349
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
350
- */
351
- async callBedrock(model, prompt, options = {}) {
352
- const { region, accessKeyId, secretAccessKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
353
-
354
- if (!region || !accessKeyId || !secretAccessKey) {
355
- const error = 'AWS credentials required (region, accessKeyId, secretAccessKey)';
356
- if (onError) onError(error);
357
- return { success: false, error };
358
- }
359
-
360
- try {
361
- // Use AWS SDK v3 for Bedrock
362
- const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
363
-
364
- const client = new BedrockRuntimeClient({
365
- region: region,
366
- credentials: {
367
- accessKeyId: accessKeyId,
368
- secretAccessKey: secretAccessKey
369
- }
370
- });
371
-
372
- // Format request based on model provider
373
- let requestBody;
374
- if (model.startsWith('anthropic.')) {
375
- requestBody = {
376
- anthropic_version: 'bedrock-2023-05-31',
377
- max_tokens: maxTokens,
378
- temperature: temperature,
379
- messages: [
380
- { role: 'user', content: prompt }
381
- ]
382
- };
383
- } else if (model.startsWith('meta.')) {
384
- requestBody = {
385
- prompt: prompt,
386
- temperature: temperature,
387
- max_gen_len: maxTokens
388
- };
389
- } else {
390
- return { success: false, error: `Unsupported Bedrock model: ${model}` };
391
- }
392
-
393
- const command = new InvokeModelWithResponseStreamCommand({
394
- modelId: model,
395
- contentType: 'application/json',
396
- accept: 'application/json',
397
- body: JSON.stringify(requestBody)
398
- });
399
-
400
- const response = await client.send(command);
401
- let fullResponse = '';
402
-
403
- for await (const event of response.body) {
404
- if (event.chunk) {
405
- const chunk = JSON.parse(new TextDecoder().decode(event.chunk.bytes));
406
-
407
- let text = '';
408
- if (chunk.delta?.text) {
409
- text = chunk.delta.text; // Anthropic format
410
- } else if (chunk.generation) {
411
- text = chunk.generation; // Meta Llama format
412
- }
413
-
414
- if (text) {
415
- fullResponse += text;
416
- if (onChunk) onChunk(text);
417
- }
418
- }
419
- }
420
-
421
- if (onComplete) onComplete(fullResponse);
422
- return { success: true, response: fullResponse, model };
423
-
424
- } catch (error) {
425
- const errorMsg = `AWS Bedrock error: ${error.message}`;
426
- if (onError) onError(errorMsg);
427
- return { success: false, error: errorMsg };
428
- }
429
- }
430
-
431
- /**
432
- * Call Claude Code CLI
433
- * @param {string} model - Model name (ignored, uses Claude Pro subscription)
434
- * @param {string} prompt - Prompt to send
435
- * @param {Object} options - Options (onChunk, onComplete, onError)
436
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
437
- */
438
- async callClaudeCode(model, prompt, options = {}) {
439
- const { onChunk, onComplete, onError } = options;
440
- const { spawn } = require('child_process');
441
-
442
- return new Promise((resolve) => {
443
- let fullResponse = '';
444
- let errorOutput = '';
445
-
446
- // Call claude CLI with the prompt
447
- const claude = spawn('claude', ['--dangerously-skip-permissions'], {
448
- stdio: ['pipe', 'pipe', 'pipe']
449
- });
450
-
451
- // Send prompt to stdin
452
- claude.stdin.write(prompt);
453
- claude.stdin.end();
454
-
455
- // Capture stdout
456
- claude.stdout.on('data', (data) => {
457
- const chunk = data.toString();
458
- fullResponse += chunk;
459
- if (onChunk) onChunk(chunk);
460
- });
461
-
462
- // Capture stderr
463
- claude.stderr.on('data', (data) => {
464
- errorOutput += data.toString();
465
- });
466
-
467
- // Handle completion
468
- claude.on('close', (code) => {
469
- if (code === 0) {
470
- if (onComplete) onComplete(fullResponse);
471
- resolve({ success: true, response: fullResponse });
472
- } else {
473
- const error = `Claude CLI exited with code ${code}: ${errorOutput}`;
474
- if (onError) onError(error);
475
- // Check for rate limits
476
- this.detectAndSaveRateLimit('claude-code', 'claude-code-cli', errorOutput);
477
- resolve({ success: false, error });
478
- }
479
- });
480
-
481
- // Handle spawn errors
482
- claude.on('error', (err) => {
483
- const error = `Failed to start Claude CLI: ${err.message}`;
484
- if (onError) onError(error);
485
- resolve({ success: false, error });
486
- });
487
- });
488
- }
489
-
490
- /**
491
- * Call the Cline CLI with a prompt via stdin
492
- */
493
- async callCline(model, prompt, options = {}) {
494
- const { onChunk, onComplete, onError } = options;
495
- const { spawn } = require('child_process');
496
-
497
- return new Promise((resolve) => {
498
- let fullResponse = '';
499
- let errorOutput = '';
500
-
501
- const cline = spawn('cline', ['--dangerously-skip-permissions'], {
502
- stdio: ['pipe', 'pipe', 'pipe']
503
- });
504
-
505
- cline.stdin.write(prompt);
506
- cline.stdin.end();
507
-
508
- cline.stdout.on('data', (data) => {
509
- const chunk = data.toString();
510
- fullResponse += chunk;
511
- if (onChunk) onChunk(chunk);
512
- });
513
-
514
- cline.stderr.on('data', (data) => {
515
- errorOutput += data.toString();
516
- });
517
-
518
- cline.on('close', (code) => {
519
- if (code === 0) {
520
- if (onComplete) onComplete(fullResponse);
521
- resolve({ success: true, response: fullResponse });
522
- } else {
523
- const error = `Cline CLI exited with code ${code}: ${errorOutput}`;
524
- if (onError) onError(error);
525
- this.detectAndSaveRateLimit('cline', 'cline-cli', errorOutput);
526
- resolve({ success: false, error });
527
- }
528
- });
529
-
530
- cline.on('error', (err) => {
531
- const error = `Failed to start Cline CLI: ${err.message}`;
532
- if (onError) onError(error);
533
- resolve({ success: false, error });
534
- });
535
- });
536
- }
537
-
538
- /**
539
- * Check if Cline CLI is available
540
- */
541
- async isClineAvailable() {
542
- const { spawn } = require('child_process');
543
- return new Promise((resolve) => {
544
- const proc = spawn('cline', ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
545
- proc.on('close', (code) => resolve(code === 0));
546
- proc.on('error', () => resolve(false));
547
- setTimeout(() => { proc.kill(); resolve(false); }, 2000);
548
- });
549
- }
550
-
551
- /**
552
- * Call the OpenCode CLI with a prompt via -p flag
553
- */
554
- async callOpenCode(model, prompt, options = {}) {
555
- const { onChunk, onComplete, onError } = options;
556
- const { spawn } = require('child_process');
557
- const path = require('path');
558
- const os = require('os');
559
-
560
- return new Promise((resolve) => {
561
- let fullResponse = '';
562
- let errorOutput = '';
563
-
564
- // Resolve opencode binary — check well-known path first
565
- let cmd = 'opencode';
566
- const knownPath = path.join(os.homedir(), '.opencode', 'bin', 'opencode');
567
- try {
568
- require('fs').accessSync(knownPath, require('fs').constants.X_OK);
569
- cmd = knownPath;
570
- } catch {
571
- // fall back to PATH lookup
572
- }
573
-
574
- const opencode = spawn(cmd, ['-p', prompt], {
575
- stdio: ['ignore', 'pipe', 'pipe']
576
- });
577
-
578
- opencode.stdout.on('data', (data) => {
579
- const chunk = data.toString();
580
- fullResponse += chunk;
581
- if (onChunk) onChunk(chunk);
582
- });
583
-
584
- opencode.stderr.on('data', (data) => {
585
- errorOutput += data.toString();
586
- });
587
-
588
- opencode.on('close', (code) => {
589
- if (code === 0) {
590
- if (onComplete) onComplete(fullResponse);
591
- resolve({ success: true, response: fullResponse });
592
- } else {
593
- const error = `OpenCode CLI exited with code ${code}: ${errorOutput}`;
594
- if (onError) onError(error);
595
- this.detectAndSaveRateLimit('opencode', 'opencode-cli', errorOutput);
596
- resolve({ success: false, error });
597
- }
598
- });
599
-
600
- opencode.on('error', (err) => {
601
- const error = `Failed to start OpenCode CLI: ${err.message}`;
602
- if (onError) onError(error);
603
- resolve({ success: false, error });
604
- });
605
- });
606
- }
607
-
608
- /**
609
- * Check if OpenCode CLI is available
610
- * @returns {Promise<boolean>}
611
- */
612
- async isOpenCodeAvailable() {
613
- const { spawn } = require('child_process');
614
- const path = require('path');
615
- const os = require('os');
616
-
617
- // Try well-known path first, then fall back to PATH
618
- let cmd = 'opencode';
619
- const knownPath = path.join(os.homedir(), '.opencode', 'bin', 'opencode');
620
- try {
621
- require('fs').accessSync(knownPath, require('fs').constants.X_OK);
622
- cmd = knownPath;
623
- } catch {
624
- // fall back to PATH lookup
625
- }
626
-
627
- return new Promise((resolve) => {
628
- const proc = spawn(cmd, ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
629
- proc.on('close', (code) => resolve(code === 0));
630
- proc.on('error', () => resolve(false));
631
- setTimeout(() => { proc.kill(); resolve(false); }, 5000);
632
- });
633
- }
634
-
635
- /**
636
- * Call the VS Code Copilot CLI with a prompt
637
- */
638
- async callVSCodeCopilotCLI(model, prompt, options = {}) {
639
- const { onChunk, onComplete, onError } = options;
640
- const { spawn } = require('child_process');
641
- const os = require('os');
642
-
643
- // Safe logging function to prevent EPIPE errors
644
- const safeLog = (message) => {
645
- try {
646
- console.log(message);
647
- } catch (err) {
648
- // Ignore EPIPE errors that occur when stdout is closed
649
- if (err.code === 'EPIPE') {
650
- // Silently ignore - this happens during process shutdown
651
- } else {
652
- // Re-throw other errors
653
- throw err;
654
- }
655
- }
656
- };
657
-
658
- safeLog(`[VS CODE COPILOT CLI] Starting call with model: ${model}`);
659
- safeLog(`[VS CODE COPILOT CLI] Prompt: ${prompt.substring(0, 100)}...`);
660
-
661
- // Set up environment with authentication if available
662
- const env = { ...process.env };
663
- if (!env.HOME) env.HOME = os.homedir();
664
-
665
- return new Promise((resolve) => {
666
- let fullResponse = '';
667
- let errorOutput = '';
668
-
669
- // Non-interactive prompt invocation
670
- // `copilot` uses `-p/--prompt` for non-interactive mode.
671
- const args = ['-p', String(prompt), '-s', '--no-ask-user'];
672
- const copilot = spawn('copilot', args, {
673
- stdio: ['ignore', 'pipe', 'pipe'],
674
- cwd: process.cwd(),
675
- env
676
- });
677
-
678
- safeLog(`[VS CODE COPILOT CLI] Spawned process with PID: ${copilot.pid}`);
679
-
680
- copilot.stdout.on('data', (data) => {
681
- const text = data.toString();
682
- fullResponse += text;
683
- safeLog(`[VS CODE COPILOT CLI] STDOUT: ${text.substring(0, 200)}...`);
684
- if (onChunk) onChunk(text);
685
- });
686
-
687
- copilot.stderr.on('data', (data) => {
688
- const text = data.toString();
689
- errorOutput += text;
690
- safeLog(`[VS CODE COPILOT CLI] STDERR: ${text.substring(0, 200)}...`);
691
- });
692
-
693
- copilot.on('close', (code) => {
694
- safeLog(`[VS CODE COPILOT CLI] Process closed with code: ${code}`);
695
- safeLog(`[VS CODE COPILOT CLI] Full response length: ${fullResponse.length}`);
696
- safeLog(`[VS CODE COPILOT CLI] Error output length: ${errorOutput.length}`);
697
- safeLog(`[VS CODE COPILOT CLI] Error output: ${errorOutput}`);
698
-
699
- if (code === 0) {
700
- if (onComplete) onComplete(fullResponse);
701
- resolve({ success: true, response: fullResponse });
702
- } else {
703
- // Check if this is an authentication error and provide a helpful message
704
- const isAuthError = this.checkForAuthenticationError(errorOutput);
705
- let error = `VS Code Copilot CLI exited with code ${code}: ${errorOutput}`;
706
-
707
- if (isAuthError) {
708
- error = `VS Code Copilot CLI requires authentication. Run 'copilot login' to authenticate with GitHub, or set COPILOT_GITHUB_TOKEN environment variable.`;
709
- safeLog(`[VS CODE COPILOT CLI] Authentication error detected: ${error}`);
710
-
711
- // If we had previously marked this provider as rate limited, clear that stale state.
712
- // Auth/setup failures should never surface as rate limit in the GUI.
713
- try {
714
- if (this.providerManager && typeof this.providerManager.clearProviderRateLimits === 'function') {
715
- this.providerManager.clearProviderRateLimits('vscode-copilot-cli');
716
- }
717
- } catch (_) { }
718
- }
719
-
720
- safeLog(`[VS CODE COPILOT CLI] Error: ${error}`);
721
- if (onError) onError(error);
722
-
723
- // Check if this is actually a rate limit error before calling detectAndSaveRateLimit
724
- const isRateLimitError = this.checkForRateLimitError(errorOutput);
725
- safeLog(`[VS CODE COPILOT CLI] Is rate limit error: ${isRateLimitError}`);
726
-
727
- if (isRateLimitError) {
728
- this.detectAndSaveRateLimit('vscode-copilot-cli', 'copilot-cli', errorOutput);
729
- }
730
-
731
- resolve({ success: false, error });
732
- }
733
- });
734
-
735
- copilot.on('error', (err) => {
736
- const error = `Failed to start VS Code Copilot CLI: ${err.message}`;
737
- safeLog(`[VS CODE COPILOT CLI] Spawn error: ${error}`);
738
- if (onError) onError(error);
739
- resolve({ success: false, error });
740
- });
741
- });
742
- }
743
-
744
- /**
745
- * Check if error output indicates an authentication error
746
- */
747
- checkForAuthenticationError(errorOutput) {
748
- const authIndicators = [
749
- 'No authentication information found',
750
- 'authentication information found',
751
- 'not authenticated',
752
- 'COPILOT_GITHUB_TOKEN',
753
- 'GH_TOKEN',
754
- 'GITHUB_TOKEN',
755
- '/login',
756
- 'gh auth login',
757
- 'OAuth Token',
758
- 'Personal Access Token'
759
- ];
760
-
761
- const isAuthError = authIndicators.some(indicator =>
762
- errorOutput.includes(indicator)
763
- );
764
-
765
- console.log(`[AUTH CHECK] Error output: "${errorOutput}"`);
766
- console.log(`[AUTH CHECK] Is authentication error: ${isAuthError}`);
767
-
768
- return isAuthError;
769
- }
770
-
771
- /**
772
- * Check if error output indicates a genuine rate limit error
773
- */
774
- checkForRateLimitError(errorOutput) {
775
- // VS Code Copilot CLI specific rate limit indicators
776
- const rateLimitIndicators = [
777
- 'rate limit',
778
- 'Rate limit',
779
- 'too many requests',
780
- 'Too many requests',
781
- '429',
782
- 'quota exceeded',
783
- 'Quota exceeded',
784
- 'usage limit',
785
- 'Usage limit',
786
- 'limit reached',
787
- 'Limit reached',
788
- 'weekly limit',
789
- 'Weekly limit',
790
- 'daily limit',
791
- 'Daily limit'
792
- ];
793
-
794
- // Exclude common authentication and setup errors that are NOT rate limits
795
- const nonRateLimitIndicators = [
796
- 'authentication information found',
797
- 'Authentication information found',
798
- 'No authentication',
799
- 'not authenticated',
800
- 'COPILOT_GITHUB_TOKEN',
801
- 'GH_TOKEN',
802
- 'GITHUB_TOKEN',
803
- '/login',
804
- 'gh auth login',
805
- 'OAuth Token',
806
- 'Personal Access Token',
807
- 'GitHub CLI'
808
- ];
809
-
810
- // First check if it contains non-rate-limit indicators
811
- const isNonRateLimit = nonRateLimitIndicators.some(indicator =>
812
- errorOutput.includes(indicator)
813
- );
814
-
815
- if (isNonRateLimit) {
816
- console.log(`[RATE LIMIT CHECK] Contains non-rate-limit indicators, not a rate limit`);
817
- return false;
818
- }
819
-
820
- // Only consider it a rate limit if it contains specific rate limit indicators
821
- const isRateLimit = rateLimitIndicators.some(indicator =>
822
- errorOutput.includes(indicator)
823
- );
824
-
825
- console.log(`[RATE LIMIT CHECK] Error output: "${errorOutput}"`);
826
- console.log(`[RATE LIMIT CHECK] Is rate limit: ${isRateLimit}`);
827
-
828
- return isRateLimit;
829
- }
830
-
831
- /**
832
- * Check if VS Code Copilot CLI is available AND authenticated
833
- * @returns {Promise<{available: boolean, needsAuth: boolean, authMethod?: string}>}
834
- */
835
- async isVSCodeCopilotCLIAvailable() {
836
- const { spawn } = require('child_process');
837
- const os = require('os');
838
-
839
- // Safe logging function to prevent EPIPE errors
840
- const safeLog = (message) => {
841
- try {
842
- console.log(message);
843
- } catch (err) {
844
- // Ignore EPIPE errors that occur when stdout is closed
845
- if (err.code === 'EPIPE') {
846
- // Silently ignore - this happens during process shutdown
847
- } else {
848
- // Re-throw other errors
849
- throw err;
850
- }
851
- }
852
- };
853
-
854
- safeLog(`[VS CODE COPILOT CLI] Checking availability and authentication...`);
855
-
856
- return new Promise((resolve) => {
857
- // First check if the CLI is installed
858
- const baseEnv = { ...process.env };
859
- if (!baseEnv.HOME) baseEnv.HOME = os.homedir();
860
-
861
- const versionProc = spawn('copilot', ['--version'], { stdio: ['ignore', 'pipe', 'pipe'], env: baseEnv });
862
-
863
- let versionStdout = '';
864
- let versionStderr = '';
865
- let versionTimeout;
866
-
867
- versionProc.stdout.on('data', (data) => {
868
- versionStdout += data.toString();
869
- safeLog(`[VS CODE COPILOT CLI] Version check STDOUT: ${data.toString().trim()}`);
870
- });
871
-
872
- versionProc.stderr.on('data', (data) => {
873
- versionStderr += data.toString();
874
- safeLog(`[VS CODE COPILOT CLI] Version check STDERR: ${data.toString().trim()}`);
875
- });
876
-
877
- versionProc.on('close', (versionCode) => {
878
- clearTimeout(versionTimeout);
879
- safeLog(`[VS CODE COPILOT CLI] Version check exited with code: ${versionCode}`);
880
-
881
- if (versionCode !== 0) {
882
- safeLog(`[VS CODE COPILOT CLI] Not installed or not in PATH`);
883
- resolve({ available: false, needsAuth: false });
884
- return;
885
- }
886
-
887
- // CLI is installed, now check if it's authenticated using a short non-interactive prompt.
888
- // Note: This CLI does not support `copilot whoami`, and GitHub CLI (`gh`) may not be installed.
889
- // We keep this probe short and interpret device-flow output as needsAuth.
890
- safeLog(`[VS CODE COPILOT CLI] CLI is installed, checking authentication (non-interactive probe)...`);
891
-
892
- const probeArgs = ['-p', 'Reply with OK', '-s', '--no-ask-user'];
893
- const probeProc = spawn('copilot', probeArgs, { stdio: ['ignore', 'pipe', 'pipe'], env: baseEnv });
894
-
895
- let probeStdout = '';
896
- let probeStderr = '';
897
- let probeFinished = false;
898
- const finishProbe = (code) => {
899
- if (probeFinished) return;
900
- probeFinished = true;
901
- const out = (probeStdout || '').trim();
902
- const err = (probeStderr || '').trim();
903
- safeLog(`[VS CODE COPILOT CLI] Probe exited with code: ${code}`);
904
- if (out) safeLog(`[VS CODE COPILOT CLI] Probe STDOUT: ${out.substring(0, 200)}`);
905
- if (err) safeLog(`[VS CODE COPILOT CLI] Probe STDERR: ${err.substring(0, 200)}`);
906
-
907
- // For copilot CLI, we consider it working if we get "OK" output even with exit code 1
908
- // The --no-ask-user flag seems to cause exit code 1 but still provides the response
909
- const isWorking = (code === 0 && out) || (code === 1 && out.trim() === 'OK');
910
-
911
- if (isWorking) {
912
- resolve({ available: true, needsAuth: false, authMethod: 'existing' });
913
- return;
914
- }
915
-
916
- const combined = `${out}\n${err}`;
917
-
918
- // Check for rate limit first
919
- const isRateLimited =
920
- combined.includes('402 You have no quota') ||
921
- combined.includes('quota') ||
922
- combined.includes('rate limit') ||
923
- combined.includes('Rate limit');
924
-
925
- if (isRateLimited) {
926
- safeLog(`[VS CODE COPILOT CLI] Detected rate limit error`);
927
- resolve({ available: true, needsAuth: false, authMethod: 'existing', rateLimited: true });
928
- return;
929
- }
930
-
931
- const needsAuth =
932
- combined.includes('copilot login') ||
933
- combined.includes('Authenticate with Copilot') ||
934
- combined.includes('github.com/login/device') ||
935
- combined.includes('To authenticate') ||
936
- combined.includes('Waiting for authorization');
937
-
938
- resolve({ available: true, needsAuth: Boolean(needsAuth), authMethod: needsAuth ? 'manual' : 'unknown' });
939
- };
940
-
941
- probeProc.stdout.on('data', (data) => { probeStdout += data.toString(); });
942
- probeProc.stderr.on('data', (data) => { probeStderr += data.toString(); });
943
- probeProc.on('close', (code) => finishProbe(code));
944
- probeProc.on('error', () => finishProbe(1));
945
- setTimeout(() => {
946
- try { probeProc.kill(); } catch (_) { }
947
- finishProbe(1);
948
- }, 8000);
949
- });
950
-
951
- versionProc.on('error', (err) => {
952
- clearTimeout(versionTimeout);
953
- safeLog(`[VS CODE COPILOT CLI] Version check error: ${err.message}`);
954
- resolve({ available: false, needsAuth: false });
955
- });
956
-
957
- versionTimeout = setTimeout(() => {
958
- safeLog(`[VS CODE COPILOT CLI] Version check timeout, killing process`);
959
- versionProc.kill();
960
- resolve({ available: false, needsAuth: false });
961
- }, 5000);
962
- });
963
- }
964
-
965
- /**
966
- * Attempt to authenticate VS Code Copilot CLI automatically
967
- * @returns {Promise<{success: boolean, method: string, reason?: string}>}
968
- */
969
- async attemptAutoAuthentication() {
970
- const { spawn } = require('child_process');
971
-
972
- // Safe logging function to prevent EPIPE errors
973
- const safeLog = (message) => {
974
- try {
975
- console.log(message);
976
- } catch (err) {
977
- // Ignore EPIPE errors that occur when stdout is closed
978
- if (err.code === 'EPIPE') {
979
- // Silently ignore - this happens during process shutdown
980
- } else {
981
- // Re-throw other errors
982
- throw err;
983
- }
984
- }
985
- };
986
-
987
- safeLog(`[VS CODE COPILOT CLI] Attempting auto-authentication...`);
988
-
989
- // Method 1: Check if GitHub CLI is authenticated and get token
990
- try {
991
- safeLog(`[VS CODE COPILOT CLI] Method 1: Checking GitHub CLI authentication...`);
992
- const ghAuth = spawn('gh', ['auth', 'status'], { stdio: ['ignore', 'pipe', 'pipe'] });
993
-
994
- let ghStdout = '';
995
- let ghStderr = '';
996
-
997
- ghAuth.stdout.on('data', (data) => {
998
- ghStdout += data.toString();
999
- });
1000
-
1001
- ghAuth.stderr.on('data', (data) => {
1002
- ghStderr += data.toString();
1003
- });
1004
-
1005
- const ghResult = await new Promise((resolve) => {
1006
- ghAuth.on('close', (code) => {
1007
- resolve({ code, stdout: ghStdout, stderr: ghStderr });
1008
- });
1009
-
1010
- ghAuth.on('error', () => {
1011
- resolve({ code: -1, stdout: '', stderr: 'gh command not found' });
1012
- });
1013
-
1014
- setTimeout(() => { ghAuth.kill(); resolve({ code: -1, stdout: '', stderr: 'timeout' }); }, 5000);
1015
- });
1016
-
1017
- if (ghResult.code === 0 && ghResult.stdout.includes('Logged in to')) {
1018
- safeLog(`[VS CODE COPILOT CLI] GitHub CLI is authenticated, getting token...`);
1019
-
1020
- // Get token from GitHub CLI
1021
- const ghToken = spawn('gh', ['auth', 'token'], { stdio: ['ignore', 'pipe', 'pipe'] });
1022
-
1023
- let tokenStdout = '';
1024
- let tokenStderr = '';
1025
-
1026
- ghToken.stdout.on('data', (data) => {
1027
- tokenStdout += data.toString();
1028
- });
1029
-
1030
- ghToken.stderr.on('data', (data) => {
1031
- tokenStderr += data.toString();
1032
- });
1033
-
1034
- const tokenResult = await new Promise((resolve) => {
1035
- ghToken.on('close', (code) => {
1036
- resolve({ code, stdout: tokenStdout, stderr: tokenStderr });
1037
- });
1038
-
1039
- ghToken.on('error', () => {
1040
- resolve({ code: 1, stdout: '', stderr: 'Failed to spawn gh auth token' });
1041
- });
1042
-
1043
- setTimeout(() => {
1044
- try { ghToken.kill(); } catch (_) { }
1045
- resolve({ code: 1, stdout: '', stderr: 'Timeout getting token' });
1046
- }, 5000);
1047
- });
1048
-
1049
- if (tokenResult.code === 0 && tokenResult.stdout) {
1050
- safeLog(`[VS CODE COPILOT CLI] Got token from GitHub CLI, testing with Copilot CLI...`);
1051
-
1052
- // Test the token with Copilot CLI
1053
- const testResult = await this.testTokenWithCopilot(tokenResult.stdout);
1054
- if (testResult.success) {
1055
- return { success: true, method: 'github-cli' };
1056
- } else {
1057
- safeLog(`[VS CODE COPILOT CLI] GitHub CLI token failed with Copilot: ${testResult.reason}`);
1058
- }
1059
- }
1060
- }
1061
- } catch (error) {
1062
- safeLog(`[VS CODE COPILOT CLI] GitHub CLI method failed: ${error.message}`);
1063
- }
1064
-
1065
- // Method 2: Check environment variables
1066
- safeLog(`[VS CODE COPILOT CLI] Method 2: Checking environment variables...`);
1067
- const envVars = ['COPILOT_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN'];
1068
-
1069
- for (const envVar of envVars) {
1070
- const token = process.env[envVar];
1071
- if (token) {
1072
- safeLog(`[VS CODE COPILOT CLI] Found ${envVar}, testing with Copilot CLI...`);
1073
-
1074
- const testResult = await this.testTokenWithCopilot(token);
1075
- if (testResult.success) {
1076
- return { success: true, method: `env-${envVar}` };
1077
- } else {
1078
- safeLog(`[VS CODE COPILOT CLI] ${envVar} token failed with Copilot: ${testResult.reason}`);
1079
- }
1080
- }
1081
- }
1082
-
1083
- safeLog(`[VS CODE COPILOT CLI] All auto-authentication methods failed`);
1084
- return { success: false, method: 'none', reason: 'No valid authentication found' };
1085
- }
1086
-
1087
- /**
1088
- * Test if a token works with VS Code Copilot CLI
1089
- * @param {string} token - GitHub token to test
1090
- * @returns {Promise<{success: boolean, reason?: string}>}
1091
- */
1092
- async testTokenWithCopilot(token) {
1093
- const { spawn } = require('child_process');
1094
-
1095
- return new Promise((resolve) => {
1096
- const env = { ...process.env, COPILOT_GITHUB_TOKEN: token };
1097
-
1098
- const testProc = spawn('copilot', ['-p', 'test', '-s'], {
1099
- stdio: ['ignore', 'pipe', 'pipe'],
1100
- env,
1101
- timeout: 5000
1102
- });
1103
-
1104
- let stderr = '';
1105
-
1106
- testProc.stderr.on('data', (data) => {
1107
- stderr += data.toString();
1108
- });
1109
-
1110
- testProc.on('close', (code) => {
1111
- const needsAuth = stderr.includes('No authentication information found') ||
1112
- stderr.includes('authentication information found') ||
1113
- stderr.includes('not authenticated');
1114
-
1115
- if (needsAuth) {
1116
- resolve({ success: false, reason: 'Token not valid for Copilot CLI' });
1117
- } else {
1118
- resolve({ success: true });
1119
- }
1120
- });
1121
-
1122
- testProc.on('error', (err) => {
1123
- resolve({ success: false, reason: err.message });
1124
- });
1125
-
1126
- setTimeout(() => {
1127
- testProc.kill();
1128
- resolve({ success: false, reason: 'timeout' });
1129
- }, 5000);
1130
- });
1131
- }
1132
-
1133
- /**
1134
- * Call any LLM provider
1135
- * @param {Object} config - Provider configuration
1136
- * @param {string} prompt - Prompt to send
1137
- * @param {Object} options - Options
1138
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
1139
- */
1140
- async call(config, prompt, options = {}) {
1141
- const { provider, model, apiKey, region, accessKeyId, secretAccessKey, fallbackModels = [] } = config;
1142
- const modelsToTry = [model, ...fallbackModels];
1143
- let lastError = null;
1144
-
1145
- for (const currentModel of modelsToTry) {
1146
- if (currentModel !== model) {
1147
- this.logger.log(`⚠️ Quota/Limit reached for previous model, failing over to ${currentModel}...`);
1148
- }
1149
-
1150
- const agentId = `${provider}:${currentModel}`;
1151
- try {
1152
- const quota = await quotaManagement.fetchQuotaForAgent(agentId);
1153
- if (quota.isExceeded()) {
1154
- const errorMessage = `Quota limit reached for ${currentModel}. Resets at ${quota.resetsAt ? quota.resetsAt.toLocaleString() : 'a later time'}.`;
1155
- lastError = { success: false, error: errorMessage };
1156
- continue; // Try next model
1157
- }
1158
- } catch (error) {
1159
- this.logger.error(`Failed to check quota for ${agentId}: ${error.message}`);
1160
- }
1161
-
1162
- const currentConfig = { ...config, model: currentModel };
1163
- let result;
1164
-
1165
- switch (provider) {
1166
- case 'ollama':
1167
- result = await this.callOllama(currentModel, prompt, options);
1168
- break;
1169
- case 'anthropic':
1170
- result = await this.callAnthropic(currentModel, prompt, { ...options, apiKey });
1171
- break;
1172
- case 'groq':
1173
- result = await this.callGroq(currentModel, prompt, { ...options, apiKey });
1174
- break;
1175
- case 'bedrock':
1176
- result = await this.callBedrock(currentModel, prompt, { ...options, region, accessKeyId, secretAccessKey });
1177
- break;
1178
- case 'claude-code':
1179
- result = await this.callClaudeCode(currentModel, prompt, options);
1180
- break;
1181
- case 'cline':
1182
- result = await this.callCline(currentModel, prompt, options);
1183
- break;
1184
- case 'opencode':
1185
- result = await this.callOpenCode(currentModel, prompt, options);
1186
- break;
1187
- case 'vscode-copilot-cli':
1188
- result = await this.callVSCodeCopilotCLI(currentModel, prompt, options);
1189
- break;
1190
- default:
1191
- return { success: false, error: `Unknown provider: ${provider}` };
1192
- }
1193
-
1194
- if (result.success) {
1195
- return result;
1196
- }
1197
-
1198
- // If failed, check for rate limit to save it
1199
- this.detectAndSaveRateLimit(provider, currentModel, result.error || '');
1200
- lastError = result;
1201
-
1202
- // If it's a "fatal" error that isn't a rate limit, we might want to stop?
1203
- // But usually we want to try the next model if possible.
1204
- }
1205
-
1206
- return lastError || { success: false, error: `All models for ${provider} failed.` };
1207
- }
1208
-
1209
- /**
1210
- * Check if Ollama is available
1211
- * @returns {Promise<boolean>}
1212
- */
1213
- async isOllamaAvailable() {
1214
- return new Promise((resolve) => {
1215
- const req = http.request({
1216
- hostname: 'localhost',
1217
- port: 11434,
1218
- path: '/api/tags',
1219
- method: 'GET',
1220
- timeout: 2000
1221
- }, (res) => {
1222
- resolve(res.statusCode === 200);
1223
- });
1224
-
1225
- req.on('error', () => resolve(false));
1226
- req.on('timeout', () => {
1227
- req.destroy();
1228
- resolve(false);
1229
- });
1230
-
1231
- req.end();
1232
- });
1233
- }
1234
-
1235
- /**
1236
- * Check if Claude Code CLI is available
1237
- * @returns {Promise<boolean>}
1238
- */
1239
- async isClaudeCodeAvailable() {
1240
- const { spawn } = require('child_process');
1241
-
1242
- return new Promise((resolve) => {
1243
- const claude = spawn('claude', ['--version'], {
1244
- stdio: ['ignore', 'pipe', 'pipe']
1245
- });
1246
-
1247
- claude.on('close', (code) => {
1248
- resolve(code === 0);
1249
- });
1250
-
1251
- claude.on('error', () => {
1252
- resolve(false);
1253
- });
1254
-
1255
- // Timeout after 2 seconds
1256
- setTimeout(() => {
1257
- claude.kill();
1258
- resolve(false);
1259
- }, 2000);
1260
- });
1261
- }
1262
-
1263
- /**
1264
- * Get list of installed Ollama models
1265
- * @returns {Promise<string[]>}
1266
- */
1267
- async getOllamaModels() {
1268
- return new Promise((resolve) => {
1269
- const req = http.request({
1270
- hostname: 'localhost',
1271
- port: 11434,
1272
- path: '/api/tags',
1273
- method: 'GET'
1274
- }, (res) => {
1275
- let data = '';
1276
-
1277
- res.on('data', (chunk) => {
1278
- data += chunk.toString();
1279
- });
1280
-
1281
- res.on('end', () => {
1282
- try {
1283
- const json = JSON.parse(data);
1284
- const models = json.models?.map(m => m.name) || [];
1285
- resolve(models);
1286
- } catch (err) {
1287
- resolve([]);
1288
- }
1289
- });
1290
- });
1291
-
1292
- req.on('error', () => resolve([]));
1293
- req.end();
1294
- });
1295
- }
1296
- }
1297
-
1298
- module.exports = DirectLLMManager;
1299
-
1
+ /**
2
+ * Direct LLM API Manager - Call LLM APIs directly without IDE CLI tools
3
+ * Supports: Ollama (local), Anthropic, Groq, AWS Bedrock
4
+ */
5
+
6
+ const https = require('https');
7
+ const http = require('http');
8
+ const quotaManagement = require('../quota-management');
9
+
10
+ class DirectLLMManager {
11
+ constructor(sharedProviderManager = null) {
12
+ this.logger = console;
13
+ // Use shared ProviderManager if provided, otherwise create new instance
14
+ // IMPORTANT: Pass shared instance to maintain rate limit state across calls
15
+ if (sharedProviderManager) {
16
+ this.providerManager = sharedProviderManager;
17
+ } else {
18
+ try {
19
+ const ProviderManager = require('../ide-integration/provider-manager.cjs');
20
+ this.providerManager = new ProviderManager();
21
+ } catch (err) {
22
+ this.providerManager = null;
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Detect and save rate limit from error message
29
+ * @param {string} provider - Provider name
30
+ * @param {string} model - Model name
31
+ * @param {string} errorMessage - Error message from API
32
+ */
33
+ detectAndSaveRateLimit(provider, model, errorMessage) {
34
+ if (!this.providerManager) return;
35
+
36
+ // Check for rate limit indicators
37
+ const isRateLimit = (errorMessage.includes('rate limit') ||
38
+ errorMessage.includes('Rate limit') ||
39
+ errorMessage.includes('too many requests') ||
40
+ errorMessage.includes('429') ||
41
+ errorMessage.includes('quota') ||
42
+ errorMessage.includes('Weekly limit reached') ||
43
+ errorMessage.includes('Daily limit reached') ||
44
+ errorMessage.includes('limit reached')) &&
45
+ !errorMessage.startsWith('Quota limit reached'); // Don't re-mark our own internal exceeded messages
46
+
47
+ if (isRateLimit) {
48
+ this.providerManager.markRateLimited(provider, model, errorMessage);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Call Ollama API directly (local)
54
+ * @param {string} model - Model name (e.g., "qwen2.5-coder:32b")
55
+ * @param {string} prompt - Prompt to send
56
+ * @param {Object} options - Options (onChunk, onComplete, onError)
57
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
58
+ */
59
+ async callOllama(model, prompt, options = {}) {
60
+ const { onChunk, onComplete, onError, temperature = 0.2 } = options;
61
+
62
+ return new Promise((resolve) => {
63
+ let fullResponse = '';
64
+
65
+ const postData = JSON.stringify({
66
+ model: model,
67
+ prompt: prompt,
68
+ stream: true,
69
+ options: {
70
+ temperature: temperature
71
+ }
72
+ });
73
+
74
+ const req = http.request({
75
+ hostname: 'localhost',
76
+ port: 11434,
77
+ path: '/api/generate',
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ 'Content-Length': Buffer.byteLength(postData)
82
+ }
83
+ }, (res) => {
84
+ let buffer = '';
85
+
86
+ res.on('data', (chunk) => {
87
+ buffer += chunk.toString();
88
+ const lines = buffer.split('\n');
89
+ buffer = lines.pop(); // Keep incomplete line in buffer
90
+
91
+ for (const line of lines) {
92
+ if (!line.trim()) continue;
93
+
94
+ try {
95
+ const data = JSON.parse(line);
96
+ if (data.response) {
97
+ fullResponse += data.response;
98
+ if (onChunk) onChunk(data.response);
99
+ }
100
+
101
+ if (data.done) {
102
+ if (onComplete) onComplete(fullResponse);
103
+ resolve({
104
+ success: true,
105
+ response: fullResponse,
106
+ model: data.model,
107
+ context: data.context
108
+ });
109
+ }
110
+ } catch (err) {
111
+ // Ignore JSON parse errors for partial chunks
112
+ }
113
+ }
114
+ });
115
+
116
+ res.on('end', () => {
117
+ if (!fullResponse) {
118
+ const error = 'No response received from Ollama';
119
+ if (onError) onError(error);
120
+ resolve({ success: false, error });
121
+ }
122
+ });
123
+ });
124
+
125
+ req.on('error', (error) => {
126
+ const errorMsg = `Ollama API error: ${error.message}`;
127
+ if (onError) onError(errorMsg);
128
+ resolve({ success: false, error: errorMsg });
129
+ });
130
+
131
+ req.write(postData);
132
+ req.end();
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Call Anthropic API directly
138
+ * @param {string} model - Model name (e.g., "claude-sonnet-4-20250514")
139
+ * @param {string} prompt - Prompt to send
140
+ * @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
141
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
142
+ */
143
+ async callAnthropic(model, prompt, options = {}) {
144
+ const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
145
+
146
+ if (!apiKey) {
147
+ const error = 'Anthropic API key required';
148
+ if (onError) onError(error);
149
+ return { success: false, error };
150
+ }
151
+
152
+ return new Promise((resolve) => {
153
+ let fullResponse = '';
154
+
155
+ const postData = JSON.stringify({
156
+ model: model,
157
+ max_tokens: maxTokens,
158
+ temperature: temperature,
159
+ messages: [
160
+ { role: 'user', content: prompt }
161
+ ],
162
+ stream: true
163
+ });
164
+
165
+ const req = https.request({
166
+ hostname: 'api.anthropic.com',
167
+ path: '/v1/messages',
168
+ method: 'POST',
169
+ headers: {
170
+ 'Content-Type': 'application/json',
171
+ 'x-api-key': apiKey,
172
+ 'anthropic-version': '2023-06-01',
173
+ 'Content-Length': Buffer.byteLength(postData)
174
+ }
175
+ }, (res) => {
176
+ let buffer = '';
177
+
178
+ res.on('data', (chunk) => {
179
+ buffer += chunk.toString();
180
+ const lines = buffer.split('\n');
181
+ buffer = lines.pop();
182
+
183
+ for (const line of lines) {
184
+ if (!line.trim() || !line.startsWith('data: ')) continue;
185
+
186
+ try {
187
+ const jsonStr = line.slice(6); // Remove "data: " prefix
188
+ if (jsonStr === '[DONE]') continue;
189
+
190
+ const data = JSON.parse(jsonStr);
191
+
192
+ if (data.type === 'content_block_delta' && data.delta?.text) {
193
+ fullResponse += data.delta.text;
194
+ if (onChunk) onChunk(data.delta.text);
195
+ } else if (data.type === 'message_stop') {
196
+ if (onComplete) onComplete(fullResponse);
197
+ resolve({
198
+ success: true,
199
+ response: fullResponse,
200
+ model: model
201
+ });
202
+ }
203
+ } catch (err) {
204
+ // Ignore JSON parse errors
205
+ }
206
+ }
207
+ });
208
+
209
+ res.on('end', () => {
210
+ if (!fullResponse) {
211
+ const error = 'No response received from Anthropic';
212
+ if (onError) onError(error);
213
+ resolve({ success: false, error });
214
+ }
215
+ });
216
+ });
217
+
218
+ req.on('error', (error) => {
219
+ const errorMsg = `Anthropic API error: ${error.message}`;
220
+ if (onError) onError(errorMsg);
221
+ resolve({ success: false, error: errorMsg });
222
+ });
223
+
224
+ req.write(postData);
225
+ req.end();
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Call Groq API directly
231
+ * @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
232
+ * @param {string} prompt - Prompt to send
233
+ * @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
234
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
235
+ */
236
+ async callGroq(model, prompt, options = {}) {
237
+ const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
238
+
239
+ if (!apiKey) {
240
+ const error = 'Groq API key required';
241
+ if (onError) onError(error);
242
+ return { success: false, error };
243
+ }
244
+
245
+ return new Promise((resolve) => {
246
+ let fullResponse = '';
247
+
248
+ const postData = JSON.stringify({
249
+ model: model,
250
+ messages: [
251
+ { role: 'user', content: prompt }
252
+ ],
253
+ temperature: temperature,
254
+ max_tokens: maxTokens,
255
+ stream: true
256
+ });
257
+
258
+ const req = https.request({
259
+ hostname: 'api.groq.com',
260
+ path: '/openai/v1/chat/completions',
261
+ method: 'POST',
262
+ headers: {
263
+ 'Content-Type': 'application/json',
264
+ 'Authorization': `Bearer ${apiKey}`,
265
+ 'Content-Length': Buffer.byteLength(postData)
266
+ }
267
+ }, (res) => {
268
+ let buffer = '';
269
+ let statusCode = res.statusCode;
270
+
271
+ // Check for rate limit or error status codes
272
+ if (statusCode === 429 || statusCode >= 400) {
273
+ let errorBody = '';
274
+ res.on('data', (chunk) => {
275
+ errorBody += chunk.toString();
276
+ });
277
+ res.on('end', () => {
278
+ const errorMsg = `Groq API error (${statusCode}): ${errorBody || 'No error details'}`;
279
+ this.detectAndSaveRateLimit('groq', model, errorMsg);
280
+ if (onError) onError(errorMsg);
281
+ resolve({ success: false, error: errorMsg });
282
+ });
283
+ return;
284
+ }
285
+
286
+ res.on('data', (chunk) => {
287
+ buffer += chunk.toString();
288
+ const lines = buffer.split('\n');
289
+ buffer = lines.pop();
290
+
291
+ for (const line of lines) {
292
+ if (!line.trim() || !line.startsWith('data: ')) continue;
293
+
294
+ try {
295
+ const jsonStr = line.slice(6);
296
+ if (jsonStr === '[DONE]') {
297
+ if (onComplete) onComplete(fullResponse);
298
+ resolve({
299
+ success: true,
300
+ response: fullResponse,
301
+ model: model
302
+ });
303
+ return;
304
+ }
305
+
306
+ const data = JSON.parse(jsonStr);
307
+ const content = data.choices?.[0]?.delta?.content;
308
+
309
+ if (content) {
310
+ fullResponse += content;
311
+ if (onChunk) onChunk(content);
312
+ }
313
+ } catch (err) {
314
+ // Ignore JSON parse errors
315
+ }
316
+ }
317
+ });
318
+
319
+ res.on('end', () => {
320
+ if (fullResponse) {
321
+ if (onComplete) onComplete(fullResponse);
322
+ resolve({ success: true, response: fullResponse, model });
323
+ } else {
324
+ const error = buffer || 'No response received from Groq';
325
+ this.detectAndSaveRateLimit('groq', model, error);
326
+ if (onError) onError(error);
327
+ resolve({ success: false, error });
328
+ }
329
+ });
330
+ });
331
+
332
+ req.on('error', (error) => {
333
+ const errorMsg = `Groq API error: ${error.message}`;
334
+ this.detectAndSaveRateLimit('groq', model, errorMsg);
335
+ if (onError) onError(errorMsg);
336
+ resolve({ success: false, error: errorMsg });
337
+ });
338
+
339
+ req.write(postData);
340
+ req.end();
341
+ });
342
+ }
343
+
344
+ /**
345
+ * Call AWS Bedrock API directly
346
+ * @param {string} model - Model ID (e.g., "anthropic.claude-sonnet-4-v1")
347
+ * @param {string} prompt - Prompt to send
348
+ * @param {Object} options - Options (region, accessKeyId, secretAccessKey, onChunk, onComplete, onError)
349
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
350
+ */
351
+ async callBedrock(model, prompt, options = {}) {
352
+ const { region, accessKeyId, secretAccessKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
353
+
354
+ if (!region || !accessKeyId || !secretAccessKey) {
355
+ const error = 'AWS credentials required (region, accessKeyId, secretAccessKey)';
356
+ if (onError) onError(error);
357
+ return { success: false, error };
358
+ }
359
+
360
+ try {
361
+ // Use AWS SDK v3 for Bedrock
362
+ const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
363
+
364
+ const client = new BedrockRuntimeClient({
365
+ region: region,
366
+ credentials: {
367
+ accessKeyId: accessKeyId,
368
+ secretAccessKey: secretAccessKey
369
+ }
370
+ });
371
+
372
+ // Format request based on model provider
373
+ let requestBody;
374
+ if (model.startsWith('anthropic.')) {
375
+ requestBody = {
376
+ anthropic_version: 'bedrock-2023-05-31',
377
+ max_tokens: maxTokens,
378
+ temperature: temperature,
379
+ messages: [
380
+ { role: 'user', content: prompt }
381
+ ]
382
+ };
383
+ } else if (model.startsWith('meta.')) {
384
+ requestBody = {
385
+ prompt: prompt,
386
+ temperature: temperature,
387
+ max_gen_len: maxTokens
388
+ };
389
+ } else {
390
+ return { success: false, error: `Unsupported Bedrock model: ${model}` };
391
+ }
392
+
393
+ const command = new InvokeModelWithResponseStreamCommand({
394
+ modelId: model,
395
+ contentType: 'application/json',
396
+ accept: 'application/json',
397
+ body: JSON.stringify(requestBody)
398
+ });
399
+
400
+ const response = await client.send(command);
401
+ let fullResponse = '';
402
+
403
+ for await (const event of response.body) {
404
+ if (event.chunk) {
405
+ const chunk = JSON.parse(new TextDecoder().decode(event.chunk.bytes));
406
+
407
+ let text = '';
408
+ if (chunk.delta?.text) {
409
+ text = chunk.delta.text; // Anthropic format
410
+ } else if (chunk.generation) {
411
+ text = chunk.generation; // Meta Llama format
412
+ }
413
+
414
+ if (text) {
415
+ fullResponse += text;
416
+ if (onChunk) onChunk(text);
417
+ }
418
+ }
419
+ }
420
+
421
+ if (onComplete) onComplete(fullResponse);
422
+ return { success: true, response: fullResponse, model };
423
+
424
+ } catch (error) {
425
+ const errorMsg = `AWS Bedrock error: ${error.message}`;
426
+ if (onError) onError(errorMsg);
427
+ return { success: false, error: errorMsg };
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Call Claude Code CLI
433
+ * @param {string} model - Model name (ignored, uses Claude Pro subscription)
434
+ * @param {string} prompt - Prompt to send
435
+ * @param {Object} options - Options (onChunk, onComplete, onError)
436
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
437
+ */
438
+ async callClaudeCode(model, prompt, options = {}) {
439
+ const { onChunk, onComplete, onError } = options;
440
+ const { spawn } = require('child_process');
441
+
442
+ return new Promise((resolve) => {
443
+ let fullResponse = '';
444
+ let errorOutput = '';
445
+
446
+ // Call claude CLI with the prompt
447
+ const claude = spawn('claude', ['--dangerously-skip-permissions'], {
448
+ stdio: ['pipe', 'pipe', 'pipe']
449
+ });
450
+
451
+ // Send prompt to stdin
452
+ claude.stdin.write(prompt);
453
+ claude.stdin.end();
454
+
455
+ // Capture stdout
456
+ claude.stdout.on('data', (data) => {
457
+ const chunk = data.toString();
458
+ fullResponse += chunk;
459
+ if (onChunk) onChunk(chunk);
460
+ });
461
+
462
+ // Capture stderr
463
+ claude.stderr.on('data', (data) => {
464
+ errorOutput += data.toString();
465
+ });
466
+
467
+ // Handle completion
468
+ claude.on('close', (code) => {
469
+ if (code === 0) {
470
+ if (onComplete) onComplete(fullResponse);
471
+ resolve({ success: true, response: fullResponse });
472
+ } else {
473
+ const error = `Claude CLI exited with code ${code}: ${errorOutput}`;
474
+ if (onError) onError(error);
475
+ // Check for rate limits
476
+ this.detectAndSaveRateLimit('claude-code', 'claude-code-cli', errorOutput);
477
+ resolve({ success: false, error });
478
+ }
479
+ });
480
+
481
+ // Handle spawn errors
482
+ claude.on('error', (err) => {
483
+ const error = `Failed to start Claude CLI: ${err.message}`;
484
+ if (onError) onError(error);
485
+ resolve({ success: false, error });
486
+ });
487
+ });
488
+ }
489
+
490
+ /**
491
+ * Call the Cline CLI with a prompt via stdin
492
+ */
493
+ async callCline(model, prompt, options = {}) {
494
+ const { onChunk, onComplete, onError } = options;
495
+ const { spawn } = require('child_process');
496
+
497
+ return new Promise((resolve) => {
498
+ let fullResponse = '';
499
+ let errorOutput = '';
500
+
501
+ const cline = spawn('cline', ['--dangerously-skip-permissions'], {
502
+ stdio: ['pipe', 'pipe', 'pipe']
503
+ });
504
+
505
+ cline.stdin.write(prompt);
506
+ cline.stdin.end();
507
+
508
+ cline.stdout.on('data', (data) => {
509
+ const chunk = data.toString();
510
+ fullResponse += chunk;
511
+ if (onChunk) onChunk(chunk);
512
+ });
513
+
514
+ cline.stderr.on('data', (data) => {
515
+ errorOutput += data.toString();
516
+ });
517
+
518
+ cline.on('close', (code) => {
519
+ if (code === 0) {
520
+ if (onComplete) onComplete(fullResponse);
521
+ resolve({ success: true, response: fullResponse });
522
+ } else {
523
+ const error = `Cline CLI exited with code ${code}: ${errorOutput}`;
524
+ if (onError) onError(error);
525
+ this.detectAndSaveRateLimit('cline', 'cline-cli', errorOutput);
526
+ resolve({ success: false, error });
527
+ }
528
+ });
529
+
530
+ cline.on('error', (err) => {
531
+ const error = `Failed to start Cline CLI: ${err.message}`;
532
+ if (onError) onError(error);
533
+ resolve({ success: false, error });
534
+ });
535
+ });
536
+ }
537
+
538
+ /**
539
+ * Check if Cline CLI is available
540
+ */
541
+ async isClineAvailable() {
542
+ const { spawn } = require('child_process');
543
+ return new Promise((resolve) => {
544
+ const proc = spawn('cline', ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
545
+ proc.on('close', (code) => resolve(code === 0));
546
+ proc.on('error', () => resolve(false));
547
+ setTimeout(() => { proc.kill(); resolve(false); }, 2000);
548
+ });
549
+ }
550
+
551
+ /**
552
+ * Call the OpenCode CLI with a prompt via -p flag
553
+ */
554
+ async callOpenCode(model, prompt, options = {}) {
555
+ const { onChunk, onComplete, onError } = options;
556
+ const { spawn } = require('child_process');
557
+ const path = require('path');
558
+ const os = require('os');
559
+
560
+ return new Promise((resolve) => {
561
+ let fullResponse = '';
562
+ let errorOutput = '';
563
+
564
+ // Resolve opencode binary — check well-known path first
565
+ let cmd = 'opencode';
566
+ const knownPath = path.join(os.homedir(), '.opencode', 'bin', 'opencode');
567
+ try {
568
+ require('fs').accessSync(knownPath, require('fs').constants.X_OK);
569
+ cmd = knownPath;
570
+ } catch {
571
+ // fall back to PATH lookup
572
+ }
573
+
574
+ const opencode = spawn(cmd, ['-p', prompt], {
575
+ stdio: ['ignore', 'pipe', 'pipe']
576
+ });
577
+
578
+ opencode.stdout.on('data', (data) => {
579
+ const chunk = data.toString();
580
+ fullResponse += chunk;
581
+ if (onChunk) onChunk(chunk);
582
+ });
583
+
584
+ opencode.stderr.on('data', (data) => {
585
+ errorOutput += data.toString();
586
+ });
587
+
588
+ opencode.on('close', (code) => {
589
+ if (code === 0) {
590
+ if (onComplete) onComplete(fullResponse);
591
+ resolve({ success: true, response: fullResponse });
592
+ } else {
593
+ const error = `OpenCode CLI exited with code ${code}: ${errorOutput}`;
594
+ if (onError) onError(error);
595
+ this.detectAndSaveRateLimit('opencode', 'opencode-cli', errorOutput);
596
+ resolve({ success: false, error });
597
+ }
598
+ });
599
+
600
+ opencode.on('error', (err) => {
601
+ const error = `Failed to start OpenCode CLI: ${err.message}`;
602
+ if (onError) onError(error);
603
+ resolve({ success: false, error });
604
+ });
605
+ });
606
+ }
607
+
608
+ /**
609
+ * Check if OpenCode CLI is available
610
+ * @returns {Promise<boolean>}
611
+ */
612
+ async isOpenCodeAvailable() {
613
+ const { spawn } = require('child_process');
614
+ const path = require('path');
615
+ const os = require('os');
616
+
617
+ // Try well-known path first, then fall back to PATH
618
+ let cmd = 'opencode';
619
+ const knownPath = path.join(os.homedir(), '.opencode', 'bin', 'opencode');
620
+ try {
621
+ require('fs').accessSync(knownPath, require('fs').constants.X_OK);
622
+ cmd = knownPath;
623
+ } catch {
624
+ // fall back to PATH lookup
625
+ }
626
+
627
+ return new Promise((resolve) => {
628
+ const proc = spawn(cmd, ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
629
+ proc.on('close', (code) => resolve(code === 0));
630
+ proc.on('error', () => resolve(false));
631
+ setTimeout(() => { proc.kill(); resolve(false); }, 5000);
632
+ });
633
+ }
634
+
635
+ /**
636
+ * Call the VS Code Copilot CLI with a prompt
637
+ */
638
+ async callVSCodeCopilotCLI(model, prompt, options = {}) {
639
+ const { onChunk, onComplete, onError } = options;
640
+ const { spawn } = require('child_process');
641
+ const os = require('os');
642
+
643
+ // Safe logging function to prevent EPIPE errors
644
+ const safeLog = (message) => {
645
+ try {
646
+ console.log(message);
647
+ } catch (err) {
648
+ // Ignore EPIPE errors that occur when stdout is closed
649
+ if (err.code === 'EPIPE') {
650
+ // Silently ignore - this happens during process shutdown
651
+ } else {
652
+ // Re-throw other errors
653
+ throw err;
654
+ }
655
+ }
656
+ };
657
+
658
+ safeLog(`[VS CODE COPILOT CLI] Starting call with model: ${model}`);
659
+ safeLog(`[VS CODE COPILOT CLI] Prompt: ${prompt.substring(0, 100)}...`);
660
+
661
+ // Set up environment with authentication if available
662
+ const env = { ...process.env };
663
+ if (!env.HOME) env.HOME = os.homedir();
664
+
665
+ return new Promise((resolve) => {
666
+ let fullResponse = '';
667
+ let errorOutput = '';
668
+
669
+ // Non-interactive prompt invocation
670
+ // `copilot` uses `-p/--prompt` for non-interactive mode.
671
+ const args = ['-p', String(prompt), '-s', '--no-ask-user'];
672
+ const copilot = spawn('copilot', args, {
673
+ stdio: ['ignore', 'pipe', 'pipe'],
674
+ cwd: process.cwd(),
675
+ env
676
+ });
677
+
678
+ safeLog(`[VS CODE COPILOT CLI] Spawned process with PID: ${copilot.pid}`);
679
+
680
+ copilot.stdout.on('data', (data) => {
681
+ const text = data.toString();
682
+ fullResponse += text;
683
+ safeLog(`[VS CODE COPILOT CLI] STDOUT: ${text.substring(0, 200)}...`);
684
+ if (onChunk) onChunk(text);
685
+ });
686
+
687
+ copilot.stderr.on('data', (data) => {
688
+ const text = data.toString();
689
+ errorOutput += text;
690
+ safeLog(`[VS CODE COPILOT CLI] STDERR: ${text.substring(0, 200)}...`);
691
+ });
692
+
693
+ copilot.on('close', (code) => {
694
+ safeLog(`[VS CODE COPILOT CLI] Process closed with code: ${code}`);
695
+ safeLog(`[VS CODE COPILOT CLI] Full response length: ${fullResponse.length}`);
696
+ safeLog(`[VS CODE COPILOT CLI] Error output length: ${errorOutput.length}`);
697
+ safeLog(`[VS CODE COPILOT CLI] Error output: ${errorOutput}`);
698
+
699
+ if (code === 0) {
700
+ if (onComplete) onComplete(fullResponse);
701
+ resolve({ success: true, response: fullResponse });
702
+ } else {
703
+ // Check if this is an authentication error and provide a helpful message
704
+ const isAuthError = this.checkForAuthenticationError(errorOutput);
705
+ let error = `VS Code Copilot CLI exited with code ${code}: ${errorOutput}`;
706
+
707
+ if (isAuthError) {
708
+ error = `VS Code Copilot CLI requires authentication. Run 'copilot login' to authenticate with GitHub, or set COPILOT_GITHUB_TOKEN environment variable.`;
709
+ safeLog(`[VS CODE COPILOT CLI] Authentication error detected: ${error}`);
710
+
711
+ // If we had previously marked this provider as rate limited, clear that stale state.
712
+ // Auth/setup failures should never surface as rate limit in the GUI.
713
+ try {
714
+ if (this.providerManager && typeof this.providerManager.clearProviderRateLimits === 'function') {
715
+ this.providerManager.clearProviderRateLimits('vscode-copilot-cli');
716
+ }
717
+ } catch (_) { }
718
+ }
719
+
720
+ safeLog(`[VS CODE COPILOT CLI] Error: ${error}`);
721
+ if (onError) onError(error);
722
+
723
+ // Check if this is actually a rate limit error before calling detectAndSaveRateLimit
724
+ const isRateLimitError = this.checkForRateLimitError(errorOutput);
725
+ safeLog(`[VS CODE COPILOT CLI] Is rate limit error: ${isRateLimitError}`);
726
+
727
+ if (isRateLimitError) {
728
+ this.detectAndSaveRateLimit('vscode-copilot-cli', 'copilot-cli', errorOutput);
729
+ }
730
+
731
+ resolve({ success: false, error });
732
+ }
733
+ });
734
+
735
+ copilot.on('error', (err) => {
736
+ const error = `Failed to start VS Code Copilot CLI: ${err.message}`;
737
+ safeLog(`[VS CODE COPILOT CLI] Spawn error: ${error}`);
738
+ if (onError) onError(error);
739
+ resolve({ success: false, error });
740
+ });
741
+ });
742
+ }
743
+
744
+ /**
745
+ * Check if error output indicates an authentication error
746
+ */
747
+ checkForAuthenticationError(errorOutput) {
748
+ const authIndicators = [
749
+ 'No authentication information found',
750
+ 'authentication information found',
751
+ 'not authenticated',
752
+ 'COPILOT_GITHUB_TOKEN',
753
+ 'GH_TOKEN',
754
+ 'GITHUB_TOKEN',
755
+ '/login',
756
+ 'gh auth login',
757
+ 'OAuth Token',
758
+ 'Personal Access Token'
759
+ ];
760
+
761
+ const isAuthError = authIndicators.some(indicator =>
762
+ errorOutput.includes(indicator)
763
+ );
764
+
765
+ console.log(`[AUTH CHECK] Error output: "${errorOutput}"`);
766
+ console.log(`[AUTH CHECK] Is authentication error: ${isAuthError}`);
767
+
768
+ return isAuthError;
769
+ }
770
+
771
+ /**
772
+ * Check if error output indicates a genuine rate limit error
773
+ */
774
+ checkForRateLimitError(errorOutput) {
775
+ // VS Code Copilot CLI specific rate limit indicators
776
+ const rateLimitIndicators = [
777
+ 'rate limit',
778
+ 'Rate limit',
779
+ 'too many requests',
780
+ 'Too many requests',
781
+ '429',
782
+ 'quota exceeded',
783
+ 'Quota exceeded',
784
+ 'usage limit',
785
+ 'Usage limit',
786
+ 'limit reached',
787
+ 'Limit reached',
788
+ 'weekly limit',
789
+ 'Weekly limit',
790
+ 'daily limit',
791
+ 'Daily limit'
792
+ ];
793
+
794
+ // Exclude common authentication and setup errors that are NOT rate limits
795
+ const nonRateLimitIndicators = [
796
+ 'authentication information found',
797
+ 'Authentication information found',
798
+ 'No authentication',
799
+ 'not authenticated',
800
+ 'COPILOT_GITHUB_TOKEN',
801
+ 'GH_TOKEN',
802
+ 'GITHUB_TOKEN',
803
+ '/login',
804
+ 'gh auth login',
805
+ 'OAuth Token',
806
+ 'Personal Access Token',
807
+ 'GitHub CLI'
808
+ ];
809
+
810
+ // First check if it contains non-rate-limit indicators
811
+ const isNonRateLimit = nonRateLimitIndicators.some(indicator =>
812
+ errorOutput.includes(indicator)
813
+ );
814
+
815
+ if (isNonRateLimit) {
816
+ console.log(`[RATE LIMIT CHECK] Contains non-rate-limit indicators, not a rate limit`);
817
+ return false;
818
+ }
819
+
820
+ // Only consider it a rate limit if it contains specific rate limit indicators
821
+ const isRateLimit = rateLimitIndicators.some(indicator =>
822
+ errorOutput.includes(indicator)
823
+ );
824
+
825
+ console.log(`[RATE LIMIT CHECK] Error output: "${errorOutput}"`);
826
+ console.log(`[RATE LIMIT CHECK] Is rate limit: ${isRateLimit}`);
827
+
828
+ return isRateLimit;
829
+ }
830
+
831
+ /**
832
+ * Check if VS Code Copilot CLI is available AND authenticated
833
+ * @returns {Promise<{available: boolean, needsAuth: boolean, authMethod?: string}>}
834
+ */
835
+ async isVSCodeCopilotCLIAvailable() {
836
+ const { spawn } = require('child_process');
837
+ const os = require('os');
838
+
839
+ // Safe logging function to prevent EPIPE errors
840
+ const safeLog = (message) => {
841
+ try {
842
+ console.log(message);
843
+ } catch (err) {
844
+ // Ignore EPIPE errors that occur when stdout is closed
845
+ if (err.code === 'EPIPE') {
846
+ // Silently ignore - this happens during process shutdown
847
+ } else {
848
+ // Re-throw other errors
849
+ throw err;
850
+ }
851
+ }
852
+ };
853
+
854
+ safeLog(`[VS CODE COPILOT CLI] Checking availability and authentication...`);
855
+
856
+ return new Promise((resolve) => {
857
+ // First check if the CLI is installed
858
+ const baseEnv = { ...process.env };
859
+ if (!baseEnv.HOME) baseEnv.HOME = os.homedir();
860
+
861
+ const versionProc = spawn('copilot', ['--version'], { stdio: ['ignore', 'pipe', 'pipe'], env: baseEnv });
862
+
863
+ let versionStdout = '';
864
+ let versionStderr = '';
865
+ let versionTimeout;
866
+
867
+ versionProc.stdout.on('data', (data) => {
868
+ versionStdout += data.toString();
869
+ safeLog(`[VS CODE COPILOT CLI] Version check STDOUT: ${data.toString().trim()}`);
870
+ });
871
+
872
+ versionProc.stderr.on('data', (data) => {
873
+ versionStderr += data.toString();
874
+ safeLog(`[VS CODE COPILOT CLI] Version check STDERR: ${data.toString().trim()}`);
875
+ });
876
+
877
+ versionProc.on('close', (versionCode) => {
878
+ clearTimeout(versionTimeout);
879
+ safeLog(`[VS CODE COPILOT CLI] Version check exited with code: ${versionCode}`);
880
+
881
+ if (versionCode !== 0) {
882
+ safeLog(`[VS CODE COPILOT CLI] Not installed or not in PATH`);
883
+ resolve({ available: false, needsAuth: false });
884
+ return;
885
+ }
886
+
887
+ // CLI is installed, now check if it's authenticated using a short non-interactive prompt.
888
+ // Note: This CLI does not support `copilot whoami`, and GitHub CLI (`gh`) may not be installed.
889
+ // We keep this probe short and interpret device-flow output as needsAuth.
890
+ safeLog(`[VS CODE COPILOT CLI] CLI is installed, checking authentication (non-interactive probe)...`);
891
+
892
+ const probeArgs = ['-p', 'Reply with OK', '-s', '--no-ask-user'];
893
+ const probeProc = spawn('copilot', probeArgs, { stdio: ['ignore', 'pipe', 'pipe'], env: baseEnv });
894
+
895
+ let probeStdout = '';
896
+ let probeStderr = '';
897
+ let probeFinished = false;
898
+ const finishProbe = (code) => {
899
+ if (probeFinished) return;
900
+ probeFinished = true;
901
+ const out = (probeStdout || '').trim();
902
+ const err = (probeStderr || '').trim();
903
+ safeLog(`[VS CODE COPILOT CLI] Probe exited with code: ${code}`);
904
+ if (out) safeLog(`[VS CODE COPILOT CLI] Probe STDOUT: ${out.substring(0, 200)}`);
905
+ if (err) safeLog(`[VS CODE COPILOT CLI] Probe STDERR: ${err.substring(0, 200)}`);
906
+
907
+ // For copilot CLI, we consider it working if we get "OK" output even with exit code 1
908
+ // The --no-ask-user flag seems to cause exit code 1 but still provides the response
909
+ const isWorking = (code === 0 && out) || (code === 1 && out.trim() === 'OK');
910
+
911
+ if (isWorking) {
912
+ resolve({ available: true, needsAuth: false, authMethod: 'existing' });
913
+ return;
914
+ }
915
+
916
+ const combined = `${out}\n${err}`;
917
+
918
+ // Check for rate limit first
919
+ const isRateLimited =
920
+ combined.includes('402 You have no quota') ||
921
+ combined.includes('quota') ||
922
+ combined.includes('rate limit') ||
923
+ combined.includes('Rate limit');
924
+
925
+ if (isRateLimited) {
926
+ safeLog(`[VS CODE COPILOT CLI] Detected rate limit error`);
927
+ resolve({ available: true, needsAuth: false, authMethod: 'existing', rateLimited: true });
928
+ return;
929
+ }
930
+
931
+ const needsAuth =
932
+ combined.includes('copilot login') ||
933
+ combined.includes('Authenticate with Copilot') ||
934
+ combined.includes('github.com/login/device') ||
935
+ combined.includes('To authenticate') ||
936
+ combined.includes('Waiting for authorization');
937
+
938
+ resolve({ available: true, needsAuth: Boolean(needsAuth), authMethod: needsAuth ? 'manual' : 'unknown' });
939
+ };
940
+
941
+ probeProc.stdout.on('data', (data) => { probeStdout += data.toString(); });
942
+ probeProc.stderr.on('data', (data) => { probeStderr += data.toString(); });
943
+ probeProc.on('close', (code) => finishProbe(code));
944
+ probeProc.on('error', () => finishProbe(1));
945
+ setTimeout(() => {
946
+ try { probeProc.kill(); } catch (_) { }
947
+ finishProbe(1);
948
+ }, 8000);
949
+ });
950
+
951
+ versionProc.on('error', (err) => {
952
+ clearTimeout(versionTimeout);
953
+ safeLog(`[VS CODE COPILOT CLI] Version check error: ${err.message}`);
954
+ resolve({ available: false, needsAuth: false });
955
+ });
956
+
957
+ versionTimeout = setTimeout(() => {
958
+ safeLog(`[VS CODE COPILOT CLI] Version check timeout, killing process`);
959
+ versionProc.kill();
960
+ resolve({ available: false, needsAuth: false });
961
+ }, 5000);
962
+ });
963
+ }
964
+
965
+ /**
966
+ * Attempt to authenticate VS Code Copilot CLI automatically
967
+ * @returns {Promise<{success: boolean, method: string, reason?: string}>}
968
+ */
969
+ async attemptAutoAuthentication() {
970
+ const { spawn } = require('child_process');
971
+
972
+ // Safe logging function to prevent EPIPE errors
973
+ const safeLog = (message) => {
974
+ try {
975
+ console.log(message);
976
+ } catch (err) {
977
+ // Ignore EPIPE errors that occur when stdout is closed
978
+ if (err.code === 'EPIPE') {
979
+ // Silently ignore - this happens during process shutdown
980
+ } else {
981
+ // Re-throw other errors
982
+ throw err;
983
+ }
984
+ }
985
+ };
986
+
987
+ safeLog(`[VS CODE COPILOT CLI] Attempting auto-authentication...`);
988
+
989
+ // Method 1: Check if GitHub CLI is authenticated and get token
990
+ try {
991
+ safeLog(`[VS CODE COPILOT CLI] Method 1: Checking GitHub CLI authentication...`);
992
+ const ghAuth = spawn('gh', ['auth', 'status'], { stdio: ['ignore', 'pipe', 'pipe'] });
993
+
994
+ let ghStdout = '';
995
+ let ghStderr = '';
996
+
997
+ ghAuth.stdout.on('data', (data) => {
998
+ ghStdout += data.toString();
999
+ });
1000
+
1001
+ ghAuth.stderr.on('data', (data) => {
1002
+ ghStderr += data.toString();
1003
+ });
1004
+
1005
+ const ghResult = await new Promise((resolve) => {
1006
+ ghAuth.on('close', (code) => {
1007
+ resolve({ code, stdout: ghStdout, stderr: ghStderr });
1008
+ });
1009
+
1010
+ ghAuth.on('error', () => {
1011
+ resolve({ code: -1, stdout: '', stderr: 'gh command not found' });
1012
+ });
1013
+
1014
+ setTimeout(() => { ghAuth.kill(); resolve({ code: -1, stdout: '', stderr: 'timeout' }); }, 5000);
1015
+ });
1016
+
1017
+ if (ghResult.code === 0 && ghResult.stdout.includes('Logged in to')) {
1018
+ safeLog(`[VS CODE COPILOT CLI] GitHub CLI is authenticated, getting token...`);
1019
+
1020
+ // Get token from GitHub CLI
1021
+ const ghToken = spawn('gh', ['auth', 'token'], { stdio: ['ignore', 'pipe', 'pipe'] });
1022
+
1023
+ let tokenStdout = '';
1024
+ let tokenStderr = '';
1025
+
1026
+ ghToken.stdout.on('data', (data) => {
1027
+ tokenStdout += data.toString();
1028
+ });
1029
+
1030
+ ghToken.stderr.on('data', (data) => {
1031
+ tokenStderr += data.toString();
1032
+ });
1033
+
1034
+ const tokenResult = await new Promise((resolve) => {
1035
+ ghToken.on('close', (code) => {
1036
+ resolve({ code, stdout: tokenStdout, stderr: tokenStderr });
1037
+ });
1038
+
1039
+ ghToken.on('error', () => {
1040
+ resolve({ code: 1, stdout: '', stderr: 'Failed to spawn gh auth token' });
1041
+ });
1042
+
1043
+ setTimeout(() => {
1044
+ try { ghToken.kill(); } catch (_) { }
1045
+ resolve({ code: 1, stdout: '', stderr: 'Timeout getting token' });
1046
+ }, 5000);
1047
+ });
1048
+
1049
+ if (tokenResult.code === 0 && tokenResult.stdout) {
1050
+ safeLog(`[VS CODE COPILOT CLI] Got token from GitHub CLI, testing with Copilot CLI...`);
1051
+
1052
+ // Test the token with Copilot CLI
1053
+ const testResult = await this.testTokenWithCopilot(tokenResult.stdout);
1054
+ if (testResult.success) {
1055
+ return { success: true, method: 'github-cli' };
1056
+ } else {
1057
+ safeLog(`[VS CODE COPILOT CLI] GitHub CLI token failed with Copilot: ${testResult.reason}`);
1058
+ }
1059
+ }
1060
+ }
1061
+ } catch (error) {
1062
+ safeLog(`[VS CODE COPILOT CLI] GitHub CLI method failed: ${error.message}`);
1063
+ }
1064
+
1065
+ // Method 2: Check environment variables
1066
+ safeLog(`[VS CODE COPILOT CLI] Method 2: Checking environment variables...`);
1067
+ const envVars = ['COPILOT_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN'];
1068
+
1069
+ for (const envVar of envVars) {
1070
+ const token = process.env[envVar];
1071
+ if (token) {
1072
+ safeLog(`[VS CODE COPILOT CLI] Found ${envVar}, testing with Copilot CLI...`);
1073
+
1074
+ const testResult = await this.testTokenWithCopilot(token);
1075
+ if (testResult.success) {
1076
+ return { success: true, method: `env-${envVar}` };
1077
+ } else {
1078
+ safeLog(`[VS CODE COPILOT CLI] ${envVar} token failed with Copilot: ${testResult.reason}`);
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+ safeLog(`[VS CODE COPILOT CLI] All auto-authentication methods failed`);
1084
+ return { success: false, method: 'none', reason: 'No valid authentication found' };
1085
+ }
1086
+
1087
+ /**
1088
+ * Test if a token works with VS Code Copilot CLI
1089
+ * @param {string} token - GitHub token to test
1090
+ * @returns {Promise<{success: boolean, reason?: string}>}
1091
+ */
1092
+ async testTokenWithCopilot(token) {
1093
+ const { spawn } = require('child_process');
1094
+
1095
+ return new Promise((resolve) => {
1096
+ const env = { ...process.env, COPILOT_GITHUB_TOKEN: token };
1097
+
1098
+ const testProc = spawn('copilot', ['-p', 'test', '-s'], {
1099
+ stdio: ['ignore', 'pipe', 'pipe'],
1100
+ env,
1101
+ timeout: 5000
1102
+ });
1103
+
1104
+ let stderr = '';
1105
+
1106
+ testProc.stderr.on('data', (data) => {
1107
+ stderr += data.toString();
1108
+ });
1109
+
1110
+ testProc.on('close', (code) => {
1111
+ const needsAuth = stderr.includes('No authentication information found') ||
1112
+ stderr.includes('authentication information found') ||
1113
+ stderr.includes('not authenticated');
1114
+
1115
+ if (needsAuth) {
1116
+ resolve({ success: false, reason: 'Token not valid for Copilot CLI' });
1117
+ } else {
1118
+ resolve({ success: true });
1119
+ }
1120
+ });
1121
+
1122
+ testProc.on('error', (err) => {
1123
+ resolve({ success: false, reason: err.message });
1124
+ });
1125
+
1126
+ setTimeout(() => {
1127
+ testProc.kill();
1128
+ resolve({ success: false, reason: 'timeout' });
1129
+ }, 5000);
1130
+ });
1131
+ }
1132
+
1133
+ /**
1134
+ * Call any LLM provider
1135
+ * @param {Object} config - Provider configuration
1136
+ * @param {string} prompt - Prompt to send
1137
+ * @param {Object} options - Options
1138
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
1139
+ */
1140
+ async call(config, prompt, options = {}) {
1141
+ const { provider, model, apiKey, region, accessKeyId, secretAccessKey, fallbackModels = [] } = config;
1142
+ const modelsToTry = [model, ...fallbackModels];
1143
+ let lastError = null;
1144
+
1145
+ for (const currentModel of modelsToTry) {
1146
+ if (currentModel !== model) {
1147
+ this.logger.log(`⚠️ Quota/Limit reached for previous model, failing over to ${currentModel}...`);
1148
+ }
1149
+
1150
+ const agentId = `${provider}:${currentModel}`;
1151
+ try {
1152
+ const quota = await quotaManagement.fetchQuotaForAgent(agentId);
1153
+ if (quota.isExceeded()) {
1154
+ const errorMessage = `Quota limit reached for ${currentModel}. Resets at ${quota.resetsAt ? quota.resetsAt.toLocaleString() : 'a later time'}.`;
1155
+ lastError = { success: false, error: errorMessage };
1156
+ continue; // Try next model
1157
+ }
1158
+ } catch (error) {
1159
+ this.logger.error(`Failed to check quota for ${agentId}: ${error.message}`);
1160
+ }
1161
+
1162
+ const currentConfig = { ...config, model: currentModel };
1163
+ let result;
1164
+
1165
+ switch (provider) {
1166
+ case 'ollama':
1167
+ result = await this.callOllama(currentModel, prompt, options);
1168
+ break;
1169
+ case 'anthropic':
1170
+ result = await this.callAnthropic(currentModel, prompt, { ...options, apiKey });
1171
+ break;
1172
+ case 'groq':
1173
+ result = await this.callGroq(currentModel, prompt, { ...options, apiKey });
1174
+ break;
1175
+ case 'bedrock':
1176
+ result = await this.callBedrock(currentModel, prompt, { ...options, region, accessKeyId, secretAccessKey });
1177
+ break;
1178
+ case 'claude-code':
1179
+ result = await this.callClaudeCode(currentModel, prompt, options);
1180
+ break;
1181
+ case 'cline':
1182
+ result = await this.callCline(currentModel, prompt, options);
1183
+ break;
1184
+ case 'opencode':
1185
+ result = await this.callOpenCode(currentModel, prompt, options);
1186
+ break;
1187
+ case 'vscode-copilot-cli':
1188
+ result = await this.callVSCodeCopilotCLI(currentModel, prompt, options);
1189
+ break;
1190
+ default:
1191
+ return { success: false, error: `Unknown provider: ${provider}` };
1192
+ }
1193
+
1194
+ if (result.success) {
1195
+ return result;
1196
+ }
1197
+
1198
+ // If failed, check for rate limit to save it
1199
+ this.detectAndSaveRateLimit(provider, currentModel, result.error || '');
1200
+ lastError = result;
1201
+
1202
+ // If it's a "fatal" error that isn't a rate limit, we might want to stop?
1203
+ // But usually we want to try the next model if possible.
1204
+ }
1205
+
1206
+ return lastError || { success: false, error: `All models for ${provider} failed.` };
1207
+ }
1208
+
1209
+ /**
1210
+ * Check if Ollama is available
1211
+ * @returns {Promise<boolean>}
1212
+ */
1213
+ async isOllamaAvailable() {
1214
+ return new Promise((resolve) => {
1215
+ const req = http.request({
1216
+ hostname: 'localhost',
1217
+ port: 11434,
1218
+ path: '/api/tags',
1219
+ method: 'GET',
1220
+ timeout: 2000
1221
+ }, (res) => {
1222
+ resolve(res.statusCode === 200);
1223
+ });
1224
+
1225
+ req.on('error', () => resolve(false));
1226
+ req.on('timeout', () => {
1227
+ req.destroy();
1228
+ resolve(false);
1229
+ });
1230
+
1231
+ req.end();
1232
+ });
1233
+ }
1234
+
1235
+ /**
1236
+ * Check if Claude Code CLI is available
1237
+ * @returns {Promise<boolean>}
1238
+ */
1239
+ async isClaudeCodeAvailable() {
1240
+ const { spawn } = require('child_process');
1241
+
1242
+ return new Promise((resolve) => {
1243
+ const claude = spawn('claude', ['--version'], {
1244
+ stdio: ['ignore', 'pipe', 'pipe']
1245
+ });
1246
+
1247
+ claude.on('close', (code) => {
1248
+ resolve(code === 0);
1249
+ });
1250
+
1251
+ claude.on('error', () => {
1252
+ resolve(false);
1253
+ });
1254
+
1255
+ // Timeout after 2 seconds
1256
+ setTimeout(() => {
1257
+ claude.kill();
1258
+ resolve(false);
1259
+ }, 2000);
1260
+ });
1261
+ }
1262
+
1263
+ /**
1264
+ * Get list of installed Ollama models
1265
+ * @returns {Promise<string[]>}
1266
+ */
1267
+ async getOllamaModels() {
1268
+ return new Promise((resolve) => {
1269
+ const req = http.request({
1270
+ hostname: 'localhost',
1271
+ port: 11434,
1272
+ path: '/api/tags',
1273
+ method: 'GET'
1274
+ }, (res) => {
1275
+ let data = '';
1276
+
1277
+ res.on('data', (chunk) => {
1278
+ data += chunk.toString();
1279
+ });
1280
+
1281
+ res.on('end', () => {
1282
+ try {
1283
+ const json = JSON.parse(data);
1284
+ const models = json.models?.map(m => m.name) || [];
1285
+ resolve(models);
1286
+ } catch (err) {
1287
+ resolve([]);
1288
+ }
1289
+ });
1290
+ });
1291
+
1292
+ req.on('error', () => resolve([]));
1293
+ req.end();
1294
+ });
1295
+ }
1296
+ }
1297
+
1298
+ module.exports = DirectLLMManager;
1299
+