vibecodingmachine-cli 2026.1.3-2209 → 2026.1.22-1441
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/__tests__/antigravity-js-handler.test.js +23 -0
- package/__tests__/provider-manager.test.js +84 -0
- package/__tests__/provider-rate-cache.test.js +27 -0
- package/bin/vibecodingmachine.js +8 -0
- package/package.json +2 -2
- package/reset_provider_order.js +21 -0
- package/scripts/convert-requirements.js +35 -0
- package/scripts/debug-parse.js +24 -0
- package/src/commands/auto-direct.js +679 -120
- package/src/commands/auto.js +200 -45
- package/src/commands/ide.js +108 -3
- package/src/commands/requirements-remote.js +10 -1
- package/src/commands/status.js +39 -1
- package/src/utils/antigravity-js-handler.js +13 -4
- package/src/utils/auth.js +37 -13
- package/src/utils/compliance-check.js +10 -0
- package/src/utils/config.js +29 -1
- package/src/utils/date-formatter.js +44 -0
- package/src/utils/interactive.js +1006 -537
- package/src/utils/kiro-js-handler.js +188 -0
- package/src/utils/provider-rate-cache.js +31 -0
- package/src/utils/provider-registry.js +42 -1
- package/src/utils/requirements-converter.js +107 -0
- package/src/utils/requirements-parser.js +144 -0
- package/tests/antigravity-js-handler.test.js +23 -0
- package/tests/integration/health-tracking.integration.test.js +284 -0
- package/tests/provider-manager.test.js +92 -0
- package/tests/rate-limit-display.test.js +44 -0
- package/tests/requirements-bullet-parsing.test.js +15 -0
- package/tests/requirements-converter.test.js +42 -0
- package/tests/requirements-heading-count.test.js +27 -0
- package/tests/requirements-legacy-parsing.test.js +15 -0
- package/tests/requirements-parse-integration.test.js +44 -0
- package/tests/wait-for-ide-completion.test.js +56 -0
- package/tests/wait-for-ide-quota-detection-cursor-screenshot.test.js +61 -0
- package/tests/wait-for-ide-quota-detection-cursor.test.js +60 -0
- package/tests/wait-for-ide-quota-detection-negative.test.js +45 -0
- package/tests/wait-for-ide-quota-detection.test.js +59 -0
- package/verify_fix.js +36 -0
- 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
|
+
});
|