vibecodingmachine-core 2025.12.25-25 → 2026.1.3-2209

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.
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Beta Request Management for VibeCodingMachine
3
+ *
4
+ * Handles beta access requests from web, CLI, and GUI applications
5
+ */
6
+
7
+ const crypto = require('crypto');
8
+
9
+ class BetaRequestManager {
10
+ constructor(apiBaseUrl = null) {
11
+ this.apiBaseUrl = apiBaseUrl || process.env.VIBECODINGMACHINE_API_URL || 'https://api.vibecodingmachine.com';
12
+ this.authToken = null;
13
+ }
14
+
15
+ /**
16
+ * Set authentication token for API requests
17
+ */
18
+ setAuthToken(token) {
19
+ this.authToken = token;
20
+ }
21
+
22
+ /**
23
+ * Make authenticated API request
24
+ */
25
+ async apiRequest(endpoint, options = {}) {
26
+ const url = `${this.apiBaseUrl}${endpoint}`;
27
+ const headers = {
28
+ 'Content-Type': 'application/json',
29
+ ...options.headers
30
+ };
31
+
32
+ if (this.authToken) {
33
+ headers.Authorization = `Bearer ${this.authToken}`;
34
+ }
35
+
36
+ const fetch = globalThis.fetch || require('node-fetch');
37
+ const response = await fetch(url, {
38
+ ...options,
39
+ headers
40
+ });
41
+
42
+ if (!response.ok) {
43
+ const error = await response.json().catch(() => ({ error: 'Network error' }));
44
+ throw new Error(error.error || `HTTP ${response.status}`);
45
+ }
46
+
47
+ return response.json();
48
+ }
49
+
50
+ /**
51
+ * Submit a beta access request
52
+ */
53
+ async submitBetaRequest(requestData) {
54
+ try {
55
+ const result = await this.apiRequest('/api/beta/request', {
56
+ method: 'POST',
57
+ body: JSON.stringify({
58
+ ...requestData,
59
+ requestDate: Date.now(),
60
+ status: 'REQUEST'
61
+ })
62
+ });
63
+ return result;
64
+ } catch (error) {
65
+ console.error('Beta request submission failed:', error);
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Check if user already has a pending or approved request
72
+ */
73
+ async checkExistingRequest(email) {
74
+ try {
75
+ const userId = this.generateUserId(email);
76
+ const result = await this.apiRequest(`/api/users/${userId}`);
77
+ return result.user;
78
+ } catch (error) {
79
+ // User doesn't exist or API error
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Validate request data
86
+ */
87
+ validateRequestData(data) {
88
+ const errors = [];
89
+
90
+ if (!data.email || !data.email.trim()) {
91
+ errors.push('Email is required');
92
+ }
93
+
94
+ if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
95
+ errors.push('Invalid email format');
96
+ }
97
+
98
+ if (!data.name || !data.name.trim()) {
99
+ errors.push('Name is required');
100
+ }
101
+
102
+ if (!data.language) {
103
+ errors.push('Language preference is required');
104
+ }
105
+
106
+ if (!data.appPurpose || !data.appPurpose.trim()) {
107
+ errors.push('App purpose description is required');
108
+ }
109
+
110
+ return {
111
+ isValid: errors.length === 0,
112
+ errors
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Get supported languages
118
+ */
119
+ getSupportedLanguages() {
120
+ return {
121
+ 'en': 'English',
122
+ 'es': 'Spanish'
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Generate user ID from email (consistent with existing system)
128
+ */
129
+ generateUserId(email) {
130
+ return crypto.createHash('sha256').update(email.toLowerCase()).digest('hex').substring(0, 16);
131
+ }
132
+
133
+ /**
134
+ * Format request data for email notification
135
+ */
136
+ formatRequestForEmail(requestData) {
137
+ const supportedLanguages = this.getSupportedLanguages();
138
+ const languageName = supportedLanguages[requestData.language] || requestData.language;
139
+
140
+ return {
141
+ recipient: 'jesse.d.olsen@gmail.com',
142
+ subject: `New Beta Request: ${requestData.name} (${requestData.email})`,
143
+ body: `
144
+ New VibeCodingMachine beta access request received:
145
+
146
+ Name: ${requestData.name}
147
+ Email: ${requestData.email}
148
+ Language: ${languageName}
149
+ App Purpose: ${requestData.appPurpose}
150
+ Other Info: ${requestData.otherInfo || 'None provided'}
151
+ Request Date: ${new Date(requestData.requestDate).toLocaleString()}
152
+ Source: ${requestData.source || 'Unknown'}
153
+
154
+ Please review and approve/reject this request in the admin panel.
155
+ `.trim()
156
+ };
157
+ }
158
+ }
159
+
160
+ module.exports = BetaRequestManager;
@@ -228,6 +228,45 @@ class UserDatabaseClient {
228
228
  throw new Error('setAdminStatus not allowed from client');
229
229
  }
230
230
 
231
+ /**
232
+ * Submit a beta access request
233
+ */
234
+ async submitBetaRequest(requestData) {
235
+ if (!this.authToken) {
236
+ console.warn('UserDatabase: No auth token, beta request submission requires authentication');
237
+ throw new Error('Authentication required for beta request');
238
+ }
239
+
240
+ try {
241
+ const result = await this.apiRequest('/api/beta/request', {
242
+ method: 'POST',
243
+ body: JSON.stringify({
244
+ ...requestData,
245
+ requestDate: Date.now(),
246
+ status: 'REQUEST'
247
+ })
248
+ });
249
+ return result;
250
+ } catch (error) {
251
+ console.warn('UserDatabase: submitBetaRequest failed:', error.message);
252
+ throw error;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Check if user already has a pending or approved request
258
+ */
259
+ async checkExistingRequest(email) {
260
+ try {
261
+ const userId = this.generateUserId(email);
262
+ const result = await this.apiRequest(`/api/users/${userId}`);
263
+ return result.user;
264
+ } catch (error) {
265
+ // User doesn't exist or API error
266
+ return null;
267
+ }
268
+ }
269
+
231
270
  /**
232
271
  * Check if user is an admin (not implemented client-side for security)
233
272
  */
@@ -235,6 +274,31 @@ class UserDatabaseClient {
235
274
  return false; // Client-side can't check admin status
236
275
  }
237
276
 
277
+ /**
278
+ * Report an error to the admin database for tracking and fixing
279
+ */
280
+ async reportError(errorData) {
281
+ try {
282
+ const errorReport = {
283
+ timestamp: Date.now(),
284
+ hostname: os.hostname(),
285
+ platform: os.platform(),
286
+ arch: os.arch(),
287
+ nodeVersion: process.version,
288
+ ...errorData
289
+ };
290
+
291
+ await this.apiRequest('/api/errors/report', {
292
+ method: 'POST',
293
+ body: JSON.stringify(errorReport)
294
+ });
295
+
296
+ return true;
297
+ } catch (error) {
298
+ console.warn('UserDatabase: reportError failed:', error.message);
299
+ return false;
300
+ }
301
+ }
238
302
  // Helper methods (same as server-side)
239
303
  generateUserId(email) {
240
304
  return crypto.createHash('sha256').update(email.toLowerCase()).digest('hex').substring(0, 16);
@@ -94,6 +94,20 @@ class UserDatabase {
94
94
  return this.apiClient.setAdminStatus(userId, isAdmin)
95
95
  }
96
96
 
97
+ /**
98
+ * Submit a beta access request
99
+ */
100
+ async submitBetaRequest(requestData) {
101
+ return this.apiClient.submitBetaRequest(requestData)
102
+ }
103
+
104
+ /**
105
+ * Check if user already has a pending or approved request
106
+ */
107
+ async checkExistingRequest(email) {
108
+ return this.apiClient.checkExistingRequest(email)
109
+ }
110
+
97
111
  /**
98
112
  * Check if user is an admin
99
113
  */
@@ -101,6 +115,13 @@ class UserDatabase {
101
115
  return this.apiClient.isAdmin(userId)
102
116
  }
103
117
 
118
+ /**
119
+ * Report an error to the admin database for tracking and fixing
120
+ */
121
+ async reportError(errorData) {
122
+ return this.apiClient.reportError(errorData)
123
+ }
124
+
104
125
  // Helper methods - delegate to API client
105
126
  generateUserId(email) {
106
127
  return this.apiClient.generateUserId(email)
@@ -335,7 +335,7 @@ class AppleScriptManager {
335
335
  };
336
336
  }
337
337
 
338
- const ideName = ide === 'windsurf' ? 'Windsurf' : ide === 'cursor' ? 'Cursor' : ide === 'antigravity' ? 'Antigravity' : ide === 'kiro' ? 'AWS Kiro' : 'Unknown IDE';
338
+ const ideName = ide === 'windsurf' ? 'Windsurf' : ide === 'cursor' ? 'Cursor' : ide === 'antigravity' ? 'Antigravity' : ide === 'kiro' ? 'AWS Kiro' : (ide === 'claude' || ide === 'claude-code') ? 'Claude' : 'Unknown IDE';
339
339
 
340
340
  this.logger.log(t('auto.direct.ide.detected', { ide: ideName }));
341
341
 
@@ -452,11 +452,11 @@ class AppleScriptManager {
452
452
  on error
453
453
  set processName to "Kiro"
454
454
  end try
455
-
455
+
456
456
  tell process processName
457
457
  set frontmost to true
458
458
  delay 1.0
459
-
459
+
460
460
  -- Attempt to focus chat via standard AI IDE shortcuts
461
461
  -- Try Cmd+L (Cursor/Windsurf standard)
462
462
  try
@@ -469,22 +469,42 @@ class AppleScriptManager {
469
469
  -- Use clipboard for more reliable text entry than keystrokes
470
470
  set the clipboard to "${text.replace(/"/g, '\\"')}"
471
471
  delay 0.3
472
-
472
+
473
473
  -- Paste
474
474
  key code 9 using {command down}
475
475
  delay 0.5
476
-
476
+
477
477
  -- Send Enter
478
478
  key code 36
479
479
  delay 0.5
480
480
  end tell
481
481
  end tell
482
482
  `;
483
+ } else if (ide === 'claude' || ide === 'claude-code') {
484
+ // Claude CLI text sending - simplified Terminal approach
485
+ this.logger.log('🚀 [Claude] Sending text to Claude terminal...');
486
+
487
+ appleScript = `
488
+ tell application "System Events"
489
+ tell process "Terminal"
490
+ set frontmost to true
491
+ delay 0.5
492
+
493
+ -- Type the text
494
+ keystroke "${text.replace(/"/g, '\\"')}"
495
+ delay 1.0
496
+
497
+ -- Submit with return
498
+ keystroke return
499
+ delay 0.3
500
+ end tell
501
+ end tell
502
+ `;
483
503
  } else {
484
504
  return {
485
505
  success: false,
486
506
  error: `Unsupported IDE for AppleScript: ${ide}`,
487
- note: 'AppleScript is only supported for Cursor, Windsurf, Antigravity, VS Code, and AWS Kiro'
507
+ note: 'AppleScript is only supported for Cursor, Windsurf, Antigravity, VS Code, AWS Kiro, and Claude'
488
508
  };
489
509
  }
490
510
 
@@ -714,13 +714,14 @@ class AppleScriptManager {
714
714
  case 'kiro':
715
715
  return await this.openKiro(repoPath);
716
716
  case 'claude':
717
+ case 'claude-code':
717
718
  return await this.openClaude(repoPath);
718
719
  case 'gemini':
719
720
  return await this.openGemini(repoPath);
720
721
  default:
721
722
  return {
722
723
  success: false,
723
- error: `Unknown IDE: ${ide}. Supported: cursor, windsurf, antigravity, vscode, claude, gemini`
724
+ error: `Unknown IDE: ${ide}. Supported: cursor, windsurf, antigravity, vscode, claude, claude-code, gemini`
724
725
  };
725
726
  }
726
727
  }
@@ -879,7 +880,7 @@ class AppleScriptManager {
879
880
  };
880
881
  }
881
882
 
882
- const ideName = ide === 'windsurf' ? 'Windsurf' : ide === 'cursor' ? 'Cursor' : ide === 'antigravity' ? 'Antigravity' : ide === 'claude' ? 'Claude' : ide === 'vscode' ? 'VS Code' : ide === 'kiro' ? 'AWS Kiro' : 'Unknown IDE';
883
+ const ideName = ide === 'windsurf' ? 'Windsurf' : ide === 'cursor' ? 'Cursor' : ide === 'antigravity' ? 'Antigravity' : (ide === 'claude' || ide === 'claude-code') ? 'Claude' : ide === 'vscode' ? 'VS Code' : ide === 'kiro' ? 'AWS Kiro' : 'Unknown IDE';
883
884
 
884
885
  this.logger.log(`🚀 [${ideName}] Starting text send on ${this.platform} platform`);
885
886
  this.logger.log(`🚀 [${ideName}] Text to send: "${text}"`);
@@ -1035,7 +1036,7 @@ class AppleScriptManager {
1035
1036
  end tell
1036
1037
  end tell
1037
1038
  `;
1038
- } else if (ide === 'claude') {
1039
+ } else if (ide === 'claude' || ide === 'claude-code') {
1039
1040
  // Use a different approach for Claude - find existing Claude terminal and send text
1040
1041
  const targetRepoPath = repoPath || '/Users/jesse/code/mediawink/vibecodingmachine';
1041
1042
  this.logger.log(`🔍 [Claude] Using repo path for terminal detection: "${targetRepoPath}" (passed: "${repoPath}")`);
@@ -1353,7 +1354,7 @@ class AppleScriptManager {
1353
1354
  this.logger.log('AppleScript interaction failed:', error.message);
1354
1355
 
1355
1356
  // For Claude, don't fall back to simulated response - return actual failure
1356
- if (ide === 'claude') {
1357
+ if (ide === 'claude' || ide === 'claude-code') {
1357
1358
  return {
1358
1359
  success: false,
1359
1360
  method: 'applescript',
@@ -2544,7 +2545,7 @@ end tell
2544
2545
  const ideName = ide.toLowerCase();
2545
2546
 
2546
2547
  // Handle Claude CLI separately since it's a terminal application
2547
- if (ideName === 'claude') {
2548
+ if (ideName === 'claude' || ideName === 'claude-code') {
2548
2549
  return await this.closeClaudeCLI();
2549
2550
  }
2550
2551
 
@@ -54,21 +54,21 @@ end tell`;
54
54
  try
55
55
  -- Use Cmd+L to focus AI panel (faster and more reliable)
56
56
  key code 37 using {command down} -- Cmd+L
57
- delay 1.5
57
+ delay 2.0
58
58
 
59
59
  -- Open new chat session with Cmd+T to prevent crashes
60
60
  key code 17 using {command down} -- Cmd+T
61
- delay 2.5
61
+ delay 3.0
62
62
 
63
63
  -- Skip clearing text to avoid selecting file content
64
64
  -- The chat input should be empty when opening new chat session
65
65
 
66
66
  -- Wait additional time for chat input field to be fully ready
67
- delay 1.0
67
+ delay 1.5
68
68
 
69
69
  -- Type the message
70
70
  keystroke "${escapedText}"
71
- delay 1.5
71
+ delay 2.0
72
72
 
73
73
  -- Send with Cmd+Enter (standard for chat interfaces)
74
74
  key code 36 using {command down}
@@ -86,33 +86,41 @@ end tell`;
86
86
  on error
87
87
  -- STRATEGY 2: Command Palette Approach (Fallback 1) - NO AI PANEL TOGGLE
88
88
  try
89
- -- Step 1: Open Command Palette (Cmd+Shift+P)
89
+ -- Step 1: Press Escape first to ensure we're starting clean
90
+ key code 53 -- Escape
91
+ delay 0.5
92
+
93
+ -- Step 2: Open Command Palette (Cmd+Shift+P)
90
94
  key code 35 using {command down, shift down} -- Cmd+Shift+P
91
- delay 0.8
95
+ delay 1.0
92
96
 
93
- -- Step 2: Type "View: Focus into Secondary Side Bar" to focus AI panel
97
+ -- Step 3: Type "View: Focus into Secondary Side Bar" to focus AI panel
94
98
  keystroke "View: Focus into Secondary Side Bar"
95
- delay 0.8
99
+ delay 1.0
96
100
 
97
- -- Step 3: Press Enter to focus AI Panel
101
+ -- Step 4: Press Enter to execute the command
98
102
  key code 36 -- Enter
99
- delay 2.0
103
+ delay 2.5
100
104
 
101
- -- Step 4: Open new chat session with Cmd+T to prevent crashes
105
+ -- Step 5: CRITICAL - Press Escape to close command palette
106
+ key code 53 -- Escape
107
+ delay 0.5
108
+
109
+ -- Step 6: Open new chat session with Cmd+T to prevent crashes
102
110
  key code 17 using {command down} -- Cmd+T
103
- delay 2.5
111
+ delay 3.0
104
112
 
105
- -- Step 5: Skip clearing text to avoid selecting file content
113
+ -- Step 7: Skip clearing text to avoid selecting file content
106
114
  -- The chat input should be empty when opening new chat session
107
115
 
108
- -- Step 6: Wait additional time for chat input field to be fully ready
109
- delay 1.0
116
+ -- Step 8: Wait additional time for chat input field to be fully ready
117
+ delay 1.5
110
118
 
111
- -- Step 7: Type the message
119
+ -- Step 9: Type the message
112
120
  keystroke "${escapedText}"
113
- delay 1.5
121
+ delay 2.0
114
122
 
115
- -- Step 8: Send with Cmd+Enter (standard for chat interfaces)
123
+ -- Step 10: Send with Cmd+Enter (standard for chat interfaces)
116
124
  key code 36 using {command down}
117
125
  delay 0.5
118
126
 
package/src/index.cjs CHANGED
@@ -20,6 +20,8 @@ const auditLogger = require('./utils/audit-logger.cjs');
20
20
  const { GCloudAuth } = require('./utils/gcloud-auth.cjs');
21
21
  const requirementHelpers = require('./utils/requirement-helpers.js');
22
22
  const requirementsParser = require('./utils/requirements-parser.js');
23
+ const requirementNumbering = require('./requirement-numbering.js');
24
+ const gitBranchManager = require('./utils/git-branch-manager.js');
23
25
  const updateChecker = require('./utils/update-checker.js');
24
26
  const electronUpdateChecker = require('./utils/electron-update-checker.js');
25
27
  const localization = require('./localization/index.js');
@@ -45,6 +47,8 @@ module.exports = {
45
47
  ...auditLogger,
46
48
  ...requirementHelpers,
47
49
  ...requirementsParser,
50
+ ...requirementNumbering,
51
+ ...gitBranchManager,
48
52
  ...updateChecker,
49
53
  ...electronUpdateChecker,
50
54
  ...localization
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ export * from './utils/repo-helpers.js';
12
12
  export * from './utils/config-helpers.js';
13
13
  export * from './utils/requirement-helpers.js';
14
14
  export * from './utils/requirements-parser.js';
15
+ export * from './requirement-numbering.js';
15
16
 
16
17
  // Compliance
17
18
  const CompliancePrompt = require('./compliance/compliance-prompt.js');
@@ -24,3 +25,6 @@ export { ChatInterface } from './ui/ChatInterface.js';
24
25
 
25
26
  // Localization
26
27
  export * from './localization/index.js';
28
+
29
+ // Error Reporting
30
+ export * from './utils/error-reporter.js';
@@ -344,6 +344,8 @@ module.exports = {
344
344
 
345
345
  // System Information
346
346
  'system.repo': 'Repo:',
347
+ 'system.git.branch': 'Branch:',
348
+ 'system.git.status.dirty': '(modified)',
347
349
  'system.computer.name': 'Computer Name:',
348
350
 
349
351
  // Configuration Dialogs
@@ -344,6 +344,8 @@ module.exports = {
344
344
 
345
345
  // System Information
346
346
  'system.repo': 'Repositorio:',
347
+ 'system.git.branch': 'Rama:',
348
+ 'system.git.status.dirty': '(modificado)',
347
349
  'system.computer.name': 'Nombre de Computadora:',
348
350
 
349
351
  // Configuration Dialogs
@@ -0,0 +1,164 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Get metadata file path for storing requirement counter
6
+ * @param {string} repoPath - Path to repository
7
+ * @returns {string} Path to metadata file
8
+ */
9
+ function getMetadataPath(repoPath) {
10
+ return path.join(repoPath, '.vibecodingmachine', 'requirement-metadata.json');
11
+ }
12
+
13
+ /**
14
+ * Get the next requirement number for this repository
15
+ * @param {string} repoPath - Path to repository
16
+ * @returns {Promise<number>} Next requirement number
17
+ */
18
+ async function getNextRequirementNumber(repoPath) {
19
+ const metadataPath = getMetadataPath(repoPath);
20
+
21
+ try {
22
+ await fs.ensureFile(metadataPath);
23
+ const metadata = await fs.readJson(metadataPath).catch(() => ({ lastRequirementNumber: 0 }));
24
+
25
+ const nextNumber = (metadata.lastRequirementNumber || 0) + 1;
26
+
27
+ // Save the incremented number
28
+ metadata.lastRequirementNumber = nextNumber;
29
+ await fs.writeJson(metadataPath, metadata, { spaces: 2 });
30
+
31
+ return nextNumber;
32
+ } catch (error) {
33
+ // Default to 1 if there's any error
34
+ console.error('Error getting next requirement number:', error.message);
35
+ return 1;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Set the last requirement number (used during initialization)
41
+ * @param {string} repoPath - Path to repository
42
+ * @param {number} lastNumber - Last requirement number
43
+ */
44
+ async function setLastRequirementNumber(repoPath, lastNumber) {
45
+ const metadataPath = getMetadataPath(repoPath);
46
+
47
+ await fs.ensureFile(metadataPath);
48
+ const metadata = await fs.readJson(metadataPath).catch(() => ({}));
49
+ metadata.lastRequirementNumber = lastNumber;
50
+ await fs.writeJson(metadataPath, metadata, { spaces: 2 });
51
+ }
52
+
53
+ /**
54
+ * Extract requirement number from requirement title
55
+ * @param {string} title - Requirement title
56
+ * @returns {number|null} Requirement number or null if not found
57
+ */
58
+ function extractRequirementNumber(title) {
59
+ const match = title.match(/^R(\d+):/);
60
+ return match ? parseInt(match[1], 10) : null;
61
+ }
62
+
63
+ /**
64
+ * Number all existing requirements in a requirements file
65
+ * @param {string} reqPath - Path to requirements file
66
+ * @param {string} repoPath - Path to repository
67
+ * @returns {Promise<number>} Number of requirements numbered
68
+ */
69
+ async function numberAllRequirements(reqPath, repoPath) {
70
+ if (!await fs.pathExists(reqPath)) {
71
+ return 0;
72
+ }
73
+
74
+ const content = await fs.readFile(reqPath, 'utf8');
75
+ const lines = content.split('\n');
76
+ const newLines = [];
77
+
78
+ let requirementCount = 0;
79
+ let maxExistingNumber = 0;
80
+
81
+ // First pass: collect existing numbers and count requirements
82
+ for (const line of lines) {
83
+ if (line.trim().startsWith('###')) {
84
+ const title = line.trim().substring(3).trim();
85
+ const existingNumber = extractRequirementNumber(title);
86
+
87
+ if (existingNumber) {
88
+ maxExistingNumber = Math.max(maxExistingNumber, existingNumber);
89
+ } else {
90
+ // This requirement needs numbering
91
+ requirementCount++;
92
+ }
93
+ }
94
+ }
95
+
96
+ // Start numbering from max existing + 1
97
+ let currentNumber = maxExistingNumber + 1;
98
+ let numbered = 0;
99
+
100
+ // Second pass: add numbers to requirements that don't have them
101
+ for (const line of lines) {
102
+ if (line.trim().startsWith('###')) {
103
+ const title = line.trim().substring(3).trim();
104
+ const existingNumber = extractRequirementNumber(title);
105
+
106
+ if (!existingNumber) {
107
+ // Add number to this requirement
108
+ newLines.push(`### R${currentNumber}: ${title}`);
109
+ currentNumber++;
110
+ numbered++;
111
+ } else {
112
+ // Keep existing numbered requirement as-is
113
+ newLines.push(line);
114
+ }
115
+ } else {
116
+ newLines.push(line);
117
+ }
118
+ }
119
+
120
+ // Save the updated file
121
+ await fs.writeFile(reqPath, newLines.join('\n'));
122
+
123
+ // Update metadata with the highest number used
124
+ await setLastRequirementNumber(repoPath, currentNumber - 1);
125
+
126
+ return numbered;
127
+ }
128
+
129
+ /**
130
+ * Create a git branch name from requirement number and title
131
+ * @param {number} reqNumber - Requirement number
132
+ * @param {string} title - Requirement title
133
+ * @returns {string} Branch name (e.g., "R1_email_system")
134
+ */
135
+ function createBranchName(reqNumber, title) {
136
+ // Remove requirement number from title if present
137
+ let cleanTitle = title.replace(/^R\d+:\s*/, '').trim();
138
+
139
+ // Take first few words (up to 3-4 words, max 30 chars)
140
+ const words = cleanTitle.split(/\s+/);
141
+ let branchSuffix = '';
142
+
143
+ for (const word of words) {
144
+ const candidate = branchSuffix ? `${branchSuffix}_${word}` : word;
145
+ if (candidate.length > 30) break;
146
+ branchSuffix = candidate;
147
+ }
148
+
149
+ // Clean up branch suffix: lowercase, remove special chars
150
+ branchSuffix = branchSuffix
151
+ .toLowerCase()
152
+ .replace(/[^a-z0-9]+/g, '_')
153
+ .replace(/^_+|_+$/g, '');
154
+
155
+ return branchSuffix ? `R${reqNumber}_${branchSuffix}` : `R${reqNumber}`;
156
+ }
157
+
158
+ module.exports = {
159
+ getNextRequirementNumber,
160
+ setLastRequirementNumber,
161
+ extractRequirementNumber,
162
+ numberAllRequirements,
163
+ createBranchName
164
+ };