maiass 5.9.6 → 5.9.9

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/lib/commit.js CHANGED
@@ -13,6 +13,51 @@ import { logCommit } from './devlog.js';
13
13
  import colors from './colors.js';
14
14
  import chalk from 'chalk';
15
15
 
16
+ /**
17
+ * Simple spinner for AI API calls
18
+ */
19
+ class Spinner {
20
+ constructor(message = 'Working') {
21
+ this.message = message;
22
+ this.frames = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f'];
23
+ this.frameIndex = 0;
24
+ this.intervalId = null;
25
+ this.isSpinning = false;
26
+ }
27
+
28
+ start() {
29
+ if (this.isSpinning) return;
30
+ this.isSpinning = true;
31
+
32
+ // Hide cursor
33
+ process.stderr.write('\x1B[?25l');
34
+
35
+ this.intervalId = setInterval(() => {
36
+ const frame = this.frames[this.frameIndex];
37
+ process.stderr.write(`\r${colors.BYellow(frame)} ${this.message}...`);
38
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
39
+ }, 100);
40
+ }
41
+
42
+ stop(success = true) {
43
+ if (!this.isSpinning) return;
44
+ this.isSpinning = false;
45
+
46
+ if (this.intervalId) {
47
+ clearInterval(this.intervalId);
48
+ this.intervalId = null;
49
+ }
50
+
51
+ // Clear line and show cursor
52
+ process.stderr.write('\r' + ' '.repeat(100) + '\r');
53
+ process.stderr.write('\x1B[?25h');
54
+
55
+ if (success) {
56
+ process.stderr.write(`${colors.BGreen('\u2713')} Done\n`);
57
+ }
58
+ }
59
+ }
60
+
16
61
  /**
17
62
  * Get color for credit display based on remaining credits (matches bashmaiass)
18
63
  * @param {number} credits - Remaining credits
@@ -373,111 +418,150 @@ ${gitDiff}`;
373
418
  log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Request body: ${JSON.stringify(requestBody, null, 2)}`);
374
419
  }
375
420
 
376
- const response = await fetch(aiEndpoint, {
377
- method: 'POST',
378
- headers: {
379
- 'Content-Type': 'application/json',
380
- 'Authorization': `Bearer ${maiassToken}`,
381
- 'X-Machine-Fingerprint': generateMachineFingerprint(),
382
- 'X-Client-Name': getClientName(),
383
- 'X-Client-Version': getClientVersion(),
384
- 'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || ''
385
- },
386
- body: JSON.stringify(requestBody)
387
- });
421
+ // Set timeout for API call (default 30 seconds)
422
+ const timeout = parseInt(process.env.MAIASS_AI_TIMEOUT || '30') * 1000;
388
423
 
389
424
  if (debugMode) {
390
- log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Response status: ${response.status} ${response.statusText}`);
391
- log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Response headers: ${JSON.stringify(Object.fromEntries(response.headers), null, 2)}`);
425
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] API timeout set to ${timeout/1000} seconds`);
392
426
  }
393
-
394
- if (!response.ok) {
395
- const errorText = await response.text();
396
- if (debugMode) {
397
- log.debug(SYMBOLS.WARNING, `[MAIASS DEBUG] Response error body: ${errorText}`);
398
- }
399
- log.error(SYMBOLS.WARNING, `AI API request failed: ${response.status} ${response.statusText}`);
400
- return null;
401
- }
402
-
403
- const data = await response.json();
404
-
405
- if (debugMode) {
406
- log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Response data: ${JSON.stringify(data, null, 2)}`);
407
- }
408
-
409
- if (data.choices && data.choices.length > 0) {
410
- let suggestion = data.choices[0].message.content.trim();
411
427
 
412
- // Enhanced debug logging: output the suggestion returned by AI
428
+ // Start spinner (unless in debug mode)
429
+ const spinner = !debugMode ? new Spinner('Waiting for AI response') : null;
430
+ if (spinner) spinner.start();
431
+
432
+ try {
433
+ // Fetch with timeout using AbortController
434
+ const controller = new AbortController();
435
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
436
+
437
+ const response = await fetch(aiEndpoint, {
438
+ method: 'POST',
439
+ headers: {
440
+ 'Content-Type': 'application/json',
441
+ 'Authorization': `Bearer ${maiassToken}`,
442
+ 'X-Machine-Fingerprint': generateMachineFingerprint(),
443
+ 'X-Client-Name': getClientName(),
444
+ 'X-Client-Version': getClientVersion(),
445
+ 'X-Subscription-ID': process.env.MAIASS_SUBSCRIPTION_ID || ''
446
+ },
447
+ body: JSON.stringify(requestBody),
448
+ signal: controller.signal
449
+ });
450
+
451
+ clearTimeout(timeoutId);
452
+
453
+ // Stop spinner on success
454
+ if (spinner) spinner.stop(true);
455
+
413
456
  if (debugMode) {
414
- log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] --- AI SUGGESTION RETURNED ---');
415
- log.debug(SYMBOLS.INFO, suggestion);
457
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Response status: ${response.status} ${response.statusText}`);
458
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Response headers: ${JSON.stringify(Object.fromEntries(response.headers), null, 2)}`);
459
+ }
460
+
461
+ if (!response.ok) {
462
+ const errorText = await response.text();
463
+ if (debugMode) {
464
+ log.debug(SYMBOLS.WARNING, `[MAIASS DEBUG] Response error body: ${errorText}`);
465
+ }
466
+ log.error(SYMBOLS.WARNING, `AI API request failed: ${response.status} ${response.statusText}`);
467
+ return null;
416
468
  }
417
469
 
418
- // Clean up any quotes that might wrap the entire response
419
- if ((suggestion.startsWith("'") && suggestion.endsWith("'")) ||
420
- (suggestion.startsWith('"') && suggestion.endsWith('"'))) {
421
- suggestion = suggestion.slice(1, -1).trim();
470
+ // Parse JSON response inside the same try block
471
+ const data = await response.json();
472
+
473
+ if (debugMode) {
474
+ log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Response data: ${JSON.stringify(data, null, 2)}`);
422
475
  }
423
476
 
424
- // Extract credit information from billing data
425
- let creditsUsed, creditsRemaining;
426
- if (data.billing) {
427
- creditsUsed = data.billing.credits_used;
428
- creditsRemaining = data.billing.credits_remaining;
477
+ if (data.choices && data.choices.length > 0) {
478
+ let suggestion = data.choices[0].message.content.trim();
479
+
480
+ // Enhanced debug logging: output the suggestion returned by AI
481
+ if (debugMode) {
482
+ log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] --- AI SUGGESTION RETURNED ---');
483
+ log.debug(SYMBOLS.INFO, suggestion);
484
+ }
429
485
 
430
- // Show warnings if available
431
- if (data.billing.warning) {
432
- log.warning(SYMBOLS.WARNING, data.billing.warning);
486
+ // Clean up any quotes that might wrap the entire response
487
+ if ((suggestion.startsWith("'") && suggestion.endsWith("'")) ||
488
+ (suggestion.startsWith('"') && suggestion.endsWith('"'))) {
489
+ suggestion = suggestion.slice(1, -1).trim();
433
490
  }
434
491
 
435
- // Display proxy messages if available
436
- if (data.messages && Array.isArray(data.messages)) {
437
- data.messages.forEach(message => {
438
- const icon = message.icon || '';
439
- const text = message.text || '';
440
-
441
- switch (message.type) {
442
- case 'error':
443
- log.error(icon, text);
444
- break;
445
- case 'warning':
446
- log.warning(icon, text);
447
- break;
448
- case 'info':
449
- log.info(icon, text);
450
- break;
451
- case 'notice':
452
- log.blue(icon, text);
453
- break;
454
- case 'success':
455
- log.success(icon, text);
456
- break;
457
- default:
458
- log.plain(`${icon} ${text}`);
459
- }
460
- });
492
+ // Extract credit information from billing data
493
+ let creditsUsed, creditsRemaining;
494
+ if (data.billing) {
495
+ creditsUsed = data.billing.credits_used;
496
+ creditsRemaining = data.billing.credits_remaining;
497
+
498
+ // Show warnings if available
499
+ if (data.billing.warning) {
500
+ log.warning(SYMBOLS.WARNING, data.billing.warning);
501
+ }
502
+
503
+ // Display proxy messages if available
504
+ if (data.messages && Array.isArray(data.messages)) {
505
+ data.messages.forEach(message => {
506
+ const icon = message.icon || '';
507
+ const text = message.text || '';
508
+
509
+ switch (message.type) {
510
+ case 'error':
511
+ log.error(icon, text);
512
+ break;
513
+ case 'warning':
514
+ log.warning(icon, text);
515
+ break;
516
+ case 'info':
517
+ log.info(icon, text);
518
+ break;
519
+ case 'notice':
520
+ log.blue(icon, text);
521
+ break;
522
+ case 'success':
523
+ log.success(icon, text);
524
+ break;
525
+ default:
526
+ log.plain(`${icon} ${text}`);
527
+ }
528
+ });
529
+ }
530
+ } else if (data.usage) {
531
+ // Fallback to legacy token display
532
+ const totalTokens = data.usage.total_tokens || 0;
533
+ const promptTokens = data.usage.prompt_tokens || 0;
534
+ const completionTokens = data.usage.completion_tokens || 0;
535
+ log.info(SYMBOLS.INFO, `Total Tokens: ${totalTokens} (${promptTokens} + ${completionTokens})`);
461
536
  }
462
- } else if (data.usage) {
463
- // Fallback to legacy token display
464
- const totalTokens = data.usage.total_tokens || 0;
465
- const promptTokens = data.usage.prompt_tokens || 0;
466
- const completionTokens = data.usage.completion_tokens || 0;
467
- log.info(SYMBOLS.INFO, `Total Tokens: ${totalTokens} (${promptTokens} + ${completionTokens})`);
537
+
538
+ return {
539
+ suggestion,
540
+ creditsUsed,
541
+ creditsRemaining
542
+ };
468
543
  }
469
544
 
470
- return {
471
- suggestion,
472
- creditsUsed,
473
- creditsRemaining
474
- };
475
- }
476
-
477
- if (debugMode) {
478
- log.debug(SYMBOLS.WARNING, '[MAIASS DEBUG] No valid AI response received - no choices in response data');
545
+ if (debugMode) {
546
+ log.debug(SYMBOLS.WARNING, '[MAIASS DEBUG] No valid AI response received - no choices in response data');
547
+ }
548
+ return null;
549
+ } catch (error) {
550
+ // Stop spinner on error
551
+ if (spinner) spinner.stop(false);
552
+
553
+ if (error.name === 'AbortError') {
554
+ log.error(SYMBOLS.WARNING, `AI request timed out after ${timeout/1000} seconds`);
555
+ log.info(SYMBOLS.INFO, 'The AI service might be slow or unresponsive. Try again or increase MAIASS_AI_TIMEOUT.');
556
+ return null;
557
+ }
558
+
559
+ if (debugMode) {
560
+ log.debug(SYMBOLS.WARNING, `[MAIASS DEBUG] AI suggestion error details: ${error.stack || error.message}`);
561
+ }
562
+ log.error(SYMBOLS.WARNING, `AI suggestion failed: ${error.message}`);
563
+ return null;
479
564
  }
480
- return null;
481
565
  } catch (error) {
482
566
  if (debugMode) {
483
567
  log.debug(SYMBOLS.WARNING, `[MAIASS DEBUG] AI suggestion error details: ${error.stack || error.message}`);
@@ -18,6 +18,7 @@ export const MAIASS_VARIABLES = {
18
18
  'MAIASS_AI_MODEL': { default: 'gpt-3.5-turbo', description: 'AI model to use' },
19
19
  'MAIASS_AI_TEMPERATURE': { default: '0.7', description: 'AI temperature setting' },
20
20
  'MAIASS_AI_MAX_CHARACTERS': { default: '8000', description: 'Max characters for AI requests' },
21
+ 'MAIASS_AI_TIMEOUT': { default: '30', description: 'AI request timeout in seconds' },
21
22
  'MAIASS_AI_COMMIT_MESSAGE_STYLE': { default: 'bullet', description: 'Commit message style' },
22
23
 
23
24
  // Version file system
@@ -516,21 +516,96 @@ function updateWordPressVersions(newVersion, projectPath = process.cwd()) {
516
516
  }
517
517
 
518
518
  /**
519
- * Detect version files in the current directory
519
+ * Extract version from a file using type and line-start pattern (matches bashmaiass behavior)
520
+ * @param {string} content - File content
521
+ * @param {string} type - File type: 'json', 'txt', 'text', 'php', 'pattern'
522
+ * @param {string} lineStart - Line start pattern for txt type
523
+ * @returns {string|null} Extracted version or null
524
+ */
525
+ function extractVersionByType(content, type, lineStart) {
526
+ if (type === 'json') {
527
+ return VERSION_FILE_TYPES.json.extract(content);
528
+ }
529
+
530
+ if (type === 'txt' || type === 'text') {
531
+ if (lineStart) {
532
+ for (const line of content.split('\n')) {
533
+ if (line.trim().startsWith(lineStart)) {
534
+ const match = line.match(/(\d+\.\d+\.\d+)/);
535
+ if (match) return match[1];
536
+ }
537
+ }
538
+ return null;
539
+ }
540
+ return VERSION_FILE_TYPES.text.extract(content);
541
+ }
542
+
543
+ if (type === 'php' || type === 'pattern') {
544
+ return VERSION_FILE_TYPES.php.extract(content);
545
+ }
546
+
547
+ // Unknown type - try generic version extraction
548
+ const match = content.match(/(\d+\.\d+\.\d+)/);
549
+ return match ? match[1] : null;
550
+ }
551
+
552
+ /**
553
+ * Detect version files in the current directory.
554
+ * Checks MAIASS_VERSION_PRIMARY_FILE env var first (from .env.maiass),
555
+ * then falls back to scanning common version file patterns.
520
556
  * @param {string} projectPath - Path to project directory
521
557
  * @returns {Array} Array of detected version files
522
558
  */
523
559
  export function detectVersionFiles(projectPath = process.cwd()) {
524
560
  const versionFiles = [];
525
561
 
526
- // Common version file patterns to check
562
+ // Check for custom primary version file from .env.maiass first
563
+ const primaryFile = process.env.MAIASS_VERSION_PRIMARY_FILE;
564
+ const primaryType = process.env.MAIASS_VERSION_PRIMARY_TYPE || 'txt';
565
+ const primaryLineStart = process.env.MAIASS_VERSION_PRIMARY_LINE_START || '';
566
+
567
+ if (primaryFile) {
568
+ logger.debug(`Custom primary version file configured: ${primaryFile} (type: ${primaryType})`);
569
+ const filePath = path.isAbsolute(primaryFile)
570
+ ? primaryFile
571
+ : path.join(projectPath, primaryFile);
572
+
573
+ if (fs.existsSync(filePath)) {
574
+ try {
575
+ const content = fs.readFileSync(filePath, 'utf8');
576
+ const version = extractVersionByType(content, primaryType, primaryLineStart);
577
+
578
+ if (version) {
579
+ logger.debug(`Found version ${version} in custom primary file: ${primaryFile}`);
580
+ versionFiles.push({
581
+ path: filePath,
582
+ filename: path.basename(filePath),
583
+ type: primaryType,
584
+ currentVersion: version,
585
+ content,
586
+ isPrimary: true,
587
+ lineStart: primaryLineStart
588
+ });
589
+ return versionFiles;
590
+ } else {
591
+ logger.warning(SYMBOLS.WARNING, `Could not extract version from custom primary file: ${primaryFile}`);
592
+ }
593
+ } catch (error) {
594
+ logger.error(SYMBOLS.CROSS, `Error reading custom primary version file ${primaryFile}: ${error.message}`);
595
+ }
596
+ } else {
597
+ logger.warning(SYMBOLS.WARNING, `Custom primary version file not found: ${filePath}`);
598
+ }
599
+ }
600
+
601
+ // Fallback: scan common version file patterns in project root
527
602
  const filesToCheck = [
528
603
  'package.json',
529
604
  'composer.json',
530
605
  'VERSION',
531
606
  'version.txt',
532
- 'style.css', // WordPress themes
533
- 'plugin.php', // WordPress plugins
607
+ 'style.css',
608
+ 'plugin.php',
534
609
  'functions.php'
535
610
  ];
536
611
 
@@ -542,7 +617,6 @@ export function detectVersionFiles(projectPath = process.cwd()) {
542
617
  const content = fs.readFileSync(filePath, 'utf8');
543
618
  const ext = path.extname(filename);
544
619
 
545
- // Determine file type and check if it contains version info
546
620
  for (const [typeName, typeConfig] of Object.entries(VERSION_FILE_TYPES)) {
547
621
  if (typeConfig.extensions.includes(ext) || typeConfig.extensions.includes('')) {
548
622
  if (typeConfig.detect(content, filename)) {
@@ -555,13 +629,12 @@ export function detectVersionFiles(projectPath = process.cwd()) {
555
629
  currentVersion: version,
556
630
  content
557
631
  });
558
- break; // Found matching type, move to next file
632
+ break;
559
633
  }
560
634
  }
561
635
  }
562
636
  }
563
637
  } catch (error) {
564
- // Skip files that can't be read
565
638
  continue;
566
639
  }
567
640
  }
@@ -603,18 +676,24 @@ export async function getCurrentVersion(projectPath = process.cwd()) {
603
676
  let primaryVersion = null;
604
677
  let primarySource = null;
605
678
 
606
- // Prioritize package.json if it exists
607
- const packageJson = versionFiles.find(f => f.filename === 'package.json');
608
- if (packageJson) {
609
- primaryVersion = packageJson.currentVersion;
610
- primarySource = 'package.json';
611
- } else if (versionFiles.length > 0) {
612
- // Use first detected version file
613
- primaryVersion = versionFiles[0].currentVersion;
614
- primarySource = versionFiles[0].filename;
615
- } else if (tagVersion) {
616
- primaryVersion = tagVersion;
617
- primarySource = 'git tags';
679
+ // Prioritize custom primary file from .env.maiass if present
680
+ const customPrimary = versionFiles.find(f => f.isPrimary);
681
+ if (customPrimary) {
682
+ primaryVersion = customPrimary.currentVersion;
683
+ primarySource = process.env.MAIASS_VERSION_PRIMARY_FILE || customPrimary.filename;
684
+ } else {
685
+ // Fallback: prioritize package.json
686
+ const packageJson = versionFiles.find(f => f.filename === 'package.json');
687
+ if (packageJson) {
688
+ primaryVersion = packageJson.currentVersion;
689
+ primarySource = 'package.json';
690
+ } else if (versionFiles.length > 0) {
691
+ primaryVersion = versionFiles[0].currentVersion;
692
+ primarySource = versionFiles[0].filename;
693
+ } else if (tagVersion) {
694
+ primaryVersion = tagVersion;
695
+ primarySource = 'git tags';
696
+ }
618
697
  }
619
698
 
620
699
  return {
@@ -765,8 +844,27 @@ export async function updateVersionFiles(newVersion, versionFiles, dryRun = fals
765
844
  // Update primary version files
766
845
  for (const file of versionFiles) {
767
846
  try {
768
- const typeConfig = VERSION_FILE_TYPES[file.type];
769
- const updatedContent = typeConfig.update(file.content, newVersion);
847
+ let updatedContent = null;
848
+
849
+ // Custom primary file with lineStart pattern needs special handling
850
+ if (file.isPrimary && file.lineStart && (file.type === 'txt' || file.type === 'text')) {
851
+ const lines = file.content.split('\n');
852
+ for (let i = 0; i < lines.length; i++) {
853
+ if (lines[i].trim().startsWith(file.lineStart)) {
854
+ lines[i] = lines[i].replace(/\d+\.\d+\.\d+/, newVersion);
855
+ }
856
+ }
857
+ updatedContent = lines.join('\n');
858
+ } else {
859
+ // Map env types to VERSION_FILE_TYPES keys
860
+ const typeKey = (file.type === 'txt' || file.type === 'text') ? 'text'
861
+ : (file.type === 'pattern') ? 'php'
862
+ : file.type;
863
+ const typeConfig = VERSION_FILE_TYPES[typeKey];
864
+ if (typeConfig) {
865
+ updatedContent = typeConfig.update(file.content, newVersion);
866
+ }
867
+ }
770
868
 
771
869
  if (!updatedContent) {
772
870
  results.failed.push({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "maiass",
3
3
  "type": "module",
4
- "version": "5.9.6",
4
+ "version": "5.9.9",
5
5
  "description": "MAIASS - Modular AI-Augmented Semantic Scribe - Intelligent Git workflow automation",
6
6
  "main": "maiass.mjs",
7
7
  "bin": {