vibecodingmachine-cli 2026.1.3-2209 → 2026.1.23-1010

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 (40) hide show
  1. package/__tests__/antigravity-js-handler.test.js +23 -0
  2. package/__tests__/provider-manager.test.js +84 -0
  3. package/__tests__/provider-rate-cache.test.js +27 -0
  4. package/bin/vibecodingmachine.js +8 -0
  5. package/package.json +2 -2
  6. package/reset_provider_order.js +21 -0
  7. package/scripts/convert-requirements.js +35 -0
  8. package/scripts/debug-parse.js +24 -0
  9. package/src/commands/auto-direct.js +679 -120
  10. package/src/commands/auto.js +200 -45
  11. package/src/commands/ide.js +108 -3
  12. package/src/commands/requirements-remote.js +10 -1
  13. package/src/commands/status.js +39 -1
  14. package/src/utils/antigravity-js-handler.js +13 -4
  15. package/src/utils/auth.js +37 -13
  16. package/src/utils/compliance-check.js +10 -0
  17. package/src/utils/config.js +29 -1
  18. package/src/utils/date-formatter.js +44 -0
  19. package/src/utils/interactive.js +1006 -537
  20. package/src/utils/kiro-js-handler.js +188 -0
  21. package/src/utils/provider-rate-cache.js +31 -0
  22. package/src/utils/provider-registry.js +42 -1
  23. package/src/utils/requirements-converter.js +107 -0
  24. package/src/utils/requirements-parser.js +144 -0
  25. package/tests/antigravity-js-handler.test.js +23 -0
  26. package/tests/integration/health-tracking.integration.test.js +284 -0
  27. package/tests/provider-manager.test.js +92 -0
  28. package/tests/rate-limit-display.test.js +44 -0
  29. package/tests/requirements-bullet-parsing.test.js +15 -0
  30. package/tests/requirements-converter.test.js +42 -0
  31. package/tests/requirements-heading-count.test.js +27 -0
  32. package/tests/requirements-legacy-parsing.test.js +15 -0
  33. package/tests/requirements-parse-integration.test.js +44 -0
  34. package/tests/wait-for-ide-completion.test.js +56 -0
  35. package/tests/wait-for-ide-quota-detection-cursor-screenshot.test.js +61 -0
  36. package/tests/wait-for-ide-quota-detection-cursor.test.js +60 -0
  37. package/tests/wait-for-ide-quota-detection-negative.test.js +45 -0
  38. package/tests/wait-for-ide-quota-detection.test.js +59 -0
  39. package/verify_fix.js +36 -0
  40. package/verify_ui.js +38 -0
@@ -0,0 +1,188 @@
1
+ const chalk = require('chalk');
2
+ const {
3
+ getProviderPreferences,
4
+ saveProviderPreferences
5
+ } = require('./provider-registry');
6
+ const { AppleScriptManager } = require('vibecodingmachine-core');
7
+
8
+ /**
9
+ * Check if Kiro agent has hit a rate limit.
10
+ * @param {string} stderr - Standard error output from the agent.
11
+ * @returns {{isRateLimited: boolean, message: string|null}} - Rate limit status and message.
12
+ */
13
+ function checkKiroRateLimit(stderr) {
14
+ const rateLimitPatterns = [
15
+ /quota limit/i,
16
+ /rate limit/i,
17
+ /too many requests/i,
18
+ /limit exceeded/i,
19
+ /usage limit/i,
20
+ /api limit/i,
21
+ /request limit/i,
22
+ /spending cap/i,
23
+ /usage cap/i,
24
+ /quota exceeded/i,
25
+ /limit exceeded/i,
26
+ /usage exceeded/i,
27
+ /rate limited/i,
28
+ /api limited/i,
29
+ /request limited/i,
30
+ /quota reached/i,
31
+ /limit reached/i,
32
+ /usage reached/i,
33
+ /cap reached/i,
34
+ /cap exceeded/i,
35
+ /daily limit/i,
36
+ /monthly limit/i,
37
+ /hourly limit/i,
38
+ /token limit/i,
39
+ /credit limit/i,
40
+ /billing limit/i,
41
+ /subscription limit/i,
42
+ /plan limit/i,
43
+ /tier limit/i,
44
+ /upgrade required/i,
45
+ /upgrade needed/i,
46
+ /upgrade to pro/i,
47
+ /upgrade to premium/i,
48
+ /upgrade plan/i,
49
+ /increase limit/i,
50
+ /exhausted/i,
51
+ /no more requests/i,
52
+ /no more credits/i,
53
+ /insufficient credits/i,
54
+ /insufficient quota/i,
55
+ /maximum reached/i,
56
+ /max requests/i,
57
+ /max usage/i,
58
+ /over limit/i,
59
+ /over quota/i,
60
+ /over usage/i,
61
+ /out of credits/i,
62
+ /out of credit/i,
63
+ /credits exhausted/i,
64
+ /credits depleted/i,
65
+ /credits used up/i,
66
+ /credit exhausted/i,
67
+ /credit depleted/i,
68
+ /credit used up/i,
69
+ /credit balance is too low/i,
70
+ /not enough credits/i,
71
+ /credits: 0/i,
72
+ /credit balance: 0/i,
73
+ /no credits available/i,
74
+ /purchase more credits/i,
75
+ /credits: \d+ remaining/i, // Credits: 0 remaining, Credits: 5 remaining, etc.
76
+ /credit balance: \d+/i, // Credit balance: 0, Credit balance: 5, etc.
77
+ /quota.*exceeded/i,
78
+ /limit.*exceeded/i,
79
+ /quota.*reached/i,
80
+ /limit.*reached/i,
81
+ /cap.*exceeded/i,
82
+ /cap.*reached/i,
83
+ /quota.*exhaust/i,
84
+ /limit.*exhaust/i,
85
+ /quota.*exhausted/i,
86
+ /limit.*exhausted/i,
87
+ /credit.*out of/i,
88
+ /credits.*out of/i,
89
+ /credit.*exhausted/i,
90
+ /credits.*exhausted/i,
91
+ /credit.*depleted/i,
92
+ /credits.*depleted/i,
93
+ /credit.*used up/i,
94
+ /credits.*used up/i,
95
+ /429/, // HTTP 429 Too Many Requests
96
+ /403/, // HTTP 403 Forbidden (sometimes used for quota)
97
+ /402/, // HTTP 402 Payment Required (sometimes used for quota)
98
+ /throttl/i, // throttling
99
+ /quota.*violat/i, // quota violation
100
+ /limit.*violat/i, // limit violation
101
+ /usage.*violat/i, // usage violation
102
+ /quota.*surpass/i, // quota surpassed
103
+ /limit.*surpass/i, // limit surpassed
104
+ /usage.*surpass/i, // usage surpassed
105
+ /quota.*exceed/i, // quota exceed
106
+ /limit.*exceed/i, // limit exceed
107
+ /usage.*exceed/i, // usage exceed
108
+ /quota.*over/i, // quota over
109
+ /limit.*over/i, // limit over
110
+ /usage.*over/i, // usage over
111
+ /quota.*hit/i, // quota hit
112
+ /limit.*hit/i, // limit hit
113
+ /usage.*hit/i, // usage hit
114
+ /quota.*reach/i, // quota reach
115
+ /limit.*reach/i, // limit reach
116
+ /usage.*reach/i, // usage reach
117
+ /quota.*max/i, // quota max
118
+ /limit.*max/i, // limit max
119
+ /usage.*max/i, // usage max
120
+ /quota.*full/i, // quota full
121
+ /limit.*full/i, // limit full
122
+ /usage.*full/i, // usage full
123
+ /quota.*deplet/i, // quota depleted
124
+ /limit.*deplet/i, // limit depleted
125
+ /usage.*deplet/i, // usage depleted
126
+ /quota.*consum/i, // quota consumed
127
+ /limit.*consum/i, // limit consumed
128
+ /usage.*consum/i, // usage consumed
129
+ /quota.*spent/i, // quota spent
130
+ /limit.*spent/i, // limit spent
131
+ /usage.*spent/i, // usage spent
132
+ /quota.*used/i, // quota used up
133
+ /limit.*used/i, // limit used up
134
+ /usage.*used/i, // usage used up
135
+ /quota.*finish/i, // quota finished
136
+ /limit.*finish/i, // limit finished
137
+ /usage.*finish/i, // usage finished
138
+ /quota.*end/i, // quota ended
139
+ /limit.*end/i, // limit ended
140
+ /usage.*end/i, // usage ended
141
+ /quota.*complete/i, // quota completed
142
+ /limit.*complete/i, // limit completed
143
+ /usage.*complete/i, // usage completed
144
+ /quota.*done/i, // quota done
145
+ /limit.*done/i, // limit done
146
+ /usage.*done/i // usage done
147
+ ];
148
+
149
+ for (const pattern of rateLimitPatterns) {
150
+ if (pattern.test(stderr)) {
151
+ return {
152
+ isRateLimited: true,
153
+ message: 'AWS Kiro quota limit reached.'
154
+ };
155
+ }
156
+ }
157
+
158
+ return { isRateLimited: false, message: null };
159
+ }
160
+
161
+ /**
162
+ * Handle rate limit for Kiro by disabling it and selecting the next available provider.
163
+ * @returns {Promise<{success: boolean, nextProvider: string|null, error: string|null}>}
164
+ */
165
+ async function handleKiroRateLimit() {
166
+ console.log(chalk.yellow('AWS Kiro rate limit detected. Switching to next provider...'));
167
+
168
+ try {
169
+ const prefs = await getProviderPreferences();
170
+ prefs.enabled.kiro = false;
171
+ await saveProviderPreferences(prefs.order, prefs.enabled);
172
+
173
+ const nextProvider = prefs.order.find(p => p !== 'kiro' && prefs.enabled[p]);
174
+ if (nextProvider) {
175
+ console.log(chalk.cyan(`Switching to next available provider: ${nextProvider}`));
176
+ return { success: true, nextProvider, error: null };
177
+ } else {
178
+ return { success: false, nextProvider: null, error: 'No fallback providers available.' };
179
+ }
180
+ } catch (error) {
181
+ return { success: false, nextProvider: null, error: 'Failed to update provider preferences.' };
182
+ }
183
+ }
184
+
185
+ module.exports = {
186
+ checkKiroRateLimit,
187
+ handleKiroRateLimit
188
+ };
@@ -0,0 +1,31 @@
1
+ const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
2
+
3
+ /**
4
+ * Read provider rate-limit file and return a Map of { providerId => quota-like object }
5
+ * The returned quota objects follow the minimal shape used by `interactive.js`:
6
+ * { type: 'rate-limit', remaining: 0, resetsAt: <ISO string> }
7
+ */
8
+ function getProviderRateLimitedQuotas(definitions) {
9
+ const pm = new ProviderManager();
10
+ const map = new Map();
11
+
12
+ definitions.forEach(def => {
13
+ try {
14
+ // Ask for provider-level info (checks all models)
15
+ const info = pm.getRateLimitInfo(def.id);
16
+ if (info && info.isRateLimited) {
17
+ map.set(def.id, {
18
+ type: 'rate-limit',
19
+ remaining: 0,
20
+ resetsAt: info.resetTime ? new Date(info.resetTime).toISOString() : null
21
+ });
22
+ }
23
+ } catch (e) {
24
+ // Ignore errors — this helper is best-effort for immediate UI display
25
+ }
26
+ });
27
+
28
+ return map;
29
+ }
30
+
31
+ module.exports = { getProviderRateLimitedQuotas };
@@ -54,6 +54,10 @@ const PROVIDER_DEFINITIONS = [
54
54
  type: 'ide',
55
55
  category: 'ide',
56
56
  ide: 'windsurf',
57
+ subAgents: [
58
+ { id: 'windsurf-default', name: 'Default', model: 'default' },
59
+ { id: 'windsurf-swe-1-lite', name: 'SWE-1-lite', model: 'swe-1-lite' }
60
+ ],
57
61
  estimatedSpeed: 90000
58
62
  },
59
63
  {
@@ -62,6 +66,13 @@ const PROVIDER_DEFINITIONS = [
62
66
  type: 'ide',
63
67
  category: 'ide',
64
68
  ide: 'antigravity',
69
+ subAgents: [
70
+ { id: 'antigravity-default', name: 'Default', model: 'antigravity' },
71
+ { id: 'antigravity-gemini-3-pro-low', name: 'Gemini 3 Pro (Low)', model: 'Gemini 3 Pro (Low)' },
72
+ { id: 'antigravity-claude-sonnet-4-5', name: 'Claude Sonnet 4.5', model: 'Claude Sonnet 4.5' },
73
+ { id: 'antigravity-claude-sonnet-4-5-thinking', name: 'Claude Sonnet 4.5 (Thinking)', model: 'Claude Sonnet 4.5 (Thinking)' },
74
+ { id: 'antigravity-gpt-oss-120b-medium', name: 'GPT-OSS 120B (Medium)', model: 'GPT-OSS 120B (Medium)' }
75
+ ],
65
76
  estimatedSpeed: 90000
66
77
  },
67
78
  {
@@ -72,6 +83,32 @@ const PROVIDER_DEFINITIONS = [
72
83
  ide: 'kiro',
73
84
  estimatedSpeed: 90000
74
85
  },
86
+ {
87
+ id: 'github-copilot',
88
+ name: 'GitHub Copilot (VS Code)',
89
+ type: 'ide',
90
+ category: 'ide',
91
+ ide: 'vscode',
92
+ extension: 'github-copilot',
93
+ estimatedSpeed: 50000
94
+ },
95
+ {
96
+ id: 'amazon-q',
97
+ name: 'Amazon Q Developer (VS Code)',
98
+ type: 'ide',
99
+ category: 'ide',
100
+ ide: 'vscode',
101
+ extension: 'amazon-q',
102
+ estimatedSpeed: 50000
103
+ },
104
+ {
105
+ id: 'replit',
106
+ name: 'Replit Agent',
107
+ type: 'ide',
108
+ category: 'ide',
109
+ ide: 'replit',
110
+ estimatedSpeed: 70000
111
+ },
75
112
  {
76
113
  id: 'ollama',
77
114
  name: 'Ollama (Local)',
@@ -111,7 +148,11 @@ function getProviderDisplayName(id) {
111
148
  'vscode': 'ide.agent.vscode',
112
149
  'cline': 'ide.agent.cline',
113
150
  'claude-code': 'ide.agent.claude.code',
114
- 'antigravity': 'ide.agent.antigravity'
151
+ 'antigravity': 'ide.agent.antigravity',
152
+ 'github-copilot': 'ide.agent.github.copilot',
153
+ 'amazon-q': 'ide.agent.amazon.q',
154
+ 'replit': 'ide.agent.replit',
155
+ 'kiro': 'ide.agent.kiro'
115
156
  };
116
157
 
117
158
  const key = translationKeyMap[id];
@@ -0,0 +1,107 @@
1
+ // Convert legacy PACKAGE: blocks and bullet (- ) items into ### heading requirements
2
+ // Provides a pure function `convertPackageBlocksToHeadings(content, sectionKey, sectionTitle)`
3
+
4
+ function convertPackageBlocksToHeadings(content, sectionKey, sectionTitle) {
5
+ const lines = content.split('\n');
6
+ const out = [];
7
+ let inSection = false;
8
+ let i = 0;
9
+
10
+ // helper to detect section header (## but not ###)
11
+ function isSectionHeader(trimmed) {
12
+ return trimmed.startsWith('##') && !trimmed.startsWith('###');
13
+ }
14
+
15
+ const todoVariants = ['⏳ Requirements not yet completed', 'Requirements not yet completed'];
16
+
17
+ while (i < lines.length) {
18
+ const line = lines[i];
19
+ const trimmed = line.trim();
20
+
21
+ // Section start/stop handling
22
+ if (isSectionHeader(trimmed)) {
23
+ // Enter section if header matches sectionTitle or TODO variants
24
+ if (sectionTitle && sectionTitle.length > 0) {
25
+ const matches = trimmed.includes(sectionTitle) || todoVariants.some(v => trimmed.includes(v));
26
+ if (matches) {
27
+ inSection = true;
28
+ out.push(line);
29
+ i++;
30
+ continue;
31
+ }
32
+ }
33
+
34
+ // If we're in the section and hit a new section header, leave it
35
+ if (inSection) {
36
+ inSection = false;
37
+ }
38
+
39
+ out.push(line);
40
+ i++;
41
+ continue;
42
+ }
43
+
44
+ if (!inSection) {
45
+ out.push(line);
46
+ i++;
47
+ continue;
48
+ }
49
+
50
+ // Inside target section: detect PACKAGE: blocks and bullets (only for TODO)
51
+ if (sectionKey === 'todo' && trimmed.startsWith('PACKAGE:')) {
52
+ const pkg = trimmed.replace(/^PACKAGE:\s*/i, '').trim() || null;
53
+ // gather subsequent non-empty lines until blank line or another PACKAGE:/## header
54
+ const details = [];
55
+ let j = i + 1;
56
+ for (; j < lines.length; j++) {
57
+ const next = lines[j];
58
+ const nextTrim = next.trim();
59
+ if (!nextTrim) {
60
+ // blank line: if we've accumulated details, stop, else skip blanks
61
+ if (details.length > 0) break;
62
+ else continue;
63
+ }
64
+ if (nextTrim.startsWith('PACKAGE:') || (isSectionHeader(nextTrim))) break;
65
+ details.push(next);
66
+ }
67
+
68
+ const title = details.length > 0 ? details[0].trim() : `Package: ${pkg}`;
69
+
70
+ // Emit heading and keep PACKAGE line afterward and the remaining details (excluding title line)
71
+ out.push(`### ${title}`);
72
+ // Keep original PACKAGE line so that package is visible in the converted output
73
+ if (pkg) out.push(`PACKAGE: ${pkg}`);
74
+
75
+ for (let k = 1; k < details.length; k++) {
76
+ out.push(details[k]);
77
+ }
78
+
79
+ // add a blank line after the converted block for readability
80
+ out.push('');
81
+
82
+ i = j; // continue from j
83
+ continue;
84
+ }
85
+
86
+ // Bullet format (only convert in TODO section)
87
+ if (sectionKey === 'todo' && trimmed.startsWith('- ')) {
88
+ const title = trimmed.substring(2).trim();
89
+ if (title) {
90
+ out.push(`### ${title}`);
91
+ out.push('');
92
+ } else {
93
+ out.push(line);
94
+ }
95
+ i++;
96
+ continue;
97
+ }
98
+
99
+ // Default: copy line
100
+ out.push(line);
101
+ i++;
102
+ }
103
+
104
+ return out.join('\n');
105
+ }
106
+
107
+ module.exports = { convertPackageBlocksToHeadings };
@@ -0,0 +1,144 @@
1
+ // parseRequirementsFromContent(content, sectionKey, sectionTitle)
2
+ // Extract requirements from a REQUIREMENTS markdown file supporting:
3
+ // - New format (### Title)
4
+ // - Legacy PACKAGE: block format
5
+ // - Bullet (- ) items
6
+
7
+ function parseRequirementsFromContent(content, sectionKey, sectionTitle) {
8
+ const lines = content.split('\n');
9
+ const requirements = [];
10
+ let inSection = false;
11
+
12
+ for (let i = 0; i < lines.length; i++) {
13
+ const line = lines[i];
14
+ const trimmed = line.trim();
15
+
16
+ // Section header detection
17
+ if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
18
+ // Flexible matching for TODO and VERIFY headers to account for emoji or plain text variants
19
+ if (sectionKey === 'verify') {
20
+ const isToVerifyHeader = trimmed === '## 🔍 TO VERIFY BY HUMAN' ||
21
+ trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN') ||
22
+ trimmed === '## 🔍 TO VERIFY' ||
23
+ trimmed.startsWith('## 🔍 TO VERIFY') ||
24
+ trimmed === '## TO VERIFY' ||
25
+ trimmed.startsWith('## TO VERIFY') ||
26
+ trimmed === '## ✅ TO VERIFY' ||
27
+ trimmed.startsWith('## ✅ TO VERIFY') ||
28
+ trimmed === sectionTitle ||
29
+ (trimmed.startsWith(sectionTitle) && trimmed.includes('Needs Human to Verify'));
30
+
31
+ if (isToVerifyHeader) {
32
+ // Ensure it's not the VERIFIED section
33
+ if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('📝 VERIFIED')) {
34
+ inSection = true;
35
+ continue;
36
+ }
37
+ } else {
38
+ // Leaving TO VERIFY when hitting other definite sections
39
+ if (trimmed.includes('⏳ Requirements not yet completed') ||
40
+ trimmed.includes('Requirements not yet completed') ||
41
+ trimmed === '## 📝 VERIFIED' ||
42
+ trimmed.startsWith('## 📝 VERIFIED')) {
43
+ inSection = false;
44
+ }
45
+ }
46
+ }
47
+
48
+ // General handling for other sections
49
+ if (sectionTitle && sectionTitle.length > 0) {
50
+ // For TODO section, match both TODO variants and section title
51
+ if (sectionKey === 'todo') {
52
+ const todoVariants = ['⏳ Requirements not yet completed', 'Requirements not yet completed'];
53
+ const isTodoHeader = todoVariants.some(v => trimmed.includes(v));
54
+ if (isTodoHeader || trimmed.includes(sectionTitle)) {
55
+ inSection = true;
56
+ continue;
57
+ }
58
+ }
59
+ // For all other sections, only match the specific section title
60
+ else if (trimmed.includes(sectionTitle)) {
61
+ inSection = true;
62
+ continue;
63
+ }
64
+ }
65
+
66
+ if (inSection) {
67
+ // If we're already in the section and we hit another ## header that doesn't match, we're leaving it
68
+ const matchesOurSection = sectionTitle && sectionTitle.length > 0 && trimmed.includes(sectionTitle);
69
+ const matchesTodo = sectionKey === 'todo' && ['⏳ Requirements not yet completed', 'Requirements not yet completed'].some(v => trimmed.includes(v));
70
+
71
+ if (!(matchesOurSection || matchesTodo)) {
72
+ break; // left the section
73
+ }
74
+ }
75
+ }
76
+
77
+ if (!inSection) continue;
78
+
79
+ // New-format requirements (### Title)
80
+ if (line.trim().startsWith('###')) {
81
+ // Remove ALL leading ### markers including spaces between them (handles "###", "### ###", "#### ####", etc.)
82
+ const title = line.trim().replace(/^(#{1,}\s*)+/, '').trim();
83
+ const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
84
+ if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
85
+ continue;
86
+ }
87
+
88
+ const details = [];
89
+ let pkg = null;
90
+ let j;
91
+ for (j = i + 1; j < lines.length; j++) {
92
+ const nextLine = lines[j].trim();
93
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) break;
94
+ // If we encounter a PACKAGE: line here, stop consuming details so the package
95
+ // block will be parsed as its own separate requirement rather than being
96
+ // absorbed into the previous heading's details.
97
+ if (nextLine.startsWith('PACKAGE:')) {
98
+ break;
99
+ } else if (nextLine) {
100
+ details.push(nextLine);
101
+ }
102
+ }
103
+
104
+ requirements.push({ title, details, pkg, lineIndex: i, source: 'heading' });
105
+ i = j - 1; // Skip past the details we just consumed
106
+ continue;
107
+ }
108
+
109
+ // Legacy PACKAGE: format
110
+ if (inSection && line.trim().startsWith('PACKAGE:')) {
111
+ let pkg = line.trim().replace(/^PACKAGE:\s*/, '').trim() || null;
112
+ const details = [];
113
+ let j;
114
+ for (j = i + 1; j < lines.length; j++) {
115
+ const next = lines[j].trim();
116
+ // Stop if we hit a section header or another requirement heading
117
+ if (next.startsWith('##') && !next.startsWith('###')) break;
118
+ if (next.startsWith('###')) break;
119
+ if (next.startsWith('PACKAGE:')) break;
120
+ if (next === '') { if (details.length > 0) break; else continue; }
121
+ details.push(next);
122
+ }
123
+
124
+ const title = (details.length > 0 ? details[0].trim() : `Package: ${pkg}`);
125
+ while (details.length > 0 && details[details.length - 1].trim() === '') details.pop();
126
+
127
+ requirements.push({ title, details: details.map(d => d.trim()), pkg, lineIndex: i, source: 'package' });
128
+ i = j - 1;
129
+ continue;
130
+ }
131
+
132
+ // Bullet-format (- ) simple items
133
+ if (inSection && line.trim().startsWith('- ') && !line.trim().startsWith('PACKAGE:')) {
134
+ const title = line.trim().substring(2).trim();
135
+ if (!title || title.length === 0) continue;
136
+ requirements.push({ title, details: [], pkg: null, lineIndex: i, source: 'bullet' });
137
+ continue;
138
+ }
139
+ }
140
+
141
+ return requirements;
142
+ }
143
+
144
+ module.exports = { parseRequirementsFromContent };
@@ -0,0 +1,23 @@
1
+ const { handleAntigravityRateLimit } = require('../src/utils/antigravity-js-handler');
2
+ const providerRegistry = require('../src/utils/provider-registry');
3
+
4
+ jest.mock('../src/utils/provider-registry');
5
+
6
+ describe('handleAntigravityRateLimit', () => {
7
+ beforeEach(() => {
8
+ jest.resetAllMocks();
9
+ });
10
+
11
+ test('suggests next provider and does not persistently disable antigravity', async () => {
12
+ providerRegistry.getProviderPreferences.mockResolvedValue({
13
+ order: ['antigravity', 'vscode'],
14
+ enabled: { antigravity: true, vscode: true }
15
+ });
16
+
17
+ const result = await handleAntigravityRateLimit();
18
+
19
+ expect(result.success).toBe(true);
20
+ expect(result.nextProvider).toBe('vscode');
21
+ expect(providerRegistry.saveProviderPreferences).not.toHaveBeenCalled();
22
+ });
23
+ });