vibecodingmachine-core 2026.3.9-907 → 2026.3.10-1548
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/auth/access-denied.html +119 -119
- package/src/auth/shared-auth-storage.js +267 -267
- package/src/autonomous-mode/feature-implementer.cjs +70 -70
- package/src/autonomous-mode/feature-implementer.js +425 -425
- package/src/beta-request.js +160 -160
- package/src/chat-management/chat-manager.cjs +71 -71
- package/src/chat-management/chat-manager.js +342 -342
- package/src/compliance/compliance-prompt.js +183 -183
- package/src/ide-integration/aider-cli-manager.cjs +850 -850
- package/src/ide-integration/applescript-manager.cjs +3215 -3215
- package/src/ide-integration/applescript-utils.js +314 -314
- package/src/ide-integration/cdp-manager.cjs +221 -221
- package/src/ide-integration/claude-code-cli-manager.cjs +456 -456
- package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
- package/src/ide-integration/continue-cli-manager.js +431 -431
- package/src/ide-integration/provider-manager.cjs +595 -595
- package/src/ide-integration/quota-detector.cjs +399 -399
- package/src/ide-integration/windows-automation-manager.js +532 -4
- package/src/ide-integration/windows-ide-manager.js +12 -3
- package/src/index.cjs +142 -142
- package/src/llm/direct-llm-manager.cjs +1299 -1299
- package/src/localization/index.js +147 -147
- package/src/quota-management/index.js +108 -108
- package/src/requirement-numbering.js +164 -164
- package/src/sync/aws-setup.js +445 -445
- package/src/ui/ButtonComponents.js +247 -247
- package/src/ui/ChatInterface.js +499 -499
- package/src/ui/StateManager.js +259 -259
- package/src/utils/audit-logger.cjs +116 -116
- package/src/utils/config-helpers.cjs +94 -94
- package/src/utils/config-helpers.js +94 -94
- package/src/utils/env-helpers.js +54 -54
- package/src/utils/error-reporter.js +117 -117
- package/src/utils/gcloud-auth.cjs +394 -394
- package/src/utils/git-branch-manager.js +278 -278
- package/src/utils/logger.cjs +193 -193
- package/src/utils/logger.js +191 -191
- package/src/utils/repo-helpers.cjs +120 -120
- package/src/utils/repo-helpers.js +120 -120
- package/src/utils/update-checker.js +246 -246
- 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
|
+
|