vibecodingmachine-core 2025.12.6-1702 → 2025.12.22-2230

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.
@@ -5,6 +5,7 @@
5
5
 
6
6
  const https = require('https');
7
7
  const http = require('http');
8
+ const quotaManagement = require('../quota-management');
8
9
 
9
10
  class DirectLLMManager {
10
11
  constructor(sharedProviderManager = null) {
@@ -22,7 +23,7 @@ class DirectLLMManager {
22
23
  }
23
24
  }
24
25
  }
25
-
26
+
26
27
  /**
27
28
  * Detect and save rate limit from error message
28
29
  * @param {string} provider - Provider name
@@ -31,17 +32,17 @@ class DirectLLMManager {
31
32
  */
32
33
  detectAndSaveRateLimit(provider, model, errorMessage) {
33
34
  if (!this.providerManager) return;
34
-
35
+
35
36
  // 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
-
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
+
45
46
  if (isRateLimit) {
46
47
  this.providerManager.markRateLimited(provider, model, errorMessage);
47
48
  }
@@ -56,10 +57,10 @@ class DirectLLMManager {
56
57
  */
57
58
  async callOllama(model, prompt, options = {}) {
58
59
  const { onChunk, onComplete, onError, temperature = 0.2 } = options;
59
-
60
+
60
61
  return new Promise((resolve) => {
61
62
  let fullResponse = '';
62
-
63
+
63
64
  const postData = JSON.stringify({
64
65
  model: model,
65
66
  prompt: prompt,
@@ -80,22 +81,22 @@ class DirectLLMManager {
80
81
  }
81
82
  }, (res) => {
82
83
  let buffer = '';
83
-
84
+
84
85
  res.on('data', (chunk) => {
85
86
  buffer += chunk.toString();
86
87
  const lines = buffer.split('\n');
87
88
  buffer = lines.pop(); // Keep incomplete line in buffer
88
-
89
+
89
90
  for (const line of lines) {
90
91
  if (!line.trim()) continue;
91
-
92
+
92
93
  try {
93
94
  const data = JSON.parse(line);
94
95
  if (data.response) {
95
96
  fullResponse += data.response;
96
97
  if (onChunk) onChunk(data.response);
97
98
  }
98
-
99
+
99
100
  if (data.done) {
100
101
  if (onComplete) onComplete(fullResponse);
101
102
  resolve({
@@ -110,7 +111,7 @@ class DirectLLMManager {
110
111
  }
111
112
  }
112
113
  });
113
-
114
+
114
115
  res.on('end', () => {
115
116
  if (!fullResponse) {
116
117
  const error = 'No response received from Ollama';
@@ -140,7 +141,7 @@ class DirectLLMManager {
140
141
  */
141
142
  async callAnthropic(model, prompt, options = {}) {
142
143
  const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
143
-
144
+
144
145
  if (!apiKey) {
145
146
  const error = 'Anthropic API key required';
146
147
  if (onError) onError(error);
@@ -149,7 +150,7 @@ class DirectLLMManager {
149
150
 
150
151
  return new Promise((resolve) => {
151
152
  let fullResponse = '';
152
-
153
+
153
154
  const postData = JSON.stringify({
154
155
  model: model,
155
156
  max_tokens: maxTokens,
@@ -172,21 +173,21 @@ class DirectLLMManager {
172
173
  }
173
174
  }, (res) => {
174
175
  let buffer = '';
175
-
176
+
176
177
  res.on('data', (chunk) => {
177
178
  buffer += chunk.toString();
178
179
  const lines = buffer.split('\n');
179
180
  buffer = lines.pop();
180
-
181
+
181
182
  for (const line of lines) {
182
183
  if (!line.trim() || !line.startsWith('data: ')) continue;
183
-
184
+
184
185
  try {
185
186
  const jsonStr = line.slice(6); // Remove "data: " prefix
186
187
  if (jsonStr === '[DONE]') continue;
187
-
188
+
188
189
  const data = JSON.parse(jsonStr);
189
-
190
+
190
191
  if (data.type === 'content_block_delta' && data.delta?.text) {
191
192
  fullResponse += data.delta.text;
192
193
  if (onChunk) onChunk(data.delta.text);
@@ -203,7 +204,7 @@ class DirectLLMManager {
203
204
  }
204
205
  }
205
206
  });
206
-
207
+
207
208
  res.on('end', () => {
208
209
  if (!fullResponse) {
209
210
  const error = 'No response received from Anthropic';
@@ -233,7 +234,7 @@ class DirectLLMManager {
233
234
  */
234
235
  async callGroq(model, prompt, options = {}) {
235
236
  const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
236
-
237
+
237
238
  if (!apiKey) {
238
239
  const error = 'Groq API key required';
239
240
  if (onError) onError(error);
@@ -242,7 +243,7 @@ class DirectLLMManager {
242
243
 
243
244
  return new Promise((resolve) => {
244
245
  let fullResponse = '';
245
-
246
+
246
247
  const postData = JSON.stringify({
247
248
  model: model,
248
249
  messages: [
@@ -265,7 +266,7 @@ class DirectLLMManager {
265
266
  }, (res) => {
266
267
  let buffer = '';
267
268
  let statusCode = res.statusCode;
268
-
269
+
269
270
  // Check for rate limit or error status codes
270
271
  if (statusCode === 429 || statusCode >= 400) {
271
272
  let errorBody = '';
@@ -280,15 +281,15 @@ class DirectLLMManager {
280
281
  });
281
282
  return;
282
283
  }
283
-
284
+
284
285
  res.on('data', (chunk) => {
285
286
  buffer += chunk.toString();
286
287
  const lines = buffer.split('\n');
287
288
  buffer = lines.pop();
288
-
289
+
289
290
  for (const line of lines) {
290
291
  if (!line.trim() || !line.startsWith('data: ')) continue;
291
-
292
+
292
293
  try {
293
294
  const jsonStr = line.slice(6);
294
295
  if (jsonStr === '[DONE]') {
@@ -300,10 +301,10 @@ class DirectLLMManager {
300
301
  });
301
302
  return;
302
303
  }
303
-
304
+
304
305
  const data = JSON.parse(jsonStr);
305
306
  const content = data.choices?.[0]?.delta?.content;
306
-
307
+
307
308
  if (content) {
308
309
  fullResponse += content;
309
310
  if (onChunk) onChunk(content);
@@ -313,7 +314,7 @@ class DirectLLMManager {
313
314
  }
314
315
  }
315
316
  });
316
-
317
+
317
318
  res.on('end', () => {
318
319
  if (fullResponse) {
319
320
  if (onComplete) onComplete(fullResponse);
@@ -348,7 +349,7 @@ class DirectLLMManager {
348
349
  */
349
350
  async callBedrock(model, prompt, options = {}) {
350
351
  const { region, accessKeyId, secretAccessKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
351
-
352
+
352
353
  if (!region || !accessKeyId || !secretAccessKey) {
353
354
  const error = 'AWS credentials required (region, accessKeyId, secretAccessKey)';
354
355
  if (onError) onError(error);
@@ -358,7 +359,7 @@ class DirectLLMManager {
358
359
  try {
359
360
  // Use AWS SDK v3 for Bedrock
360
361
  const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
361
-
362
+
362
363
  const client = new BedrockRuntimeClient({
363
364
  region: region,
364
365
  credentials: {
@@ -401,14 +402,14 @@ class DirectLLMManager {
401
402
  for await (const event of response.body) {
402
403
  if (event.chunk) {
403
404
  const chunk = JSON.parse(new TextDecoder().decode(event.chunk.bytes));
404
-
405
+
405
406
  let text = '';
406
407
  if (chunk.delta?.text) {
407
408
  text = chunk.delta.text; // Anthropic format
408
409
  } else if (chunk.generation) {
409
410
  text = chunk.generation; // Meta Llama format
410
411
  }
411
-
412
+
412
413
  if (text) {
413
414
  fullResponse += text;
414
415
  if (onChunk) onChunk(text);
@@ -418,7 +419,7 @@ class DirectLLMManager {
418
419
 
419
420
  if (onComplete) onComplete(fullResponse);
420
421
  return { success: true, response: fullResponse, model };
421
-
422
+
422
423
  } catch (error) {
423
424
  const errorMsg = `AWS Bedrock error: ${error.message}`;
424
425
  if (onError) onError(errorMsg);
@@ -436,32 +437,32 @@ class DirectLLMManager {
436
437
  async callClaudeCode(model, prompt, options = {}) {
437
438
  const { onChunk, onComplete, onError } = options;
438
439
  const { spawn } = require('child_process');
439
-
440
+
440
441
  return new Promise((resolve) => {
441
442
  let fullResponse = '';
442
443
  let errorOutput = '';
443
-
444
+
444
445
  // Call claude CLI with the prompt
445
446
  const claude = spawn('claude', ['--dangerously-skip-permissions'], {
446
447
  stdio: ['pipe', 'pipe', 'pipe']
447
448
  });
448
-
449
+
449
450
  // Send prompt to stdin
450
451
  claude.stdin.write(prompt);
451
452
  claude.stdin.end();
452
-
453
+
453
454
  // Capture stdout
454
455
  claude.stdout.on('data', (data) => {
455
456
  const chunk = data.toString();
456
457
  fullResponse += chunk;
457
458
  if (onChunk) onChunk(chunk);
458
459
  });
459
-
460
+
460
461
  // Capture stderr
461
462
  claude.stderr.on('data', (data) => {
462
463
  errorOutput += data.toString();
463
464
  });
464
-
465
+
465
466
  // Handle completion
466
467
  claude.on('close', (code) => {
467
468
  if (code === 0) {
@@ -475,7 +476,7 @@ class DirectLLMManager {
475
476
  resolve({ success: false, error });
476
477
  }
477
478
  });
478
-
479
+
479
480
  // Handle spawn errors
480
481
  claude.on('error', (err) => {
481
482
  const error = `Failed to start Claude CLI: ${err.message}`;
@@ -493,27 +494,63 @@ class DirectLLMManager {
493
494
  * @returns {Promise<{success: boolean, response?: string, error?: string}>}
494
495
  */
495
496
  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}` };
497
+ const { provider, model, apiKey, region, accessKeyId, secretAccessKey, fallbackModels = [] } = config;
498
+ const modelsToTry = [model, ...fallbackModels];
499
+ let lastError = null;
500
+
501
+ for (const currentModel of modelsToTry) {
502
+ if (currentModel !== model) {
503
+ this.logger.log(`⚠️ Quota/Limit reached for previous model, failing over to ${currentModel}...`);
504
+ }
505
+
506
+ const agentId = `${provider}:${currentModel}`;
507
+ try {
508
+ const quota = await quotaManagement.fetchQuotaForAgent(agentId);
509
+ if (quota.isExceeded()) {
510
+ const errorMessage = `Quota limit reached for ${currentModel}. Resets at ${quota.resetsAt ? quota.resetsAt.toLocaleString() : 'a later time'}.`;
511
+ lastError = { success: false, error: errorMessage };
512
+ continue; // Try next model
513
+ }
514
+ } catch (error) {
515
+ this.logger.error(`Failed to check quota for ${agentId}: ${error.message}`);
516
+ }
517
+
518
+ const currentConfig = { ...config, model: currentModel };
519
+ let result;
520
+
521
+ switch (provider) {
522
+ case 'ollama':
523
+ result = await this.callOllama(currentModel, prompt, options);
524
+ break;
525
+ case 'anthropic':
526
+ result = await this.callAnthropic(currentModel, prompt, { ...options, apiKey });
527
+ break;
528
+ case 'groq':
529
+ result = await this.callGroq(currentModel, prompt, { ...options, apiKey });
530
+ break;
531
+ case 'bedrock':
532
+ result = await this.callBedrock(currentModel, prompt, { ...options, region, accessKeyId, secretAccessKey });
533
+ break;
534
+ case 'claude-code':
535
+ result = await this.callClaudeCode(currentModel, prompt, options);
536
+ break;
537
+ default:
538
+ return { success: false, error: `Unknown provider: ${provider}` };
539
+ }
540
+
541
+ if (result.success) {
542
+ return result;
543
+ }
544
+
545
+ // If failed, check for rate limit to save it
546
+ this.detectAndSaveRateLimit(provider, currentModel, result.error || '');
547
+ lastError = result;
548
+
549
+ // If it's a "fatal" error that isn't a rate limit, we might want to stop?
550
+ // But usually we want to try the next model if possible.
516
551
  }
552
+
553
+ return lastError || { success: false, error: `All models for ${provider} failed.` };
517
554
  }
518
555
 
519
556
  /**
@@ -548,20 +585,20 @@ class DirectLLMManager {
548
585
  */
549
586
  async isClaudeCodeAvailable() {
550
587
  const { spawn } = require('child_process');
551
-
588
+
552
589
  return new Promise((resolve) => {
553
590
  const claude = spawn('claude', ['--version'], {
554
591
  stdio: ['ignore', 'pipe', 'pipe']
555
592
  });
556
-
593
+
557
594
  claude.on('close', (code) => {
558
595
  resolve(code === 0);
559
596
  });
560
-
597
+
561
598
  claude.on('error', () => {
562
599
  resolve(false);
563
600
  });
564
-
601
+
565
602
  // Timeout after 2 seconds
566
603
  setTimeout(() => {
567
604
  claude.kill();
@@ -583,11 +620,11 @@ class DirectLLMManager {
583
620
  method: 'GET'
584
621
  }, (res) => {
585
622
  let data = '';
586
-
623
+
587
624
  res.on('data', (chunk) => {
588
625
  data += chunk.toString();
589
626
  });
590
-
627
+
591
628
  res.on('end', () => {
592
629
  try {
593
630
  const json = JSON.parse(data);
@@ -0,0 +1,108 @@
1
+ const sharedAuth = require('../auth/shared-auth-storage');
2
+ const ProviderManager = require('../ide-integration/provider-manager.cjs');
3
+
4
+ // In-memory cache for quota data.
5
+ const quotaCache = new Map();
6
+
7
+ /**
8
+ * Represents the quota details for a specific agent.
9
+ */
10
+ class Quota {
11
+ /**
12
+ * @param {string} agentId - The unique identifier for the agent (e.g., 'openai:gpt-4').
13
+ * @param {number} limit - The total quota limit.
14
+ * @param {number} remaining - The remaining quota.
15
+ * @param {Date} resetsAt - The timestamp when the quota resets.
16
+ * @param {string} type - The type of quota ('global', 'rate-limit', 'infinite').
17
+ */
18
+ constructor(agentId, limit, remaining, resetsAt, type = 'rate-limit') {
19
+ this.agentId = agentId;
20
+ this.limit = limit;
21
+ this.remaining = remaining;
22
+ this.resetsAt = resetsAt;
23
+ this.lastUpdated = new Date();
24
+ this.type = type;
25
+ }
26
+
27
+ /**
28
+ * Checks if the quota has been exceeded.
29
+ * @returns {boolean} True if the remaining quota is zero or less.
30
+ */
31
+ isExceeded() {
32
+ return this.remaining <= 0;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Fetches quota information for a given agent.
38
+ *
39
+ * @param {string} agentId - The ID of the agent to fetch quota for (e.g., 'anthropic:claude-3').
40
+ * @returns {Promise<Quota>} A promise that resolves to a Quota object.
41
+ */
42
+ async function fetchQuotaForAgent(agentId) {
43
+ // 1. Handle Global Daily Iteration Quota
44
+ if (agentId === 'global:iterations') {
45
+ const quotaInfo = await sharedAuth.canRunAutoMode();
46
+ const today = new Date();
47
+ const tonight = new Date(today);
48
+ tonight.setHours(24, 0, 0, 0);
49
+
50
+ const quota = new Quota(
51
+ 'global:iterations',
52
+ quotaInfo.maxIterations || 10,
53
+ quotaInfo.todayUsage !== undefined ? Math.max(0, (quotaInfo.maxIterations || 10) - quotaInfo.todayUsage) : 10,
54
+ tonight,
55
+ 'global'
56
+ );
57
+ quotaCache.set(agentId, quota);
58
+ return quota;
59
+ }
60
+
61
+ // 2. Handle Local Agent (Infinite Quota)
62
+ if (agentId.startsWith('local-') || agentId.includes('ollama')) {
63
+ const quota = new Quota(agentId, Infinity, Infinity, null, 'infinite');
64
+ quotaCache.set(agentId, quota);
65
+ return quota;
66
+ }
67
+
68
+ // 3. Handle Provider Rate Limits via ProviderManager
69
+ try {
70
+ const providerManager = new ProviderManager();
71
+ const parts = agentId.split(':');
72
+ const provider = parts[0];
73
+ const model = parts[1] || provider;
74
+
75
+ const isLimited = providerManager.isRateLimited(provider, model);
76
+ const timeUntilReset = providerManager.getTimeUntilReset(provider, model);
77
+
78
+ const quota = new Quota(
79
+ agentId,
80
+ 1, // Binary limit for rate limits (1 if available, 0 if limited)
81
+ isLimited ? 0 : 1,
82
+ timeUntilReset ? new Date(Date.now() + timeUntilReset) : null,
83
+ 'rate-limit'
84
+ );
85
+
86
+ quotaCache.set(agentId, quota);
87
+ return quota;
88
+ } catch (error) {
89
+ console.error(`Error fetching provider quota for ${agentId}:`, error);
90
+ return new Quota(agentId, 1, 1, null, 'rate-limit');
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Retrieves the cached quota for a given agent.
96
+ *
97
+ * @param {string} agentId - The ID of the agent.
98
+ * @returns {Quota | undefined} The cached Quota object or undefined if not found.
99
+ */
100
+ function getCachedQuota(agentId) {
101
+ return quotaCache.get(agentId);
102
+ }
103
+
104
+ module.exports = {
105
+ Quota,
106
+ fetchQuotaForAgent,
107
+ getCachedQuota,
108
+ };
@@ -54,17 +54,27 @@ class SyncEngine extends EventEmitter {
54
54
  // Initialize DynamoDB client
55
55
  const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
56
56
  const { DynamoDBDocumentClient } = require('@aws-sdk/lib-dynamodb');
57
-
57
+
58
58
  const region = process.env.AWS_REGION || 'us-east-1';
59
59
  const client = new DynamoDBClient({ region });
60
60
  this.dynamoClient = DynamoDBDocumentClient.from(client);
61
-
61
+
62
62
  // Initialize WebSocket client for real-time updates
63
63
  await this._initializeWebSocket();
64
-
64
+
65
65
  this.emit('initialized');
66
66
  return true;
67
67
  } catch (error) {
68
+ // If AWS SDK is not installed in this environment (common in local dev),
69
+ // fall back to offline mode instead of throwing so the CLI remains usable.
70
+ if (error && error.code === 'MODULE_NOT_FOUND' && /@aws-sdk\//.test(error.message)) {
71
+ this.options.offlineMode = true;
72
+ this.isOnline = false;
73
+ this.emit('warning', { type: 'offline-fallback', message: 'AWS SDK not available, running in offline mode', error });
74
+ this.emit('initialized');
75
+ return false;
76
+ }
77
+
68
78
  this.emit('error', { type: 'initialization', error });
69
79
  throw error;
70
80
  }
@@ -177,11 +187,16 @@ class SyncEngine extends EventEmitter {
177
187
  * Fetch remote changes from DynamoDB
178
188
  */
179
189
  async _fetchRemoteChanges() {
190
+ if (!this.dynamoClient) {
191
+ this.emit('warning', { type: 'no-dynamo', message: 'DynamoDB client not initialized, skipping remote fetch' });
192
+ return [];
193
+ }
194
+
180
195
  const { ScanCommand } = require('@aws-sdk/lib-dynamodb');
181
-
196
+
182
197
  const tableName = process.env.DYNAMODB_TABLE_NAME || 'vibecodingmachine-requirements';
183
198
  const lastSync = this.lastSyncTime || 0;
184
-
199
+
185
200
  try {
186
201
  // Use Scan with filter instead of Query since we need to check all items
187
202
  // In production, consider using DynamoDB Streams for real-time updates
@@ -195,7 +210,7 @@ class SyncEngine extends EventEmitter {
195
210
  ':lastSync': lastSync
196
211
  }
197
212
  });
198
-
213
+
199
214
  const response = await this.dynamoClient.send(command);
200
215
  return response.Items || [];
201
216
  } catch (error) {
@@ -303,10 +318,17 @@ class SyncEngine extends EventEmitter {
303
318
  * Push local changes to remote
304
319
  */
305
320
  async _pushLocalChanges(changes) {
321
+ if (!this.dynamoClient) {
322
+ this.emit('warning', { type: 'no-dynamo', message: 'DynamoDB client not initialized, queuing local changes' });
323
+ // Queue all changes for later push
324
+ this.offlineQueue.push(...changes);
325
+ return;
326
+ }
327
+
306
328
  const { PutCommand } = require('@aws-sdk/lib-dynamodb');
307
-
329
+
308
330
  const tableName = process.env.DYNAMODB_TABLE_NAME || 'vibecodingmachine-requirements';
309
-
331
+
310
332
  for (const change of changes) {
311
333
  try {
312
334
  const command = new PutCommand({
@@ -317,12 +339,12 @@ class SyncEngine extends EventEmitter {
317
339
  ...change
318
340
  }
319
341
  });
320
-
342
+
321
343
  await this.dynamoClient.send(command);
322
344
  this.emit('local-change-pushed', change);
323
345
  } catch (error) {
324
346
  this.emit('error', { type: 'push-local', change, error });
325
-
347
+
326
348
  // Add to offline queue if push fails
327
349
  if (!this.isOnline) {
328
350
  this.offlineQueue.push(change);