agentvibes 2.17.8 → 3.0.0

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/src/installer.js CHANGED
@@ -335,6 +335,7 @@ async function collectConfiguration(options = {}) {
335
335
  const config = {
336
336
  provider: null,
337
337
  piperPath: null,
338
+ sshHost: null,
338
339
  defaultVoice: null,
339
340
  reverb: 'light',
340
341
  backgroundMusic: {
@@ -376,83 +377,75 @@ async function collectConfiguration(options = {}) {
376
377
  } else if (currentPage === 1) {
377
378
  // Page 2: TTS Provider & Voice Storage
378
379
 
379
- // On non-macOS platforms, only Piper is available - auto-select it
380
- if (process.platform !== 'darwin') {
381
- console.log(boxen(
382
- chalk.white('Text-to-Speech (TTS) converts Claude\'s text responses into spoken audio.\n\n') +
383
- chalk.white('Your TTS Provider:\n\n') +
384
- chalk.green('šŸ†“ Piper TTS (Free, Offline)\n') +
385
- chalk.gray(' • 50+ Hugging Face AI voices\n') +
386
- chalk.gray(' • Human-like speech quality\n') +
387
- chalk.gray(' • No API key required\n\n') +
388
- chalk.dim('(Automatically selected - only option for Linux/WSL)'),
389
- {
390
- padding: 1,
391
- margin: { top: 0, bottom: 0, left: 0, right: 0 },
392
- borderStyle: 'round',
393
- borderColor: 'gray',
394
- width: 80
395
- }
396
- ));
380
+ // Show provider selection with all available options
381
+ const isMacOS = process.platform === 'darwin';
397
382
 
398
- config.provider = 'piper';
383
+ console.log(boxen(
384
+ chalk.white('Text-to-Speech (TTS) converts Claude\'s text responses into spoken audio.\n\n') +
385
+ chalk.white('Choose your Text-to-Speech provider.\n\n') +
386
+ (isMacOS ? chalk.yellow('šŸŽ macOS Say\n') +
387
+ chalk.gray(' • Built-in to macOS\n') +
388
+ chalk.gray(' • Zero setup required\n') +
389
+ chalk.gray(' • 40+ system voices\n\n') : '') +
390
+ chalk.green('šŸ†“ Piper TTS\n') +
391
+ chalk.gray(' • Free & offline\n') +
392
+ chalk.gray(' • 50+ Hugging Face AI voices\n') +
393
+ chalk.gray(' • Human-like speech quality\n\n') +
394
+ chalk.blue('šŸ“± Termux SSH\n') +
395
+ chalk.gray(' • Only use if project is on a Remote Server\n') +
396
+ chalk.gray(' • Pushes audio to your Android Device via SSH\n') +
397
+ chalk.gray(' • Native Android TTS\n') +
398
+ chalk.gray(' • See: github.com/paulpreibisch/AgentVibes/blob/master/.claude/docs/TERMUX_SETUP.md'),
399
+ {
400
+ padding: 1,
401
+ margin: { top: 0, bottom: 0, left: 0, right: 0 },
402
+ borderStyle: 'round',
403
+ borderColor: 'gray',
404
+ width: 80
405
+ }
406
+ ));
399
407
 
400
- // No confirmation needed - just auto-continue
401
- } else {
402
- // macOS - show choice between macOS Say and Piper
403
- console.log(boxen(
404
- chalk.white('Text-to-Speech (TTS) converts Claude\'s text responses into spoken audio.\n\n') +
405
- chalk.white('Choose your Text-to-Speech provider.\n\n') +
406
- chalk.yellow('šŸŽ macOS Say\n') +
407
- chalk.gray(' • Built-in to macOS\n') +
408
- chalk.gray(' • Zero setup required\n') +
409
- chalk.gray(' • 40+ system voices\n\n') +
410
- chalk.green('šŸ†“ Piper TTS\n') +
411
- chalk.gray(' • Free & offline\n') +
412
- chalk.gray(' • 50+ Hugging Face AI voices\n') +
413
- chalk.gray(' • Human-like speech quality'),
414
- {
415
- padding: 1,
416
- margin: { top: 0, bottom: 0, left: 0, right: 0 },
417
- borderStyle: 'round',
418
- borderColor: 'gray',
419
- width: 80
420
- }
421
- ));
408
+ // Provider selection
409
+ const providerChoices = [];
422
410
 
423
- // Provider selection
424
- const providerChoices = [
425
- {
426
- name: chalk.yellow('šŸŽ macOS Say (Recommended)'),
427
- value: 'macos'
428
- },
429
- {
430
- name: chalk.green('šŸ†“ Piper TTS (Free, Offline)'),
431
- value: 'piper'
432
- },
433
- new inquirer.Separator(),
434
- {
435
- name: chalk.magentaBright('← Back to Welcome'),
436
- value: '__back__'
437
- }
438
- ];
411
+ if (isMacOS) {
412
+ providerChoices.push({
413
+ name: chalk.yellow('šŸŽ macOS Say (Recommended)'),
414
+ value: 'macos'
415
+ });
416
+ }
439
417
 
440
- const { provider } = await inquirer.prompt([{
441
- type: 'list',
442
- name: 'provider',
443
- message: chalk.yellow('Select TTS provider:'),
444
- choices: providerChoices,
445
- default: config.provider || 'macos'
446
- }]);
418
+ providerChoices.push({
419
+ name: chalk.green('šŸ†“ Piper TTS (Free, Offline)'),
420
+ value: 'piper'
421
+ });
447
422
 
448
- // Check if user wants to go back
449
- if (provider === '__back__') {
450
- return null;
451
- }
423
+ providerChoices.push({
424
+ name: chalk.blue('šŸ“± Termux SSH (Android)') + chalk.gray(' - Only choose if your project is on a remote server and you want audio sent to your Android device. See: github.com/paulpreibisch/AgentVibes/blob/master/.claude/docs/TERMUX_SETUP.md'),
425
+ value: 'termux-ssh'
426
+ });
427
+
428
+ providerChoices.push(new inquirer.Separator());
429
+ providerChoices.push({
430
+ name: chalk.magentaBright('← Back to Welcome'),
431
+ value: '__back__'
432
+ });
433
+
434
+ const { provider } = await inquirer.prompt([{
435
+ type: 'list',
436
+ name: 'provider',
437
+ message: chalk.yellow('Select TTS provider:'),
438
+ choices: providerChoices,
439
+ default: config.provider || (isMacOS ? 'macos' : 'piper')
440
+ }]);
452
441
 
453
- config.provider = provider;
442
+ // Check if user wants to go back
443
+ if (provider === '__back__') {
444
+ return null;
454
445
  }
455
446
 
447
+ config.provider = provider;
448
+
456
449
  // If Piper selected, ask for voice storage location
457
450
  if (config.provider === 'piper') {
458
451
  const homeDir = process.env.HOME || process.env.USERPROFILE;
@@ -494,6 +487,59 @@ async function collectConfiguration(options = {}) {
494
487
  }
495
488
  }
496
489
 
490
+ // If Termux SSH selected, ask for SSH host alias
491
+ if (config.provider === 'termux-ssh') {
492
+ console.log('\n' + boxen(
493
+ chalk.white('Termux SSH requires an SSH host alias configured in ~/.ssh/config\n') +
494
+ chalk.white('Example: "android" pointing to your Android device\n\n') +
495
+ chalk.cyan('šŸ“– Documentation:\n') +
496
+ chalk.gray(' github.com/paulpreibisch/AgentVibes/blob/master/.claude/docs/TERMUX_SETUP.md\n') +
497
+ chalk.gray(' After install: .claude/docs/TERMUX_SETUP.md\n\n') +
498
+ chalk.cyan('šŸ”— Required Components:\n') +
499
+ chalk.gray(' • Tailscale VPN: ') + chalk.blue('https://tailscale.com/download/android\n') +
500
+ chalk.gray(' • F-Droid Store: ') + chalk.blue('https://f-droid.org\n') +
501
+ chalk.gray(' • Termux App: ') + chalk.blue('https://f-droid.org/packages/com.termux\n') +
502
+ chalk.gray(' • Termux:API: ') + chalk.blue('https://f-droid.org/packages/com.termux.api'),
503
+ {
504
+ padding: 1,
505
+ margin: { top: 0, bottom: 0, left: 0, right: 0 },
506
+ borderStyle: 'round',
507
+ borderColor: 'blue',
508
+ width: 80
509
+ }
510
+ ));
511
+
512
+ const { configureNow } = await inquirer.prompt([{
513
+ type: 'confirm',
514
+ name: 'configureNow',
515
+ message: chalk.yellow('Configure SSH host alias now?'),
516
+ default: false
517
+ }]);
518
+
519
+ if (configureNow) {
520
+ const { sshHost } = await inquirer.prompt([{
521
+ type: 'input',
522
+ name: 'sshHost',
523
+ message: chalk.yellow('Enter your SSH host alias (e.g., "android"):'),
524
+ validate: (input) => {
525
+ if (!input || input.trim() === '') {
526
+ return 'Please provide a valid SSH host alias';
527
+ }
528
+ // Security: Basic validation - no spaces, no special chars that could cause issues
529
+ if (!/^[a-zA-Z0-9_-]+$/.test(input.trim())) {
530
+ return 'SSH host alias should only contain letters, numbers, dashes, and underscores';
531
+ }
532
+ return true;
533
+ }
534
+ }]);
535
+
536
+ config.sshHost = sshHost.trim();
537
+ } else {
538
+ console.log(chalk.yellow('\nāš ļø SSH host not configured - you can set it later:'));
539
+ console.log(chalk.gray(' echo "your-host-alias" > ~/.claude/termux-ssh-host.txt\n'));
540
+ }
541
+ }
542
+
497
543
  } else if (currentPage === 2) {
498
544
  // Page 3: Voice Selection
499
545
  console.log(boxen(
@@ -588,10 +634,42 @@ async function collectConfiguration(options = {}) {
588
634
  currentPage++;
589
635
  continue;
590
636
  }
637
+
638
+ } else if (config.provider === 'termux-ssh') {
639
+ // Termux SSH - voices are managed on Android device
640
+ console.log(boxen(
641
+ chalk.white('Android TTS voices are managed on your Android device.\n\n') +
642
+ chalk.gray('To configure voices:\n') +
643
+ chalk.gray(' 1. Open Android ') + chalk.cyan('Settings → Accessibility → Text-to-Speech\n') +
644
+ chalk.gray(' 2. Install voice engines from Play Store (e.g., Google TTS)\n') +
645
+ chalk.gray(' 3. Select your preferred engine and voice\n\n') +
646
+ chalk.yellow('AgentVibes will use your Android\'s selected TTS voice automatically.'),
647
+ {
648
+ padding: 1,
649
+ margin: { top: 0, bottom: 0, left: 0, right: 0 },
650
+ borderStyle: 'round',
651
+ borderColor: 'blue',
652
+ width: 80
653
+ }
654
+ ));
655
+
656
+ console.log(chalk.green('\nāœ“ Android TTS will use device-configured voice\n'));
657
+
658
+ // Auto-advance to next page
659
+ currentPage++;
660
+ continue;
591
661
  }
592
662
 
593
663
  } else if (currentPage === 3) {
594
664
  // Page 4: Audio Settings (Reverb + Background Music)
665
+ // Skip for termux-ssh - audio effects/background music don't work with SSH text-only TTS
666
+ if (config.provider === 'termux-ssh') {
667
+ console.log(chalk.yellow('⊘ Audio effects and background music are not available for Termux SSH provider'));
668
+ console.log(chalk.gray(' (Audio plays on Android device, not locally)\n'));
669
+ currentPage++;
670
+ continue;
671
+ }
672
+
595
673
  console.log(boxen(
596
674
  chalk.white('Configure audio effects and background music for your Agents.\n\n') +
597
675
  chalk.yellow('Reverb:\n') +
@@ -823,17 +901,19 @@ function showWelcome() {
823
901
  * Shown during install and update commands
824
902
  */
825
903
  function getReleaseInfoBoxen() {
826
- return chalk.cyan.bold('šŸ“¦ AgentVibes v2.17.8 - Repository Cleanup\n\n') +
904
+ return chalk.cyan.bold('šŸ“¦ AgentVibes v3.0.0 - Cross-Platform Remote Audio\n\n') +
827
905
  chalk.green.bold('šŸŽ™ļø WHAT\'S NEW:\n\n') +
828
- chalk.cyan('AgentVibes v2.17.8 is a maintenance release focusing on repository organization\n') +
829
- chalk.cyan('and cleanup. This release removes 12 outdated files including old release notes\n') +
830
- chalk.cyan('from versions 2.4.0 through 2.16.0, legacy setup scripts, and temporary documentation.\n') +
831
- chalk.cyan('The cleanup reduces repository size by over 3,000 lines while preserving all functionality.\n\n') +
906
+ chalk.cyan('AgentVibes v3.0.0 introduces the termux-ssh TTS provider, enabling true mobile-first\n') +
907
+ chalk.cyan('interactive conversations with Claude Code. Route TTS audio to your Android device via\n') +
908
+ chalk.cyan('SSH—whether coding locally on your laptop or on remote servers—and have hands-free,\n') +
909
+ chalk.cyan('voice-driven conversations with Claude using just your phone. Transforms how developers\n') +
910
+ chalk.cyan('interact with AI assistants.\n\n') +
832
911
  chalk.green.bold('✨ KEY HIGHLIGHTS:\n\n') +
833
- chalk.gray(' 🧹 Repository Cleanup - Removed 8 outdated release notes files (v2.4.0-v2.16.0)\n') +
834
- chalk.gray(' šŸ“ Documentation Consolidation - All release history in single RELEASE_NOTES.md\n') +
835
- chalk.gray(' šŸ—‘ļø Legacy Script Removal - Cleaned up obsolete VS Code color and RDP audio scripts\n') +
836
- chalk.gray(' āœ… Test Coverage - Added installer page flow test, all 236 tests passing\n\n') +
912
+ chalk.gray(' šŸ“± Mobile-First AI Conversations - Fully interactive conversations with Claude Code\n') +
913
+ chalk.gray(' šŸ’» Local + Remote Development - Works for local coding and remote server development\n') +
914
+ chalk.gray(' 🌐 Tailscale Integration - Internet-wide access without port forwarding\n') +
915
+ chalk.gray(' šŸŽ„ Demo Video - Watch it in action: https://youtu.be/ngLiA_KQtTA\n') +
916
+ chalk.gray(' šŸŽÆ Full MCP Compatibility - Complete integration with all MCP workflows\n\n') +
837
917
  chalk.gray('šŸ“– Full Release Notes: RELEASE_NOTES.md\n') +
838
918
  chalk.gray('🌐 Website: https://agentvibes.org\n') +
839
919
  chalk.gray('šŸ“¦ Repository: https://github.com/paulpreibisch/AgentVibes\n\n') +
@@ -857,15 +937,15 @@ async function playWelcomeDemo(targetDir, spinner, options = {}) {
857
937
  let audioPlayer = null;
858
938
 
859
939
  try {
860
- execSync('which paplay 2>/dev/null', { stdio: 'pipe' }); // NOSONAR - Safe: checking system PATH for audio player
940
+ execFileSync('which', ['paplay'], { stdio: 'pipe' });
861
941
  audioPlayer = 'paplay';
862
942
  } catch {
863
943
  try {
864
- execSync('which afplay 2>/dev/null', { stdio: 'pipe' }); // NOSONAR - Safe: checking system PATH for audio player
944
+ execFileSync('which', ['afplay'], { stdio: 'pipe' });
865
945
  audioPlayer = 'afplay';
866
946
  } catch {
867
947
  try {
868
- execSync('which mpv 2>/dev/null', { stdio: 'pipe' }); // NOSONAR - Safe: checking system PATH for audio player
948
+ execFileSync('which', ['mpv'], { stdio: 'pipe' });
869
949
  audioPlayer = 'mpv';
870
950
  } catch {}
871
951
  }
@@ -1116,9 +1196,9 @@ Without the \`.bmad-agent-context\` file:
1116
1196
  // ============================================================================
1117
1197
 
1118
1198
  /**
1119
- * Prompt user to select TTS provider (Piper or macOS Say)
1199
+ * Prompt user to select TTS provider (Piper, macOS Say, or Termux SSH)
1120
1200
  * @param {Object} options - Installation options
1121
- * @returns {Promise<string>} Selected provider ('piper' or 'macos')
1201
+ * @returns {Promise<string>} Selected provider ('piper', 'macos', or 'termux-ssh')
1122
1202
  */
1123
1203
  async function promptProviderSelection(options) {
1124
1204
  const isMacOS = process.platform === 'darwin';
@@ -1133,41 +1213,30 @@ async function promptProviderSelection(options) {
1133
1213
  return 'piper';
1134
1214
  }
1135
1215
 
1136
- // Auto-select if only one provider available
1137
- if (!isMacOS) {
1138
- // On Linux/WSL, only Piper is available - auto-select it
1139
- console.log(boxen(
1140
- chalk.bold('šŸŽ¤ TTS Provider\n\n') +
1141
- chalk.green('āœ“ Piper TTS (Free, Offline)\n') +
1142
- chalk.gray(' 50+ Hugging Face AI voices\n') +
1143
- chalk.gray(' Human-like speech quality\n') +
1144
- chalk.gray(' No API key required'),
1145
- {
1146
- padding: 1,
1147
- margin: 1,
1148
- borderStyle: 'round',
1149
- borderColor: 'green',
1150
- title: chalk.bold('TTS Provider Auto-detected'),
1151
- titleAlignment: 'center'
1152
- }
1153
- ));
1154
- console.log('');
1155
- return 'piper';
1156
- }
1157
-
1158
- // On macOS, both providers available - prompt user
1216
+ // Always show all providers - let user choose
1159
1217
  console.log(chalk.cyan('šŸŽ­ Choose Your TTS Provider:\n'));
1160
1218
 
1161
- const choices = [
1162
- {
1219
+ const choices = [];
1220
+
1221
+ // macOS Say (only on macOS)
1222
+ if (isMacOS) {
1223
+ choices.push({
1163
1224
  name: chalk.yellow('šŸŽ macOS Say (Recommended)') + chalk.gray(' - Built-in, zero setup required'),
1164
1225
  value: 'macos',
1165
- },
1166
- {
1167
- name: chalk.green('šŸ†“ Piper TTS (Free, Offline)') + chalk.gray(' - 50+ Hugging Face AI voices, human-like speech'),
1168
- value: 'piper',
1169
- }
1170
- ];
1226
+ });
1227
+ }
1228
+
1229
+ // Piper TTS (all platforms)
1230
+ choices.push({
1231
+ name: chalk.green('šŸ†“ Piper TTS (Free, Offline)') + chalk.gray(' - 50+ Hugging Face AI voices, human-like speech'),
1232
+ value: 'piper',
1233
+ });
1234
+
1235
+ // Termux SSH (all platforms)
1236
+ choices.push({
1237
+ name: chalk.blue('šŸ“± Termux SSH (Android)') + chalk.gray(' - Only choose if your project is on a remote server and you want audio sent to your Android device. See: github.com/paulpreibisch/AgentVibes/blob/master/.claude/docs/TERMUX_SETUP.md'),
1238
+ value: 'termux-ssh',
1239
+ });
1171
1240
 
1172
1241
  const { provider } = await inquirer.prompt([
1173
1242
  {
@@ -1175,7 +1244,7 @@ async function promptProviderSelection(options) {
1175
1244
  name: 'provider',
1176
1245
  message: 'Which TTS provider would you like to use?',
1177
1246
  choices,
1178
- default: 'macos',
1247
+ default: isMacOS ? 'macos' : 'piper',
1179
1248
  },
1180
1249
  ]);
1181
1250
 
@@ -1271,6 +1340,58 @@ async function handlePiperConfiguration() {
1271
1340
  return piperPath;
1272
1341
  }
1273
1342
 
1343
+ /**
1344
+ * Handle Termux SSH configuration (SSH host alias setup)
1345
+ * @returns {Promise<string|null>} SSH host alias or null if user skips
1346
+ */
1347
+ async function handleTermuxSshConfiguration() {
1348
+ console.log(chalk.cyan('\nšŸ“± Termux SSH Configuration:\n'));
1349
+ console.log(chalk.gray(' Termux SSH requires an SSH host alias configured in ~/.ssh/config'));
1350
+ console.log(chalk.gray(' Example: "android" pointing to your Android device\n'));
1351
+ console.log(chalk.gray(' See documentation: .claude/docs/TERMUX_SETUP.md\n'));
1352
+ console.log(chalk.cyan(' šŸ”— Required Components:\n'));
1353
+ console.log(chalk.gray(' • Tailscale VPN: ') + chalk.blue('https://tailscale.com/download/android'));
1354
+ console.log(chalk.gray(' • F-Droid Store: ') + chalk.blue('https://f-droid.org'));
1355
+ console.log(chalk.gray(' • Termux App: ') + chalk.blue('https://f-droid.org/packages/com.termux'));
1356
+ console.log(chalk.gray(' • Termux:API: ') + chalk.blue('https://f-droid.org/packages/com.termux.api\n'));
1357
+
1358
+ const { configureNow } = await inquirer.prompt([
1359
+ {
1360
+ type: 'confirm',
1361
+ name: 'configureNow',
1362
+ message: 'Do you want to configure the SSH host alias now?',
1363
+ default: false,
1364
+ },
1365
+ ]);
1366
+
1367
+ if (!configureNow) {
1368
+ console.log(chalk.yellow('āš ļø SSH host not configured - you can set it later:'));
1369
+ console.log(chalk.gray(' echo "your-host-alias" > ~/.claude/termux-ssh-host.txt\n'));
1370
+ return null;
1371
+ }
1372
+
1373
+ const { sshHost } = await inquirer.prompt([
1374
+ {
1375
+ type: 'input',
1376
+ name: 'sshHost',
1377
+ message: 'Enter your SSH host alias (e.g., "android"):',
1378
+ validate: (input) => {
1379
+ if (!input || input.trim() === '') {
1380
+ return 'Please provide a valid SSH host alias';
1381
+ }
1382
+ // Basic validation: no spaces, no special chars that could cause issues
1383
+ if (!/^[a-zA-Z0-9_-]+$/.test(input.trim())) {
1384
+ return 'SSH host alias should only contain letters, numbers, dashes, and underscores';
1385
+ }
1386
+ return true;
1387
+ },
1388
+ },
1389
+ ]);
1390
+
1391
+ const sshHostTrimmed = sshHost.trim();
1392
+ console.log(chalk.green(`āœ“ SSH host alias set to: ${sshHostTrimmed}`));
1393
+ return sshHostTrimmed;
1394
+ }
1274
1395
 
1275
1396
  /**
1276
1397
  * Copy command files to target directory
@@ -2546,6 +2667,15 @@ async function handleBmadIntegration(targetDir, options = {}) {
2546
2667
  */
2547
2668
  async function showRecentChanges(sourceDir) {
2548
2669
  try {
2670
+ // Check if sourceDir actually has a .git directory
2671
+ const gitDir = path.join(sourceDir, '.git');
2672
+ const gitDirExists = await fs.access(gitDir).then(() => true).catch(() => false);
2673
+
2674
+ if (!gitDirExists) {
2675
+ // No .git directory - skip git log to avoid showing parent repo's commits
2676
+ throw new Error('No .git directory in package - using release notes');
2677
+ }
2678
+
2549
2679
  const { execSync } = await import('node:child_process');
2550
2680
  const gitLog = execSync( // NOSONAR - Safe: fixed command with controlled cwd, no user input
2551
2681
  'git log --oneline --no-decorate -5',
@@ -2827,7 +2957,7 @@ async function install(options = {}) {
2827
2957
  const preInstallPages = [];
2828
2958
 
2829
2959
  // Page 1: Configuration Summary
2830
- const providerLabels = { piper: 'Piper TTS', macos: 'macOS Say' };
2960
+ const providerLabels = { piper: 'Piper TTS', macos: 'macOS Say', 'termux-ssh': 'Termux SSH (Android)' };
2831
2961
  const reverbLabels = {
2832
2962
  off: 'Off',
2833
2963
  light: 'Light',
@@ -2847,6 +2977,13 @@ async function install(options = {}) {
2847
2977
  if (selectedProvider === 'piper' && piperVoicesPath) {
2848
2978
  configContent += chalk.gray(` Voice storage: ${piperVoicesPath}\n`);
2849
2979
  }
2980
+ if (selectedProvider === 'termux-ssh') {
2981
+ if (userConfig.sshHost) {
2982
+ configContent += chalk.gray(` SSH host: ${userConfig.sshHost}\n`);
2983
+ } else {
2984
+ configContent += chalk.yellow(` SSH host: Not configured (set later)\n`);
2985
+ }
2986
+ }
2850
2987
  configContent += '\n';
2851
2988
  configContent += chalk.cyan('šŸŽ›ļø Audio Settings:\n');
2852
2989
  configContent += chalk.white(` Reverb: ${reverbLabels[userConfig.reverb]}\n`);
@@ -2923,11 +3060,15 @@ async function install(options = {}) {
2923
3060
  console.log(header);
2924
3061
  console.log(configBoxen);
2925
3062
  console.log('');
2926
- console.log(chalk.gray('Play audio welcome message from Paul, creator of AgentVibes.\n'));
3063
+ // Don't show welcome message text for termux-ssh (it won't work)
3064
+ if (userConfig.provider !== 'termux-ssh') {
3065
+ console.log(chalk.gray('Play audio welcome message from Paul, creator of AgentVibes.\n'));
3066
+ }
2927
3067
  }
2928
3068
 
2929
3069
  // Ask welcome message question BEFORE showing navigation
2930
- if (!options.yes) {
3070
+ // Skip for termux-ssh - welcome audio plays locally, not on Android device
3071
+ if (!options.yes && userConfig.provider !== 'termux-ssh') {
2931
3072
  // Ask if user wants to hear welcome message
2932
3073
  const { playWelcome } = await inquirer.prompt([
2933
3074
  {
@@ -2945,21 +3086,23 @@ async function install(options = {}) {
2945
3086
  spinner.succeed(chalk.green('Welcome message complete!'));
2946
3087
  console.log(''); // Spacing after completion
2947
3088
  }
3089
+ } else if (!options.yes && userConfig.provider === 'termux-ssh') {
3090
+ console.log(chalk.yellow('⊘ Welcome message skipped (not available for Termux SSH)\n'));
3091
+ }
2948
3092
 
2949
- // Now show navigation menu (Continue to installation)
2950
- const { startInstall } = await inquirer.prompt([
2951
- {
2952
- type: 'confirm',
2953
- name: 'startInstall',
2954
- message: chalk.yellow('āœ… Start Installation?'),
2955
- default: true,
2956
- },
2957
- ]);
3093
+ // Now show navigation menu (Continue to installation)
3094
+ const { startInstall } = await inquirer.prompt([
3095
+ {
3096
+ type: 'confirm',
3097
+ name: 'startInstall',
3098
+ message: chalk.yellow('āœ… Start Installation?'),
3099
+ default: true,
3100
+ },
3101
+ ]);
2958
3102
 
2959
- if (!startInstall) {
2960
- console.log(chalk.red('\nāŒ Installation cancelled.\n'));
2961
- process.exit(0);
2962
- }
3103
+ if (!startInstall) {
3104
+ console.log(chalk.red('\nāŒ Installation cancelled.\n'));
3105
+ process.exit(0);
2963
3106
  }
2964
3107
 
2965
3108
  // User already confirmed by pressing "Start Installation", so no need for another confirmation
@@ -3023,6 +3166,11 @@ async function install(options = {}) {
3023
3166
  await fs.writeFile(piperConfigPath, piperVoicesPath);
3024
3167
  }
3025
3168
 
3169
+ if (selectedProvider === 'termux-ssh' && userConfig.sshHost) {
3170
+ const sshHostConfigPath = path.join(claudeDir, 'termux-ssh-host.txt');
3171
+ await fs.writeFile(sshHostConfigPath, userConfig.sshHost);
3172
+ }
3173
+
3026
3174
  // Set default voice based on user selection or provider defaults
3027
3175
  const voiceConfigPath = path.join(claudeDir, 'tts-voice.txt');
3028
3176
  let defaultVoice = userConfig.defaultVoice;
@@ -3034,6 +3182,12 @@ async function install(options = {}) {
3034
3182
  defaultVoice = 'en_US-ryan-high';
3035
3183
  break;
3036
3184
  case 'macos':
3185
+ defaultVoice = 'Samantha';
3186
+ break;
3187
+ case 'termux-ssh':
3188
+ // Android TTS voices are managed in Android settings, not here
3189
+ defaultVoice = 'android-system-default';
3190
+ break;
3037
3191
  default:
3038
3192
  defaultVoice = 'Samantha';
3039
3193
  break;
@@ -3451,6 +3605,200 @@ program
3451
3605
  );
3452
3606
  });
3453
3607
 
3608
+ program
3609
+ .command('uninstall')
3610
+ .description('Uninstall AgentVibes from current project')
3611
+ .option('-d, --directory <path>', 'Installation directory (default: current directory)')
3612
+ .option('-y, --yes', 'Skip confirmation prompt (auto-confirm)')
3613
+ .option('--global', 'Also remove global configuration (~/.claude/, ~/.agentvibes/)')
3614
+ .option('--with-piper', 'Also remove Piper TTS installation (~/piper/)')
3615
+ .action(async (options) => {
3616
+ const currentDir = process.env.INIT_CWD || process.cwd();
3617
+ const targetDir = options.directory || currentDir;
3618
+
3619
+ showWelcome();
3620
+
3621
+ console.log(chalk.cyan('šŸ“ Uninstall Details:'));
3622
+ console.log(chalk.gray(` Target directory: ${targetDir}`));
3623
+ console.log(chalk.gray(` Package version: ${VERSION}\n`));
3624
+
3625
+ // Check if installed
3626
+ const commandsDir = path.join(targetDir, '.claude', 'commands', 'agent-vibes');
3627
+ let isInstalled = false;
3628
+ try {
3629
+ await fs.access(commandsDir);
3630
+ isInstalled = true;
3631
+ } catch {}
3632
+
3633
+ if (!isInstalled) {
3634
+ console.log(chalk.yellow('āš ļø AgentVibes is not installed in this directory.'));
3635
+ console.log(chalk.gray(` Directory checked: ${targetDir}/.claude/`));
3636
+ console.log(chalk.gray(' Nothing to uninstall.\n'));
3637
+ process.exit(0);
3638
+ }
3639
+
3640
+ // Show what will be removed
3641
+ console.log(chalk.cyan('šŸ“¦ What will be removed:\n'));
3642
+
3643
+ const itemsToRemove = [];
3644
+
3645
+ // Project-level items
3646
+ console.log(chalk.white.bold(' Project Files:'));
3647
+ itemsToRemove.push({ path: '.claude/commands/agent-vibes/', desc: 'AgentVibes slash commands' });
3648
+ itemsToRemove.push({ path: '.claude/hooks/', desc: 'TTS scripts' });
3649
+ itemsToRemove.push({ path: '.claude/personalities/', desc: 'Personality templates' });
3650
+ itemsToRemove.push({ path: '.claude/output-styles/', desc: 'Output style templates' });
3651
+ itemsToRemove.push({ path: '.claude/audio/', desc: 'Audio cache' });
3652
+ itemsToRemove.push({ path: '.claude/tts-*.txt', desc: 'TTS configuration files' });
3653
+ itemsToRemove.push({ path: '.claude/*.json', desc: 'AgentVibes settings' });
3654
+ itemsToRemove.push({ path: '.agentvibes/', desc: 'BMAD integration files' });
3655
+
3656
+ for (const item of itemsToRemove) {
3657
+ console.log(chalk.gray(` • ${item.path}`));
3658
+ }
3659
+
3660
+ // Global items
3661
+ if (options.global) {
3662
+ console.log(chalk.white.bold('\n Global Files:'));
3663
+ console.log(chalk.gray(' • ~/.claude/ (global configuration)'));
3664
+ console.log(chalk.gray(' • ~/.agentvibes/ (global cache)'));
3665
+ }
3666
+
3667
+ // Piper TTS
3668
+ if (options.withPiper) {
3669
+ console.log(chalk.white.bold('\n TTS Engine:'));
3670
+ console.log(chalk.gray(' • ~/piper/ (Piper TTS installation)'));
3671
+ }
3672
+
3673
+ console.log('');
3674
+
3675
+ // Confirmation
3676
+ if (!options.yes) {
3677
+ const { confirm } = await inquirer.prompt([
3678
+ {
3679
+ type: 'confirm',
3680
+ name: 'confirm',
3681
+ message: chalk.yellow('Are you sure you want to uninstall AgentVibes?'),
3682
+ default: false,
3683
+ },
3684
+ ]);
3685
+
3686
+ if (!confirm) {
3687
+ console.log(chalk.green('\nāœ“ Uninstall cancelled. AgentVibes remains installed.\n'));
3688
+ process.exit(0);
3689
+ }
3690
+ } else {
3691
+ console.log(chalk.gray('āœ“ Auto-confirmed (--yes flag)\n'));
3692
+ }
3693
+
3694
+ const spinner = ora('Uninstalling AgentVibes...').start();
3695
+
3696
+ try {
3697
+ let removedCount = 0;
3698
+
3699
+ // Remove project-level files
3700
+ const projectPaths = [
3701
+ path.join(targetDir, '.claude', 'commands', 'agent-vibes'),
3702
+ path.join(targetDir, '.claude', 'hooks'),
3703
+ path.join(targetDir, '.claude', 'personalities'),
3704
+ path.join(targetDir, '.claude', 'output-styles'),
3705
+ path.join(targetDir, '.claude', 'audio'),
3706
+ path.join(targetDir, '.agentvibes'),
3707
+ ];
3708
+
3709
+ for (const dirPath of projectPaths) {
3710
+ try {
3711
+ await fs.rm(dirPath, { recursive: true, force: true });
3712
+ removedCount++;
3713
+ } catch (err) {
3714
+ // Ignore if directory doesn't exist
3715
+ }
3716
+ }
3717
+
3718
+ // Remove TTS config files
3719
+ const configPatterns = [
3720
+ 'tts-voice.txt',
3721
+ 'tts-provider.txt',
3722
+ 'tts-personality.txt',
3723
+ 'tts-verbosity.txt',
3724
+ 'tts-translate.txt',
3725
+ 'tts-target-voice.txt',
3726
+ 'tts-target-language.txt',
3727
+ 'tts-language.txt',
3728
+ 'personalities.json',
3729
+ 'github-star-reminder.txt',
3730
+ 'piper-voices-dir.txt',
3731
+ 'verbosity.txt',
3732
+ ];
3733
+
3734
+ for (const pattern of configPatterns) {
3735
+ const filePath = path.join(targetDir, '.claude', pattern);
3736
+ try {
3737
+ await fs.unlink(filePath);
3738
+ } catch (err) {
3739
+ // Ignore if file doesn't exist
3740
+ }
3741
+ }
3742
+
3743
+ // Remove global files if requested
3744
+ if (options.global) {
3745
+ const homedir = process.env.HOME || process.env.USERPROFILE;
3746
+ const globalPaths = [
3747
+ path.join(homedir, '.claude'),
3748
+ path.join(homedir, '.agentvibes'),
3749
+ ];
3750
+
3751
+ for (const dirPath of globalPaths) {
3752
+ try {
3753
+ await fs.rm(dirPath, { recursive: true, force: true });
3754
+ removedCount++;
3755
+ } catch (err) {
3756
+ // Ignore if directory doesn't exist
3757
+ }
3758
+ }
3759
+ }
3760
+
3761
+ // Remove Piper TTS if requested
3762
+ if (options.withPiper) {
3763
+ const homedir = process.env.HOME || process.env.USERPROFILE;
3764
+ const piperPath = path.join(homedir, 'piper');
3765
+
3766
+ try {
3767
+ await fs.rm(piperPath, { recursive: true, force: true });
3768
+ removedCount++;
3769
+ } catch (err) {
3770
+ // Ignore if directory doesn't exist
3771
+ }
3772
+ }
3773
+
3774
+ spinner.succeed(chalk.green('Successfully uninstalled AgentVibes!\n'));
3775
+
3776
+ // Show summary
3777
+ console.log(
3778
+ boxen(
3779
+ chalk.green.bold('āœ“ Uninstall Complete\n\n') +
3780
+ chalk.gray('AgentVibes has been removed from this project.\n') +
3781
+ (options.global ? chalk.gray('Global configuration has been removed.\n') : '') +
3782
+ (options.withPiper ? chalk.gray('Piper TTS has been removed.\n') : '') +
3783
+ chalk.gray('\nTo reinstall: ') + chalk.cyan('npx agentvibes install\n') +
3784
+ chalk.gray('\nWe\'d love to know why you uninstalled!\n') +
3785
+ chalk.gray('Share feedback: ') + chalk.cyan('https://github.com/paulpreibisch/AgentVibes/issues'),
3786
+ {
3787
+ padding: 1,
3788
+ margin: 1,
3789
+ borderStyle: 'round',
3790
+ borderColor: 'green',
3791
+ }
3792
+ )
3793
+ );
3794
+
3795
+ } catch (err) {
3796
+ spinner.fail(chalk.red('Failed to uninstall AgentVibes'));
3797
+ console.error(chalk.red(`Error: ${err.message}`));
3798
+ process.exit(1);
3799
+ }
3800
+ });
3801
+
3454
3802
  program
3455
3803
  .command('status')
3456
3804
  .description('Show installation status')