vibecodingmachine-core 1.0.2 → 2025.11.2-7.1302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.babelrc +13 -13
  2. package/README.md +28 -28
  3. package/__tests__/applescript-manager-claude-fix.test.js +286 -286
  4. package/__tests__/requirement-2-auto-start-looping.test.js +69 -69
  5. package/__tests__/requirement-3-auto-start-looping.test.js +69 -69
  6. package/__tests__/requirement-4-auto-start-looping.test.js +69 -69
  7. package/__tests__/requirement-6-auto-start-looping.test.js +73 -73
  8. package/__tests__/requirement-7-status-tracking.test.js +332 -332
  9. package/jest.config.js +18 -18
  10. package/jest.setup.js +12 -12
  11. package/package.json +48 -48
  12. package/src/auth/access-denied.html +119 -119
  13. package/src/auth/shared-auth-storage.js +230 -230
  14. package/src/autonomous-mode/feature-implementer.cjs +70 -70
  15. package/src/autonomous-mode/feature-implementer.js +425 -425
  16. package/src/chat-management/chat-manager.cjs +71 -71
  17. package/src/chat-management/chat-manager.js +342 -342
  18. package/src/ide-integration/__tests__/applescript-manager-thread-closure.test.js +227 -227
  19. package/src/ide-integration/aider-cli-manager.cjs +850 -850
  20. package/src/ide-integration/applescript-manager.cjs +1088 -1088
  21. package/src/ide-integration/applescript-manager.js +2802 -2802
  22. package/src/ide-integration/applescript-utils.js +306 -306
  23. package/src/ide-integration/cdp-manager.cjs +221 -221
  24. package/src/ide-integration/cdp-manager.js +321 -321
  25. package/src/ide-integration/claude-code-cli-manager.cjs +301 -301
  26. package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
  27. package/src/ide-integration/continue-cli-manager.js +431 -431
  28. package/src/ide-integration/provider-manager.cjs +354 -354
  29. package/src/ide-integration/quota-detector.cjs +34 -34
  30. package/src/ide-integration/quota-detector.js +349 -349
  31. package/src/ide-integration/windows-automation-manager.js +262 -262
  32. package/src/index.cjs +47 -43
  33. package/src/index.js +17 -17
  34. package/src/llm/direct-llm-manager.cjs +609 -609
  35. package/src/ui/ButtonComponents.js +247 -247
  36. package/src/ui/ChatInterface.js +499 -499
  37. package/src/ui/StateManager.js +259 -259
  38. package/src/utils/audit-logger.cjs +116 -116
  39. package/src/utils/config-helpers.cjs +94 -94
  40. package/src/utils/config-helpers.js +94 -94
  41. package/src/utils/electron-update-checker.js +113 -85
  42. package/src/utils/gcloud-auth.cjs +394 -394
  43. package/src/utils/logger.cjs +193 -193
  44. package/src/utils/logger.js +191 -191
  45. package/src/utils/repo-helpers.cjs +120 -120
  46. package/src/utils/repo-helpers.js +120 -120
  47. package/src/utils/requirement-helpers.js +432 -432
  48. package/src/utils/update-checker.js +227 -167
  49. package/src/utils/version-checker.js +169 -0
@@ -1,609 +1,609 @@
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
-
9
- class DirectLLMManager {
10
- constructor(sharedProviderManager = null) {
11
- this.logger = console;
12
- // Use shared ProviderManager if provided, otherwise create new instance
13
- // IMPORTANT: Pass shared instance to maintain rate limit state across calls
14
- if (sharedProviderManager) {
15
- this.providerManager = sharedProviderManager;
16
- } else {
17
- try {
18
- const ProviderManager = require('../ide-integration/provider-manager.cjs');
19
- this.providerManager = new ProviderManager();
20
- } catch (err) {
21
- this.providerManager = null;
22
- }
23
- }
24
- }
25
-
26
- /**
27
- * Detect and save rate limit from error message
28
- * @param {string} provider - Provider name
29
- * @param {string} model - Model name
30
- * @param {string} errorMessage - Error message from API
31
- */
32
- detectAndSaveRateLimit(provider, model, errorMessage) {
33
- if (!this.providerManager) return;
34
-
35
- // Check for rate limit indicators
36
- const isRateLimit = errorMessage.includes('rate limit') ||
37
- errorMessage.includes('Rate limit') ||
38
- errorMessage.includes('too many requests') ||
39
- errorMessage.includes('429') ||
40
- errorMessage.includes('quota') ||
41
- errorMessage.includes('Weekly limit reached') ||
42
- errorMessage.includes('Daily limit reached') ||
43
- errorMessage.includes('limit reached');
44
-
45
- if (isRateLimit) {
46
- this.providerManager.markRateLimited(provider, model, errorMessage);
47
- }
48
- }
49
-
50
- /**
51
- * Call Ollama API directly (local)
52
- * @param {string} model - Model name (e.g., "qwen2.5-coder:32b")
53
- * @param {string} prompt - Prompt to send
54
- * @param {Object} options - Options (onChunk, onComplete, onError)
55
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
56
- */
57
- async callOllama(model, prompt, options = {}) {
58
- const { onChunk, onComplete, onError, temperature = 0.2 } = options;
59
-
60
- return new Promise((resolve) => {
61
- let fullResponse = '';
62
-
63
- const postData = JSON.stringify({
64
- model: model,
65
- prompt: prompt,
66
- stream: true,
67
- options: {
68
- temperature: temperature
69
- }
70
- });
71
-
72
- const req = http.request({
73
- hostname: 'localhost',
74
- port: 11434,
75
- path: '/api/generate',
76
- method: 'POST',
77
- headers: {
78
- 'Content-Type': 'application/json',
79
- 'Content-Length': Buffer.byteLength(postData)
80
- }
81
- }, (res) => {
82
- let buffer = '';
83
-
84
- res.on('data', (chunk) => {
85
- buffer += chunk.toString();
86
- const lines = buffer.split('\n');
87
- buffer = lines.pop(); // Keep incomplete line in buffer
88
-
89
- for (const line of lines) {
90
- if (!line.trim()) continue;
91
-
92
- try {
93
- const data = JSON.parse(line);
94
- if (data.response) {
95
- fullResponse += data.response;
96
- if (onChunk) onChunk(data.response);
97
- }
98
-
99
- if (data.done) {
100
- if (onComplete) onComplete(fullResponse);
101
- resolve({
102
- success: true,
103
- response: fullResponse,
104
- model: data.model,
105
- context: data.context
106
- });
107
- }
108
- } catch (err) {
109
- // Ignore JSON parse errors for partial chunks
110
- }
111
- }
112
- });
113
-
114
- res.on('end', () => {
115
- if (!fullResponse) {
116
- const error = 'No response received from Ollama';
117
- if (onError) onError(error);
118
- resolve({ success: false, error });
119
- }
120
- });
121
- });
122
-
123
- req.on('error', (error) => {
124
- const errorMsg = `Ollama API error: ${error.message}`;
125
- if (onError) onError(errorMsg);
126
- resolve({ success: false, error: errorMsg });
127
- });
128
-
129
- req.write(postData);
130
- req.end();
131
- });
132
- }
133
-
134
- /**
135
- * Call Anthropic API directly
136
- * @param {string} model - Model name (e.g., "claude-sonnet-4-20250514")
137
- * @param {string} prompt - Prompt to send
138
- * @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
139
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
140
- */
141
- async callAnthropic(model, prompt, options = {}) {
142
- const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
143
-
144
- if (!apiKey) {
145
- const error = 'Anthropic API key required';
146
- if (onError) onError(error);
147
- return { success: false, error };
148
- }
149
-
150
- return new Promise((resolve) => {
151
- let fullResponse = '';
152
-
153
- const postData = JSON.stringify({
154
- model: model,
155
- max_tokens: maxTokens,
156
- temperature: temperature,
157
- messages: [
158
- { role: 'user', content: prompt }
159
- ],
160
- stream: true
161
- });
162
-
163
- const req = https.request({
164
- hostname: 'api.anthropic.com',
165
- path: '/v1/messages',
166
- method: 'POST',
167
- headers: {
168
- 'Content-Type': 'application/json',
169
- 'x-api-key': apiKey,
170
- 'anthropic-version': '2023-06-01',
171
- 'Content-Length': Buffer.byteLength(postData)
172
- }
173
- }, (res) => {
174
- let buffer = '';
175
-
176
- res.on('data', (chunk) => {
177
- buffer += chunk.toString();
178
- const lines = buffer.split('\n');
179
- buffer = lines.pop();
180
-
181
- for (const line of lines) {
182
- if (!line.trim() || !line.startsWith('data: ')) continue;
183
-
184
- try {
185
- const jsonStr = line.slice(6); // Remove "data: " prefix
186
- if (jsonStr === '[DONE]') continue;
187
-
188
- const data = JSON.parse(jsonStr);
189
-
190
- if (data.type === 'content_block_delta' && data.delta?.text) {
191
- fullResponse += data.delta.text;
192
- if (onChunk) onChunk(data.delta.text);
193
- } else if (data.type === 'message_stop') {
194
- if (onComplete) onComplete(fullResponse);
195
- resolve({
196
- success: true,
197
- response: fullResponse,
198
- model: model
199
- });
200
- }
201
- } catch (err) {
202
- // Ignore JSON parse errors
203
- }
204
- }
205
- });
206
-
207
- res.on('end', () => {
208
- if (!fullResponse) {
209
- const error = 'No response received from Anthropic';
210
- if (onError) onError(error);
211
- resolve({ success: false, error });
212
- }
213
- });
214
- });
215
-
216
- req.on('error', (error) => {
217
- const errorMsg = `Anthropic API error: ${error.message}`;
218
- if (onError) onError(errorMsg);
219
- resolve({ success: false, error: errorMsg });
220
- });
221
-
222
- req.write(postData);
223
- req.end();
224
- });
225
- }
226
-
227
- /**
228
- * Call Groq API directly
229
- * @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
230
- * @param {string} prompt - Prompt to send
231
- * @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
232
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
233
- */
234
- async callGroq(model, prompt, options = {}) {
235
- const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
236
-
237
- if (!apiKey) {
238
- const error = 'Groq API key required';
239
- if (onError) onError(error);
240
- return { success: false, error };
241
- }
242
-
243
- return new Promise((resolve) => {
244
- let fullResponse = '';
245
-
246
- const postData = JSON.stringify({
247
- model: model,
248
- messages: [
249
- { role: 'user', content: prompt }
250
- ],
251
- temperature: temperature,
252
- max_tokens: maxTokens,
253
- stream: true
254
- });
255
-
256
- const req = https.request({
257
- hostname: 'api.groq.com',
258
- path: '/openai/v1/chat/completions',
259
- method: 'POST',
260
- headers: {
261
- 'Content-Type': 'application/json',
262
- 'Authorization': `Bearer ${apiKey}`,
263
- 'Content-Length': Buffer.byteLength(postData)
264
- }
265
- }, (res) => {
266
- let buffer = '';
267
- let statusCode = res.statusCode;
268
-
269
- // Check for rate limit or error status codes
270
- if (statusCode === 429 || statusCode >= 400) {
271
- let errorBody = '';
272
- res.on('data', (chunk) => {
273
- errorBody += chunk.toString();
274
- });
275
- res.on('end', () => {
276
- const errorMsg = `Groq API error (${statusCode}): ${errorBody || 'No error details'}`;
277
- this.detectAndSaveRateLimit('groq', model, errorMsg);
278
- if (onError) onError(errorMsg);
279
- resolve({ success: false, error: errorMsg });
280
- });
281
- return;
282
- }
283
-
284
- res.on('data', (chunk) => {
285
- buffer += chunk.toString();
286
- const lines = buffer.split('\n');
287
- buffer = lines.pop();
288
-
289
- for (const line of lines) {
290
- if (!line.trim() || !line.startsWith('data: ')) continue;
291
-
292
- try {
293
- const jsonStr = line.slice(6);
294
- if (jsonStr === '[DONE]') {
295
- if (onComplete) onComplete(fullResponse);
296
- resolve({
297
- success: true,
298
- response: fullResponse,
299
- model: model
300
- });
301
- return;
302
- }
303
-
304
- const data = JSON.parse(jsonStr);
305
- const content = data.choices?.[0]?.delta?.content;
306
-
307
- if (content) {
308
- fullResponse += content;
309
- if (onChunk) onChunk(content);
310
- }
311
- } catch (err) {
312
- // Ignore JSON parse errors
313
- }
314
- }
315
- });
316
-
317
- res.on('end', () => {
318
- if (fullResponse) {
319
- if (onComplete) onComplete(fullResponse);
320
- resolve({ success: true, response: fullResponse, model });
321
- } else {
322
- const error = buffer || 'No response received from Groq';
323
- this.detectAndSaveRateLimit('groq', model, error);
324
- if (onError) onError(error);
325
- resolve({ success: false, error });
326
- }
327
- });
328
- });
329
-
330
- req.on('error', (error) => {
331
- const errorMsg = `Groq API error: ${error.message}`;
332
- this.detectAndSaveRateLimit('groq', model, errorMsg);
333
- if (onError) onError(errorMsg);
334
- resolve({ success: false, error: errorMsg });
335
- });
336
-
337
- req.write(postData);
338
- req.end();
339
- });
340
- }
341
-
342
- /**
343
- * Call AWS Bedrock API directly
344
- * @param {string} model - Model ID (e.g., "anthropic.claude-sonnet-4-v1")
345
- * @param {string} prompt - Prompt to send
346
- * @param {Object} options - Options (region, accessKeyId, secretAccessKey, onChunk, onComplete, onError)
347
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
348
- */
349
- async callBedrock(model, prompt, options = {}) {
350
- const { region, accessKeyId, secretAccessKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
351
-
352
- if (!region || !accessKeyId || !secretAccessKey) {
353
- const error = 'AWS credentials required (region, accessKeyId, secretAccessKey)';
354
- if (onError) onError(error);
355
- return { success: false, error };
356
- }
357
-
358
- try {
359
- // Use AWS SDK v3 for Bedrock
360
- const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
361
-
362
- const client = new BedrockRuntimeClient({
363
- region: region,
364
- credentials: {
365
- accessKeyId: accessKeyId,
366
- secretAccessKey: secretAccessKey
367
- }
368
- });
369
-
370
- // Format request based on model provider
371
- let requestBody;
372
- if (model.startsWith('anthropic.')) {
373
- requestBody = {
374
- anthropic_version: 'bedrock-2023-05-31',
375
- max_tokens: maxTokens,
376
- temperature: temperature,
377
- messages: [
378
- { role: 'user', content: prompt }
379
- ]
380
- };
381
- } else if (model.startsWith('meta.')) {
382
- requestBody = {
383
- prompt: prompt,
384
- temperature: temperature,
385
- max_gen_len: maxTokens
386
- };
387
- } else {
388
- return { success: false, error: `Unsupported Bedrock model: ${model}` };
389
- }
390
-
391
- const command = new InvokeModelWithResponseStreamCommand({
392
- modelId: model,
393
- contentType: 'application/json',
394
- accept: 'application/json',
395
- body: JSON.stringify(requestBody)
396
- });
397
-
398
- const response = await client.send(command);
399
- let fullResponse = '';
400
-
401
- for await (const event of response.body) {
402
- if (event.chunk) {
403
- const chunk = JSON.parse(new TextDecoder().decode(event.chunk.bytes));
404
-
405
- let text = '';
406
- if (chunk.delta?.text) {
407
- text = chunk.delta.text; // Anthropic format
408
- } else if (chunk.generation) {
409
- text = chunk.generation; // Meta Llama format
410
- }
411
-
412
- if (text) {
413
- fullResponse += text;
414
- if (onChunk) onChunk(text);
415
- }
416
- }
417
- }
418
-
419
- if (onComplete) onComplete(fullResponse);
420
- return { success: true, response: fullResponse, model };
421
-
422
- } catch (error) {
423
- const errorMsg = `AWS Bedrock error: ${error.message}`;
424
- if (onError) onError(errorMsg);
425
- return { success: false, error: errorMsg };
426
- }
427
- }
428
-
429
- /**
430
- * Call Claude Code CLI
431
- * @param {string} model - Model name (ignored, uses Claude Pro subscription)
432
- * @param {string} prompt - Prompt to send
433
- * @param {Object} options - Options (onChunk, onComplete, onError)
434
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
435
- */
436
- async callClaudeCode(model, prompt, options = {}) {
437
- const { onChunk, onComplete, onError } = options;
438
- const { spawn } = require('child_process');
439
-
440
- return new Promise((resolve) => {
441
- let fullResponse = '';
442
- let errorOutput = '';
443
-
444
- // Call claude CLI with the prompt
445
- const claude = spawn('claude', ['--dangerously-skip-permissions'], {
446
- stdio: ['pipe', 'pipe', 'pipe']
447
- });
448
-
449
- // Send prompt to stdin
450
- claude.stdin.write(prompt);
451
- claude.stdin.end();
452
-
453
- // Capture stdout
454
- claude.stdout.on('data', (data) => {
455
- const chunk = data.toString();
456
- fullResponse += chunk;
457
- if (onChunk) onChunk(chunk);
458
- });
459
-
460
- // Capture stderr
461
- claude.stderr.on('data', (data) => {
462
- errorOutput += data.toString();
463
- });
464
-
465
- // Handle completion
466
- claude.on('close', (code) => {
467
- if (code === 0) {
468
- if (onComplete) onComplete(fullResponse);
469
- resolve({ success: true, response: fullResponse });
470
- } else {
471
- const error = `Claude CLI exited with code ${code}: ${errorOutput}`;
472
- if (onError) onError(error);
473
- // Check for rate limits
474
- this.detectAndSaveRateLimit('claude-code', 'claude-code-cli', errorOutput);
475
- resolve({ success: false, error });
476
- }
477
- });
478
-
479
- // Handle spawn errors
480
- claude.on('error', (err) => {
481
- const error = `Failed to start Claude CLI: ${err.message}`;
482
- if (onError) onError(error);
483
- resolve({ success: false, error });
484
- });
485
- });
486
- }
487
-
488
- /**
489
- * Call any LLM provider
490
- * @param {Object} config - Provider configuration
491
- * @param {string} prompt - Prompt to send
492
- * @param {Object} options - Options
493
- * @returns {Promise<{success: boolean, response?: string, error?: string}>}
494
- */
495
- async call(config, prompt, options = {}) {
496
- const { provider, model, apiKey, region, accessKeyId, secretAccessKey } = config;
497
-
498
- switch (provider) {
499
- case 'ollama':
500
- return this.callOllama(model, prompt, options);
501
-
502
- case 'anthropic':
503
- return this.callAnthropic(model, prompt, { ...options, apiKey });
504
-
505
- case 'groq':
506
- return this.callGroq(model, prompt, { ...options, apiKey });
507
-
508
- case 'bedrock':
509
- return this.callBedrock(model, prompt, { ...options, region, accessKeyId, secretAccessKey });
510
-
511
- case 'claude-code':
512
- return this.callClaudeCode(model, prompt, options);
513
-
514
- default:
515
- return { success: false, error: `Unknown provider: ${provider}` };
516
- }
517
- }
518
-
519
- /**
520
- * Check if Ollama is available
521
- * @returns {Promise<boolean>}
522
- */
523
- async isOllamaAvailable() {
524
- return new Promise((resolve) => {
525
- const req = http.request({
526
- hostname: 'localhost',
527
- port: 11434,
528
- path: '/api/tags',
529
- method: 'GET',
530
- timeout: 2000
531
- }, (res) => {
532
- resolve(res.statusCode === 200);
533
- });
534
-
535
- req.on('error', () => resolve(false));
536
- req.on('timeout', () => {
537
- req.destroy();
538
- resolve(false);
539
- });
540
-
541
- req.end();
542
- });
543
- }
544
-
545
- /**
546
- * Check if Claude Code CLI is available
547
- * @returns {Promise<boolean>}
548
- */
549
- async isClaudeCodeAvailable() {
550
- const { spawn } = require('child_process');
551
-
552
- return new Promise((resolve) => {
553
- const claude = spawn('claude', ['--version'], {
554
- stdio: ['ignore', 'pipe', 'pipe']
555
- });
556
-
557
- claude.on('close', (code) => {
558
- resolve(code === 0);
559
- });
560
-
561
- claude.on('error', () => {
562
- resolve(false);
563
- });
564
-
565
- // Timeout after 2 seconds
566
- setTimeout(() => {
567
- claude.kill();
568
- resolve(false);
569
- }, 2000);
570
- });
571
- }
572
-
573
- /**
574
- * Get list of installed Ollama models
575
- * @returns {Promise<string[]>}
576
- */
577
- async getOllamaModels() {
578
- return new Promise((resolve) => {
579
- const req = http.request({
580
- hostname: 'localhost',
581
- port: 11434,
582
- path: '/api/tags',
583
- method: 'GET'
584
- }, (res) => {
585
- let data = '';
586
-
587
- res.on('data', (chunk) => {
588
- data += chunk.toString();
589
- });
590
-
591
- res.on('end', () => {
592
- try {
593
- const json = JSON.parse(data);
594
- const models = json.models?.map(m => m.name) || [];
595
- resolve(models);
596
- } catch (err) {
597
- resolve([]);
598
- }
599
- });
600
- });
601
-
602
- req.on('error', () => resolve([]));
603
- req.end();
604
- });
605
- }
606
- }
607
-
608
- module.exports = DirectLLMManager;
609
-
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
+
9
+ class DirectLLMManager {
10
+ constructor(sharedProviderManager = null) {
11
+ this.logger = console;
12
+ // Use shared ProviderManager if provided, otherwise create new instance
13
+ // IMPORTANT: Pass shared instance to maintain rate limit state across calls
14
+ if (sharedProviderManager) {
15
+ this.providerManager = sharedProviderManager;
16
+ } else {
17
+ try {
18
+ const ProviderManager = require('../ide-integration/provider-manager.cjs');
19
+ this.providerManager = new ProviderManager();
20
+ } catch (err) {
21
+ this.providerManager = null;
22
+ }
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Detect and save rate limit from error message
28
+ * @param {string} provider - Provider name
29
+ * @param {string} model - Model name
30
+ * @param {string} errorMessage - Error message from API
31
+ */
32
+ detectAndSaveRateLimit(provider, model, errorMessage) {
33
+ if (!this.providerManager) return;
34
+
35
+ // Check for rate limit indicators
36
+ const isRateLimit = errorMessage.includes('rate limit') ||
37
+ errorMessage.includes('Rate limit') ||
38
+ errorMessage.includes('too many requests') ||
39
+ errorMessage.includes('429') ||
40
+ errorMessage.includes('quota') ||
41
+ errorMessage.includes('Weekly limit reached') ||
42
+ errorMessage.includes('Daily limit reached') ||
43
+ errorMessage.includes('limit reached');
44
+
45
+ if (isRateLimit) {
46
+ this.providerManager.markRateLimited(provider, model, errorMessage);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Call Ollama API directly (local)
52
+ * @param {string} model - Model name (e.g., "qwen2.5-coder:32b")
53
+ * @param {string} prompt - Prompt to send
54
+ * @param {Object} options - Options (onChunk, onComplete, onError)
55
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
56
+ */
57
+ async callOllama(model, prompt, options = {}) {
58
+ const { onChunk, onComplete, onError, temperature = 0.2 } = options;
59
+
60
+ return new Promise((resolve) => {
61
+ let fullResponse = '';
62
+
63
+ const postData = JSON.stringify({
64
+ model: model,
65
+ prompt: prompt,
66
+ stream: true,
67
+ options: {
68
+ temperature: temperature
69
+ }
70
+ });
71
+
72
+ const req = http.request({
73
+ hostname: 'localhost',
74
+ port: 11434,
75
+ path: '/api/generate',
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ 'Content-Length': Buffer.byteLength(postData)
80
+ }
81
+ }, (res) => {
82
+ let buffer = '';
83
+
84
+ res.on('data', (chunk) => {
85
+ buffer += chunk.toString();
86
+ const lines = buffer.split('\n');
87
+ buffer = lines.pop(); // Keep incomplete line in buffer
88
+
89
+ for (const line of lines) {
90
+ if (!line.trim()) continue;
91
+
92
+ try {
93
+ const data = JSON.parse(line);
94
+ if (data.response) {
95
+ fullResponse += data.response;
96
+ if (onChunk) onChunk(data.response);
97
+ }
98
+
99
+ if (data.done) {
100
+ if (onComplete) onComplete(fullResponse);
101
+ resolve({
102
+ success: true,
103
+ response: fullResponse,
104
+ model: data.model,
105
+ context: data.context
106
+ });
107
+ }
108
+ } catch (err) {
109
+ // Ignore JSON parse errors for partial chunks
110
+ }
111
+ }
112
+ });
113
+
114
+ res.on('end', () => {
115
+ if (!fullResponse) {
116
+ const error = 'No response received from Ollama';
117
+ if (onError) onError(error);
118
+ resolve({ success: false, error });
119
+ }
120
+ });
121
+ });
122
+
123
+ req.on('error', (error) => {
124
+ const errorMsg = `Ollama API error: ${error.message}`;
125
+ if (onError) onError(errorMsg);
126
+ resolve({ success: false, error: errorMsg });
127
+ });
128
+
129
+ req.write(postData);
130
+ req.end();
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Call Anthropic API directly
136
+ * @param {string} model - Model name (e.g., "claude-sonnet-4-20250514")
137
+ * @param {string} prompt - Prompt to send
138
+ * @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
139
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
140
+ */
141
+ async callAnthropic(model, prompt, options = {}) {
142
+ const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
143
+
144
+ if (!apiKey) {
145
+ const error = 'Anthropic API key required';
146
+ if (onError) onError(error);
147
+ return { success: false, error };
148
+ }
149
+
150
+ return new Promise((resolve) => {
151
+ let fullResponse = '';
152
+
153
+ const postData = JSON.stringify({
154
+ model: model,
155
+ max_tokens: maxTokens,
156
+ temperature: temperature,
157
+ messages: [
158
+ { role: 'user', content: prompt }
159
+ ],
160
+ stream: true
161
+ });
162
+
163
+ const req = https.request({
164
+ hostname: 'api.anthropic.com',
165
+ path: '/v1/messages',
166
+ method: 'POST',
167
+ headers: {
168
+ 'Content-Type': 'application/json',
169
+ 'x-api-key': apiKey,
170
+ 'anthropic-version': '2023-06-01',
171
+ 'Content-Length': Buffer.byteLength(postData)
172
+ }
173
+ }, (res) => {
174
+ let buffer = '';
175
+
176
+ res.on('data', (chunk) => {
177
+ buffer += chunk.toString();
178
+ const lines = buffer.split('\n');
179
+ buffer = lines.pop();
180
+
181
+ for (const line of lines) {
182
+ if (!line.trim() || !line.startsWith('data: ')) continue;
183
+
184
+ try {
185
+ const jsonStr = line.slice(6); // Remove "data: " prefix
186
+ if (jsonStr === '[DONE]') continue;
187
+
188
+ const data = JSON.parse(jsonStr);
189
+
190
+ if (data.type === 'content_block_delta' && data.delta?.text) {
191
+ fullResponse += data.delta.text;
192
+ if (onChunk) onChunk(data.delta.text);
193
+ } else if (data.type === 'message_stop') {
194
+ if (onComplete) onComplete(fullResponse);
195
+ resolve({
196
+ success: true,
197
+ response: fullResponse,
198
+ model: model
199
+ });
200
+ }
201
+ } catch (err) {
202
+ // Ignore JSON parse errors
203
+ }
204
+ }
205
+ });
206
+
207
+ res.on('end', () => {
208
+ if (!fullResponse) {
209
+ const error = 'No response received from Anthropic';
210
+ if (onError) onError(error);
211
+ resolve({ success: false, error });
212
+ }
213
+ });
214
+ });
215
+
216
+ req.on('error', (error) => {
217
+ const errorMsg = `Anthropic API error: ${error.message}`;
218
+ if (onError) onError(errorMsg);
219
+ resolve({ success: false, error: errorMsg });
220
+ });
221
+
222
+ req.write(postData);
223
+ req.end();
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Call Groq API directly
229
+ * @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
230
+ * @param {string} prompt - Prompt to send
231
+ * @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
232
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
233
+ */
234
+ async callGroq(model, prompt, options = {}) {
235
+ const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
236
+
237
+ if (!apiKey) {
238
+ const error = 'Groq API key required';
239
+ if (onError) onError(error);
240
+ return { success: false, error };
241
+ }
242
+
243
+ return new Promise((resolve) => {
244
+ let fullResponse = '';
245
+
246
+ const postData = JSON.stringify({
247
+ model: model,
248
+ messages: [
249
+ { role: 'user', content: prompt }
250
+ ],
251
+ temperature: temperature,
252
+ max_tokens: maxTokens,
253
+ stream: true
254
+ });
255
+
256
+ const req = https.request({
257
+ hostname: 'api.groq.com',
258
+ path: '/openai/v1/chat/completions',
259
+ method: 'POST',
260
+ headers: {
261
+ 'Content-Type': 'application/json',
262
+ 'Authorization': `Bearer ${apiKey}`,
263
+ 'Content-Length': Buffer.byteLength(postData)
264
+ }
265
+ }, (res) => {
266
+ let buffer = '';
267
+ let statusCode = res.statusCode;
268
+
269
+ // Check for rate limit or error status codes
270
+ if (statusCode === 429 || statusCode >= 400) {
271
+ let errorBody = '';
272
+ res.on('data', (chunk) => {
273
+ errorBody += chunk.toString();
274
+ });
275
+ res.on('end', () => {
276
+ const errorMsg = `Groq API error (${statusCode}): ${errorBody || 'No error details'}`;
277
+ this.detectAndSaveRateLimit('groq', model, errorMsg);
278
+ if (onError) onError(errorMsg);
279
+ resolve({ success: false, error: errorMsg });
280
+ });
281
+ return;
282
+ }
283
+
284
+ res.on('data', (chunk) => {
285
+ buffer += chunk.toString();
286
+ const lines = buffer.split('\n');
287
+ buffer = lines.pop();
288
+
289
+ for (const line of lines) {
290
+ if (!line.trim() || !line.startsWith('data: ')) continue;
291
+
292
+ try {
293
+ const jsonStr = line.slice(6);
294
+ if (jsonStr === '[DONE]') {
295
+ if (onComplete) onComplete(fullResponse);
296
+ resolve({
297
+ success: true,
298
+ response: fullResponse,
299
+ model: model
300
+ });
301
+ return;
302
+ }
303
+
304
+ const data = JSON.parse(jsonStr);
305
+ const content = data.choices?.[0]?.delta?.content;
306
+
307
+ if (content) {
308
+ fullResponse += content;
309
+ if (onChunk) onChunk(content);
310
+ }
311
+ } catch (err) {
312
+ // Ignore JSON parse errors
313
+ }
314
+ }
315
+ });
316
+
317
+ res.on('end', () => {
318
+ if (fullResponse) {
319
+ if (onComplete) onComplete(fullResponse);
320
+ resolve({ success: true, response: fullResponse, model });
321
+ } else {
322
+ const error = buffer || 'No response received from Groq';
323
+ this.detectAndSaveRateLimit('groq', model, error);
324
+ if (onError) onError(error);
325
+ resolve({ success: false, error });
326
+ }
327
+ });
328
+ });
329
+
330
+ req.on('error', (error) => {
331
+ const errorMsg = `Groq API error: ${error.message}`;
332
+ this.detectAndSaveRateLimit('groq', model, errorMsg);
333
+ if (onError) onError(errorMsg);
334
+ resolve({ success: false, error: errorMsg });
335
+ });
336
+
337
+ req.write(postData);
338
+ req.end();
339
+ });
340
+ }
341
+
342
+ /**
343
+ * Call AWS Bedrock API directly
344
+ * @param {string} model - Model ID (e.g., "anthropic.claude-sonnet-4-v1")
345
+ * @param {string} prompt - Prompt to send
346
+ * @param {Object} options - Options (region, accessKeyId, secretAccessKey, onChunk, onComplete, onError)
347
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
348
+ */
349
+ async callBedrock(model, prompt, options = {}) {
350
+ const { region, accessKeyId, secretAccessKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
351
+
352
+ if (!region || !accessKeyId || !secretAccessKey) {
353
+ const error = 'AWS credentials required (region, accessKeyId, secretAccessKey)';
354
+ if (onError) onError(error);
355
+ return { success: false, error };
356
+ }
357
+
358
+ try {
359
+ // Use AWS SDK v3 for Bedrock
360
+ const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
361
+
362
+ const client = new BedrockRuntimeClient({
363
+ region: region,
364
+ credentials: {
365
+ accessKeyId: accessKeyId,
366
+ secretAccessKey: secretAccessKey
367
+ }
368
+ });
369
+
370
+ // Format request based on model provider
371
+ let requestBody;
372
+ if (model.startsWith('anthropic.')) {
373
+ requestBody = {
374
+ anthropic_version: 'bedrock-2023-05-31',
375
+ max_tokens: maxTokens,
376
+ temperature: temperature,
377
+ messages: [
378
+ { role: 'user', content: prompt }
379
+ ]
380
+ };
381
+ } else if (model.startsWith('meta.')) {
382
+ requestBody = {
383
+ prompt: prompt,
384
+ temperature: temperature,
385
+ max_gen_len: maxTokens
386
+ };
387
+ } else {
388
+ return { success: false, error: `Unsupported Bedrock model: ${model}` };
389
+ }
390
+
391
+ const command = new InvokeModelWithResponseStreamCommand({
392
+ modelId: model,
393
+ contentType: 'application/json',
394
+ accept: 'application/json',
395
+ body: JSON.stringify(requestBody)
396
+ });
397
+
398
+ const response = await client.send(command);
399
+ let fullResponse = '';
400
+
401
+ for await (const event of response.body) {
402
+ if (event.chunk) {
403
+ const chunk = JSON.parse(new TextDecoder().decode(event.chunk.bytes));
404
+
405
+ let text = '';
406
+ if (chunk.delta?.text) {
407
+ text = chunk.delta.text; // Anthropic format
408
+ } else if (chunk.generation) {
409
+ text = chunk.generation; // Meta Llama format
410
+ }
411
+
412
+ if (text) {
413
+ fullResponse += text;
414
+ if (onChunk) onChunk(text);
415
+ }
416
+ }
417
+ }
418
+
419
+ if (onComplete) onComplete(fullResponse);
420
+ return { success: true, response: fullResponse, model };
421
+
422
+ } catch (error) {
423
+ const errorMsg = `AWS Bedrock error: ${error.message}`;
424
+ if (onError) onError(errorMsg);
425
+ return { success: false, error: errorMsg };
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Call Claude Code CLI
431
+ * @param {string} model - Model name (ignored, uses Claude Pro subscription)
432
+ * @param {string} prompt - Prompt to send
433
+ * @param {Object} options - Options (onChunk, onComplete, onError)
434
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
435
+ */
436
+ async callClaudeCode(model, prompt, options = {}) {
437
+ const { onChunk, onComplete, onError } = options;
438
+ const { spawn } = require('child_process');
439
+
440
+ return new Promise((resolve) => {
441
+ let fullResponse = '';
442
+ let errorOutput = '';
443
+
444
+ // Call claude CLI with the prompt
445
+ const claude = spawn('claude', ['--dangerously-skip-permissions'], {
446
+ stdio: ['pipe', 'pipe', 'pipe']
447
+ });
448
+
449
+ // Send prompt to stdin
450
+ claude.stdin.write(prompt);
451
+ claude.stdin.end();
452
+
453
+ // Capture stdout
454
+ claude.stdout.on('data', (data) => {
455
+ const chunk = data.toString();
456
+ fullResponse += chunk;
457
+ if (onChunk) onChunk(chunk);
458
+ });
459
+
460
+ // Capture stderr
461
+ claude.stderr.on('data', (data) => {
462
+ errorOutput += data.toString();
463
+ });
464
+
465
+ // Handle completion
466
+ claude.on('close', (code) => {
467
+ if (code === 0) {
468
+ if (onComplete) onComplete(fullResponse);
469
+ resolve({ success: true, response: fullResponse });
470
+ } else {
471
+ const error = `Claude CLI exited with code ${code}: ${errorOutput}`;
472
+ if (onError) onError(error);
473
+ // Check for rate limits
474
+ this.detectAndSaveRateLimit('claude-code', 'claude-code-cli', errorOutput);
475
+ resolve({ success: false, error });
476
+ }
477
+ });
478
+
479
+ // Handle spawn errors
480
+ claude.on('error', (err) => {
481
+ const error = `Failed to start Claude CLI: ${err.message}`;
482
+ if (onError) onError(error);
483
+ resolve({ success: false, error });
484
+ });
485
+ });
486
+ }
487
+
488
+ /**
489
+ * Call any LLM provider
490
+ * @param {Object} config - Provider configuration
491
+ * @param {string} prompt - Prompt to send
492
+ * @param {Object} options - Options
493
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
494
+ */
495
+ async call(config, prompt, options = {}) {
496
+ const { provider, model, apiKey, region, accessKeyId, secretAccessKey } = config;
497
+
498
+ switch (provider) {
499
+ case 'ollama':
500
+ return this.callOllama(model, prompt, options);
501
+
502
+ case 'anthropic':
503
+ return this.callAnthropic(model, prompt, { ...options, apiKey });
504
+
505
+ case 'groq':
506
+ return this.callGroq(model, prompt, { ...options, apiKey });
507
+
508
+ case 'bedrock':
509
+ return this.callBedrock(model, prompt, { ...options, region, accessKeyId, secretAccessKey });
510
+
511
+ case 'claude-code':
512
+ return this.callClaudeCode(model, prompt, options);
513
+
514
+ default:
515
+ return { success: false, error: `Unknown provider: ${provider}` };
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Check if Ollama is available
521
+ * @returns {Promise<boolean>}
522
+ */
523
+ async isOllamaAvailable() {
524
+ return new Promise((resolve) => {
525
+ const req = http.request({
526
+ hostname: 'localhost',
527
+ port: 11434,
528
+ path: '/api/tags',
529
+ method: 'GET',
530
+ timeout: 2000
531
+ }, (res) => {
532
+ resolve(res.statusCode === 200);
533
+ });
534
+
535
+ req.on('error', () => resolve(false));
536
+ req.on('timeout', () => {
537
+ req.destroy();
538
+ resolve(false);
539
+ });
540
+
541
+ req.end();
542
+ });
543
+ }
544
+
545
+ /**
546
+ * Check if Claude Code CLI is available
547
+ * @returns {Promise<boolean>}
548
+ */
549
+ async isClaudeCodeAvailable() {
550
+ const { spawn } = require('child_process');
551
+
552
+ return new Promise((resolve) => {
553
+ const claude = spawn('claude', ['--version'], {
554
+ stdio: ['ignore', 'pipe', 'pipe']
555
+ });
556
+
557
+ claude.on('close', (code) => {
558
+ resolve(code === 0);
559
+ });
560
+
561
+ claude.on('error', () => {
562
+ resolve(false);
563
+ });
564
+
565
+ // Timeout after 2 seconds
566
+ setTimeout(() => {
567
+ claude.kill();
568
+ resolve(false);
569
+ }, 2000);
570
+ });
571
+ }
572
+
573
+ /**
574
+ * Get list of installed Ollama models
575
+ * @returns {Promise<string[]>}
576
+ */
577
+ async getOllamaModels() {
578
+ return new Promise((resolve) => {
579
+ const req = http.request({
580
+ hostname: 'localhost',
581
+ port: 11434,
582
+ path: '/api/tags',
583
+ method: 'GET'
584
+ }, (res) => {
585
+ let data = '';
586
+
587
+ res.on('data', (chunk) => {
588
+ data += chunk.toString();
589
+ });
590
+
591
+ res.on('end', () => {
592
+ try {
593
+ const json = JSON.parse(data);
594
+ const models = json.models?.map(m => m.name) || [];
595
+ resolve(models);
596
+ } catch (err) {
597
+ resolve([]);
598
+ }
599
+ });
600
+ });
601
+
602
+ req.on('error', () => resolve([]));
603
+ req.end();
604
+ });
605
+ }
606
+ }
607
+
608
+ module.exports = DirectLLMManager;
609
+