agentvibes 4.4.1 → 4.5.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.
Files changed (50) hide show
  1. package/.agentvibes/config.json +4 -4
  2. package/.claude/config/reverb-level.txt +1 -1
  3. package/.claude/github-star-reminder.txt +1 -1
  4. package/.claude/hooks-windows/bmad-speak.ps1 +112 -0
  5. package/.claude/hooks-windows/play-tts-piper.ps1 +3 -4
  6. package/.claude/hooks-windows/play-tts-sapi.ps1 +3 -4
  7. package/.claude/hooks-windows/play-tts-soprano.ps1 +2 -3
  8. package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -0
  9. package/.claude/hooks-windows/play-tts.ps1 +14 -6
  10. package/.claude/hooks-windows/provider-manager.ps1 +16 -1
  11. package/CLAUDE.md +4 -0
  12. package/README.md +39 -9
  13. package/RELEASE_NOTES.md +39 -0
  14. package/bin/agent-vibes +1 -1
  15. package/bin/agentvibes-voice-browser.js +1 -1
  16. package/bin/bmad-speak.js +52 -0
  17. package/bin/mcp-server.js +1 -1
  18. package/bin/test-bmad-pr +1 -1
  19. package/package.json +1 -1
  20. package/setup-windows.ps1 +4 -4
  21. package/src/console/app.js +58 -11
  22. package/src/console/tabs/agents-tab.js +61 -65
  23. package/src/console/tabs/help-tab.js +107 -54
  24. package/src/console/tabs/install-tab.js +107 -47
  25. package/src/console/tabs/music-tab.js +1030 -1011
  26. package/src/console/tabs/placeholder-tab.js +27 -0
  27. package/src/console/tabs/readme-tab.js +9 -7
  28. package/src/console/tabs/receiver-tab.js +23 -12
  29. package/src/console/tabs/settings-tab.js +4001 -3783
  30. package/src/console/tabs/voices-tab.js +1680 -1653
  31. package/src/console/widgets/personality-picker.js +35 -7
  32. package/src/console/widgets/reverb-picker.js +9 -6
  33. package/src/console/widgets/track-picker.js +6 -1
  34. package/src/i18n/de.js +201 -0
  35. package/src/i18n/en.js +201 -0
  36. package/src/i18n/es.js +201 -0
  37. package/src/i18n/fr.js +201 -0
  38. package/src/i18n/hi.js +201 -0
  39. package/src/i18n/ja.js +201 -0
  40. package/src/i18n/ko.js +201 -0
  41. package/src/i18n/pt.js +201 -0
  42. package/src/i18n/strings.js +54 -0
  43. package/src/i18n/zh-CN.js +201 -0
  44. package/src/installer/language-screen.js +31 -0
  45. package/src/installer.js +79 -25
  46. package/src/services/language-service.js +47 -0
  47. package/src/utils/file-ownership-verifier.js +2 -2
  48. package/src/utils/provider-validator.js +9 -13
  49. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +0 -209
  50. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +0 -108
@@ -0,0 +1,201 @@
1
+ export default {
2
+ languageSelectTitle: "🌐 Select Language / Seleccionar Idioma / Choisir la langue / Sprache wählen / Selecionar Idioma / 言語を選択 / भाषा चुनें / 选择语言 / 언어 선택",
3
+ languageApplied: "语言已应用",
4
+ welcomeTitle: "欢迎使用 AgentVibes!",
5
+ installDetails: "安装详情",
6
+ installLocation: "安装位置",
7
+ packageVersion: "包版本",
8
+ readyToConfigure: "准备好配置 AgentVibes 了吗?",
9
+ installationCancelled: "安装已取消。",
10
+ startInstall: "开始安装",
11
+ installationCancelledMsg: "安装已被用户取消。",
12
+ installedSuccess: "AgentVibes 安装成功!",
13
+ installComplete: "安装完成",
14
+ installationFailed: "安装失败",
15
+ installing: "正在安装 AgentVibes...",
16
+ providerLabel: "提供商",
17
+ voiceLabel: "语音",
18
+ locationLabel: "位置",
19
+ versionLabel: "版本",
20
+ configurationSetup: "配置",
21
+ configurationIntro: "请配置您的 AgentVibes 安装。",
22
+ navigationHint: "使用方向键在页面之间导航。",
23
+ nonInteractiveDetected: "检测到非交互模式",
24
+ installError: "安装失败",
25
+ continuePrompt: "继续",
26
+ cancelPrompt: "取消",
27
+ setupWizard: "安装向导",
28
+ setupWizardSubtitle: "为AI助手提供个性化TTS。",
29
+ dependencyCheck: "依赖项检查",
30
+ checkingDependencies: "正在检查依赖项...",
31
+ depColumn: "依赖项",
32
+ statusColumn: "状态",
33
+ installed: "已安装",
34
+ notFound: "未找到",
35
+ ffmpegMissing: "未找到(背景音乐所需)",
36
+ ttsDetected: "已检测到TTS提供商",
37
+ noTtsFound: "未找到TTS提供商。请先安装Piper或Soprano。",
38
+ providerSelection: "提供商选择",
39
+ availableProviders: "可用的TTS提供商:",
40
+ providerAndVoice: "提供商和语音",
41
+ voiceChangeHint: "(安装后可在设置中更改)",
42
+ introText: "介绍文本",
43
+ introTextLabel: "介绍文本",
44
+ none: "无",
45
+ example: "示例",
46
+ screen1Hint: "屏幕1/5: 欢迎 | [←/→] 导航 | [Enter] 开始 | [Esc] 退出",
47
+ screen2Hint: "屏幕2/5: 依赖项 | [←] 返回 | [Enter] 下一步",
48
+ screen3Hint: "屏幕3/5: 提供商 | [←] 返回 | [↑↓] 选择 | [Enter/→] 确认",
49
+ screen4Hint: "屏幕4/5: 配置 | [Esc] 返回 | [E] 编辑 | [↓] 接受并安装",
50
+ screen5HintDone: "屏幕5/5: 完成 | [Enter] 确定 — 完成",
51
+ screen5HintWait: "屏幕5/5: 安装中... | 请稍候",
52
+ languageSettings: "语言",
53
+ languageSettingsSubtitle: "选择界面语言",
54
+ currentLanguage: "当前语言",
55
+ changeLanguage: "更改语言",
56
+ beginBtn: "▶ 开始",
57
+ exitBtn: "✗ 退出",
58
+ footerText: "[Enter] 继续/完成 [Esc] 返回/退出 [C] 控制台 [S/V/M/A/R] 标签 [Q] 退出",
59
+ customizationTool: "定制工具",
60
+ quitLabel: "[Q] 退出",
61
+ tabInstall: "安装",
62
+ tabSettings: "设置",
63
+ tabVoices: "语音",
64
+ tabMusic: "音乐",
65
+ tabBmad: "BMad",
66
+ tabReceiver: "接收器",
67
+ tabReadme: "说明",
68
+ tabHelp: "帮助",
69
+ subTabVoice: " [V] 语音 ",
70
+ subTabEffects: " [E] 效果 ",
71
+ subTabPersonality: " [P] 个性 ",
72
+ subTabOutput: " [O] 输出 ",
73
+ subTabLanguage: " [L] 语言 ",
74
+ sectionProviderVoice: " 🎤 提供商与语音 ",
75
+ sectionAudioEffects: " ⚡ 音频效果 ",
76
+ sectionBgMusic: " 🎸 背景音乐 ",
77
+ sectionStyle: " 🎭 风格 ",
78
+ sectionIntroText: " ✍️ 介绍文本 ",
79
+ sectionAudioDest: " 📡 音频目标 ",
80
+ sectionConfigStorage: " 💾 配置存储 ",
81
+ sectionLanguage: " 🌐 语言 ",
82
+ providerRowLabel: "提供商:",
83
+ currentVoiceLabel: "当前语音:",
84
+ reverbLabel: "混响:",
85
+ trackLabel: "曲目:",
86
+ volumeLabel: "音量:",
87
+ verbosityLabel: "详细度:",
88
+ personalityLabel: "个性:",
89
+ introTextRowLabel: "介绍文本:",
90
+ destinationLabel: "目标:",
91
+ sshAliasLabel: "SSH别名:",
92
+ globalLabel: "全局:",
93
+ localLabel: "本地:",
94
+ languageLabel: "语言:",
95
+ switchBtn: "切换",
96
+ changeBtn: "更改",
97
+ playBtn: "▶ 播放",
98
+ stopBtn: "■ 停止",
99
+ previewBtn: "▶ 预览",
100
+ fullPreviewBtn: "▶ 完整预览",
101
+ saveGloballyBtn: "全局保存",
102
+ saveLocallyBtn: "本地保存",
103
+ cancelChangesBtn: "取消更改",
104
+ editBtn: "编辑",
105
+ clearBtn: "清除",
106
+ applyLanguageBtn: "✓ 应用语言",
107
+ enabledBtn: "已启用",
108
+ disabledBtn: "已禁用",
109
+ okSaveBtn: "OK — 保存",
110
+ enableBtn: "启用",
111
+ streamingTextBtn: "仅文本流 ✓",
112
+ streamingPulseBtn: "Pulse音频流",
113
+ continueArrowBtn: "继续 →",
114
+ acceptInstallBtn: "✓ 接受并安装",
115
+ okDoneBtn: "✓ OK — 完成",
116
+ editInstallBtn: "编辑",
117
+ musicDisabledMsg: "音乐已禁用。现在启用?",
118
+ settingsFooter: "[↑↓] 组 [←→] 兄弟/子标签 [Enter/Space] 激活 [Tab] 切换标签 [Q] 退出",
119
+ voicesFooter: "[↑↓/jk] 导航 [Space] 预览 [Enter] 选择 [F] 收藏 [/] 搜索",
120
+ musicFooter: "[↑↓/jk] 导航 [Space] 预览 [Enter] 选择 [M] 切换 [*] 收藏 [F] 筛选 [Q] 退出",
121
+ helpFooter: "[↑↓/jk] 滚动 [/] 搜索 [PgUp/PgDn] 翻页 [S/V/M/A/R] 标签 [Q] 退出",
122
+ readmeFooter: "[↑↓/jk] 滚动 [PgUp/PgDn] 翻页 [/] 搜索 [S/V/M/A/R] 标签 [Q] 退出",
123
+ receiverFooter: "SSH接收器 [Q] 退出",
124
+ searchLabel: "搜索:",
125
+ settingsSavedMsg: "设置已保存",
126
+ changesRevertedMsg: "更改已撤销",
127
+ musicBuiltInHeader: "── 内置音轨 ",
128
+ musicStatusHeader: "── 音乐状态 ",
129
+ musicStatusLabel: "音乐:",
130
+ musicActiveTrack: "当前音轨:",
131
+ musicFilterLabel: "过滤:",
132
+ musicFilterAll: "全部",
133
+ musicFilterFavs: "收藏",
134
+ musicToggleBtn: "[切换音乐]",
135
+ musicAddCustomBtn: "[添加自定义音轨]",
136
+ voicesHeader: "── 语音 ",
137
+ voicesColName: "名称",
138
+ voicesColGender: "性别",
139
+ voicesColProvider: "提供者",
140
+ voicesInfoHeader: "── 语音信息 ",
141
+ voicesSwitchBtn: "[切换语音]",
142
+ voicesFavoriteBtn: "[★ 收藏]",
143
+ voicesDownloadBtn: "[下载语音]",
144
+ voicesRowHintInstalled: "[Space] 预览 [Enter] 选择 [*] 收藏",
145
+ voicesRowHintUninstalled: "[Enter] 下载并安装",
146
+ musicRowHint: "[Space] 播放 [Enter] 选择 [*] 收藏",
147
+ musicHintText: "[Space] 预览 [Enter] 设置音轨 [*] 收藏",
148
+ genderFemale: "女性",
149
+ genderMale: "男性",
150
+ voiceInfoVoice: "语音:",
151
+ voiceInfoGender: "性别:",
152
+ voiceInfoLanguage: "语言:",
153
+ voiceInfoQuality: "质量:",
154
+ voiceInfoProvider: "提供者:",
155
+ voiceInfoId: "ID:",
156
+ voiceInfoSpeaker: "说话者:",
157
+ voiceInfoModel: "模型:",
158
+ voiceInfoSpeakerId: "说话者ID:",
159
+ voiceInfoDownload: "⬇ 按[Enter]下载并安装",
160
+ voicePlaying: "(播放中)",
161
+ bmadTitle: "🧙 BMAD代理",
162
+ bmadWhatIsHeader: "什么是BMAD?",
163
+ bmadDesc: "BMad方法(Build More Architect Dreams)是一个AI驱动的开发框架模块,\n帮助您完成从构思规划到智能实现的整个软件开发过程。提供专业AI代理、\n引导式工作流程和智能规划。\n\n如果您习惯使用Claude、Cursor或GitHub Copilot等AI编程助手,\n您已经准备好开始了。",
164
+ bmadInstallHeader: "在您的项目中安装BMAD:",
165
+ bmadLearnMoreHeader: "了解更多:",
166
+ bmadInstalledNote: "BMAD安装后,此标签页将显示您所有的代理,并允许您独立自定义每个代理\n的语音、前置文本、混响、个性和背景音乐。",
167
+ receiverWhatIsTitle: "什么是SSH接收器?",
168
+ receiverDesc: "SSH接收器让您的远程服务器通过这台机器发声。当远程服务器上的AI助手\n需要播放TTS音频时,它通过SSH将文本发送到这台机器,由这台机器生成\n并通过本地扬声器播放音频。\n\n远程AI ──[SSH]──► 这台机器 ──[piper+sox+ffmpeg]──► 您的扬声器",
169
+ helpSectionGlobal: "全局快捷键",
170
+ helpSectionNavigation: "导航快捷键",
171
+ helpSectionColors: "标签颜色指南",
172
+ helpQuit: "退出控制台",
173
+ helpForceQuit: "强制退出",
174
+ helpSwitchSettings: "切换到设置标签",
175
+ helpSwitchVoices: "切换到语音标签",
176
+ helpSwitchMusic: "切换到音乐标签",
177
+ helpSwitchReadme: "切换到Readme标签",
178
+ helpSwitchHelp: "切换到帮助标签",
179
+ helpSwitchInstall: "切换到安装标签",
180
+ helpCloseModal: "关闭对话框 / 返回",
181
+ helpNavigateLists: "浏览列表",
182
+ helpSelectActivate: "选择 / 激活",
183
+ helpTogglePreview: "切换 / 预览",
184
+ helpNextButton: "下一个按钮",
185
+ helpPrevButton: "上一个按钮",
186
+ helpOpenSearch: "打开搜索/筛选",
187
+ helpToggleFavFilter: "切换收藏筛选(语音/音乐)",
188
+ helpToggleFav: "切换收藏(音乐标签)",
189
+ helpToggleMusic: "开启/关闭音乐(音乐标签)",
190
+ helpColorSettings: "设置标签页脚",
191
+ helpColorVoices: "语音标签页脚",
192
+ helpColorMusic: "音乐标签页脚",
193
+ helpColorReadme: "Readme标签页脚",
194
+ helpColorHelp: "帮助标签页脚",
195
+ helpColorInstall: "安装标签页脚",
196
+ helpSearchLabel: "搜索:",
197
+ readmeScrollMore: "↓ 向下滚动查看更多 ↓",
198
+ readmeNotFound: "*(当前目录中未找到README.md)*",
199
+ bmadFooterNobmad: "[Tab] 切换标签 [Q] 退出",
200
+ bmadFooterBmad: "[↑↓/jk] 导航 [Space] 预览 [Enter] 配置 [A] 自动分配 [B] 批量 [X] 重置 [Q] 退出",
201
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * File: src/installer/language-screen.js
3
+ *
4
+ * Language selection screen for the AgentVibes installer TUI.
5
+ * Presents a list of supported languages and returns the selected code.
6
+ */
7
+
8
+ import inquirer from 'inquirer';
9
+ import { t, SUPPORTED_LANGUAGES } from '../i18n/strings.js';
10
+
11
+ /**
12
+ * Show the language selection screen and return the chosen language code.
13
+ *
14
+ * @param {string} [currentLang='en'] - Currently active language code (used as default)
15
+ * @returns {Promise<string>} The selected language code (e.g. 'es', 'zh-CN')
16
+ */
17
+ export async function selectLanguage(currentLang = 'en') {
18
+ const { lang } = await inquirer.prompt([{
19
+ type: 'list',
20
+ name: 'lang',
21
+ message: '🌐 Select Language / Seleccionar Idioma / Choisir la langue / Sprache wählen / Selecionar Idioma / 言語を選択 / भाषा चुनें / 选择语言 / 언어 선택',
22
+ default: currentLang,
23
+ choices: SUPPORTED_LANGUAGES
24
+ }]);
25
+
26
+ console.clear();
27
+ const chosenName = SUPPORTED_LANGUAGES.find(l => l.value === lang)?.name ?? lang;
28
+ console.log(`\u2713 ${t(lang, 'languageApplied')}: ${chosenName}\n`);
29
+
30
+ return lang;
31
+ }
package/src/installer.js CHANGED
@@ -72,6 +72,8 @@ import {
72
72
  } from './utils/provider-validator.js';
73
73
  import { promptForCustomMusic } from './installer/music-file-input.js';
74
74
  import { createPreviewListPrompt } from './utils/preview-list-prompt.js';
75
+ import { selectLanguage } from './installer/language-screen.js';
76
+ import { t } from './i18n/strings.js';
75
77
 
76
78
  const __filename = fileURLToPath(import.meta.url);
77
79
  const __dirname = path.dirname(__filename);
@@ -199,9 +201,9 @@ function supportsEmoji() {
199
201
 
200
202
  const isModernTerminal = modernTerminals.some(t => term.toLowerCase().includes(t));
201
203
 
202
- // Windows Terminal always supports emoji
204
+ // Windows Terminal always supports emoji — coerce to boolean to avoid returning WT_SESSION UUID
203
205
  const isWindowsTerminal = process.platform === 'win32' &&
204
- (process.env.WT_SESSION || process.env.WT_PROFILE_ID);
206
+ !!(process.env.WT_SESSION || process.env.WT_PROFILE_ID);
205
207
 
206
208
  // macOS Terminal and iTerm2
207
209
  const isMacOS = process.platform === 'darwin';
@@ -1115,6 +1117,9 @@ async function collectConfiguration(options = {}) {
1115
1117
  }
1116
1118
  const homeDir = process.env.HOME || process.env.USERPROFILE;
1117
1119
  config.piperPath = path.join(homeDir, '.claude', 'piper-voices');
1120
+ // AI agent / non-interactive defaults: no reverb, no background music
1121
+ config.reverb = 'none';
1122
+ config.backgroundMusic = { enabled: false, track: 'agentvibes_soft_flamenco_loop.mp3' };
1118
1123
  return config;
1119
1124
  }
1120
1125
 
@@ -4936,8 +4941,14 @@ async function install(options = {}) {
4936
4941
  const configOffset = 0;
4937
4942
 
4938
4943
  // Loop to allow going back to welcome screen
4944
+ let lang = 'en';
4939
4945
  let userConfig = null;
4946
+ const isNonInteractive = options.yes || options.nonInteractive || process.env.AGENT_VIBES_NON_INTERACTIVE === '1';
4940
4947
  while (!userConfig) {
4948
+ // Language selection screen — skip in non-interactive / CI mode
4949
+ if (!isNonInteractive) {
4950
+ lang = await selectLanguage(lang);
4951
+ }
4941
4952
  showWelcome();
4942
4953
 
4943
4954
  // Show release notes and recent changes after welcome banner
@@ -4970,6 +4981,7 @@ async function install(options = {}) {
4970
4981
  // Returns null if user wants to go back to welcome
4971
4982
  userConfig = await collectConfiguration({
4972
4983
  ...options,
4984
+ lang,
4973
4985
  pageOffset: configOffset,
4974
4986
  totalPages: configPages // Temporary, will show correct count later
4975
4987
  });
@@ -4979,19 +4991,37 @@ async function install(options = {}) {
4979
4991
  const piperVoicesPath = userConfig.piperPath;
4980
4992
  const targetDir = options.directory || currentDir;
4981
4993
 
4982
- // Confirm and start installation
4983
- const { startInstall } = await inquirer.prompt([
4984
- {
4985
- type: 'confirm',
4986
- name: 'startInstall',
4987
- message: chalk.yellow('✅ Start Installation?'),
4988
- default: true,
4989
- },
4990
- ]);
4994
+ // Non-interactive mode: structured logging and piper validation before install
4995
+ if (options.nonInteractive || process.env.AGENT_VIBES_NON_INTERACTIVE === '1') {
4996
+ console.log(`[AV] Non-interactive mode detected`);
4997
+ console.log(`[AV] Provider: ${selectedProvider} | Platform: ${process.platform}`);
4991
4998
 
4992
- if (!startInstall) {
4993
- console.log(chalk.red('\n❌ Installation cancelled.\n'));
4994
- process.exit(0);
4999
+ if (isPiperProvider(selectedProvider) && !isPiperInstalled()) {
5000
+ process.stderr.write(`[AV ERROR] Piper binaries not found.\n`);
5001
+ process.stderr.write(`[AV] To install Piper manually, run:\n`);
5002
+ process.stderr.write(`[AV] npx agentvibes --install-piper\n`);
5003
+ process.stderr.write(`[AV] Or visit: https://github.com/paulpreibisch/AgentVibes#-installation\n`);
5004
+ process.exit(1);
5005
+ }
5006
+
5007
+ console.log(`[AV] Installing to: ${targetDir}/.claude/`);
5008
+ }
5009
+
5010
+ // Confirm and start installation (skip in non-interactive / --yes mode)
5011
+ if (!options.yes && !options.nonInteractive && process.env.AGENT_VIBES_NON_INTERACTIVE !== '1') {
5012
+ const { startInstall } = await inquirer.prompt([
5013
+ {
5014
+ type: 'confirm',
5015
+ name: 'startInstall',
5016
+ message: chalk.yellow('✅ Start Installation?'),
5017
+ default: true,
5018
+ },
5019
+ ]);
5020
+
5021
+ if (!startInstall) {
5022
+ console.log(chalk.red('\n❌ Installation cancelled.\n'));
5023
+ process.exit(0);
5024
+ }
4995
5025
  }
4996
5026
 
4997
5027
  // Silent spinner for copy functions — suppresses per-file output
@@ -5183,6 +5213,13 @@ Troubleshooting:
5183
5213
  }
5184
5214
  }
5185
5215
 
5216
+ // Persist language selection — validate against known codes before writing
5217
+ if (lang && /^[a-zA-Z]{2}(-[a-zA-Z]{2})?$/.test(lang)) {
5218
+ const langConfigPath = path.join(claudeDir, 'config', 'language.txt');
5219
+ await fs.mkdir(path.join(claudeDir, 'config'), { recursive: true });
5220
+ await fs.writeFile(langConfigPath, lang, { mode: 0o600 });
5221
+ }
5222
+
5186
5223
  // Apply verbosity, personality, pretext
5187
5224
  await fs.writeFile(path.join(claudeDir, 'tts-verbosity.txt'), userConfig.verbosity);
5188
5225
  if (userConfig.personality && userConfig.personality !== 'none') {
@@ -5215,19 +5252,28 @@ Troubleshooting:
5215
5252
 
5216
5253
  spinner.succeed(chalk.green('AgentVibes installed successfully!'));
5217
5254
 
5218
- // Clean final summary
5219
- console.log('');
5220
- console.log(chalk.green.bold(' ✅ Installation Complete'));
5221
- console.log(chalk.gray(` Provider: ${selectedProvider}`));
5222
- console.log(chalk.gray(` Location: ${targetDir}/.claude/`));
5223
- console.log(chalk.gray(` Version: ${VERSION}`));
5224
- console.log('');
5225
- console.log(chalk.white(' Run ') + chalk.cyan('npx agentvibes') + chalk.white(' to open the console.'));
5226
- console.log('');
5255
+ if (options.nonInteractive || process.env.AGENT_VIBES_NON_INTERACTIVE === '1') {
5256
+ console.log(`[AV] Installation complete`);
5257
+ console.log(`[AV] Provider: ${selectedProvider} | Location: ${targetDir}/.claude/ | Version: ${VERSION}`);
5258
+ } else {
5259
+ // Clean final summary
5260
+ console.log('');
5261
+ console.log(chalk.green.bold(' ✅ Installation Complete'));
5262
+ console.log(chalk.gray(` Provider: ${selectedProvider}`));
5263
+ console.log(chalk.gray(` Location: ${targetDir}/.claude/`));
5264
+ console.log(chalk.gray(` Version: ${VERSION}`));
5265
+ console.log('');
5266
+ console.log(chalk.white(' Run ') + chalk.cyan('npx agentvibes') + chalk.white(' to open the console.'));
5267
+ console.log('');
5268
+ }
5227
5269
 
5228
5270
  } catch (error) {
5229
- spinner.fail('Installation failed!');
5230
- console.error(chalk.red('\n❌ Error:'), error.message);
5271
+ if (options.nonInteractive || process.env.AGENT_VIBES_NON_INTERACTIVE === '1') {
5272
+ process.stderr.write(`[AV ERROR] Installation failed: ${error.message}\n`);
5273
+ } else {
5274
+ spinner.fail('Installation failed!');
5275
+ console.error(chalk.red('\n❌ Error:'), error.message);
5276
+ }
5231
5277
  process.exit(1);
5232
5278
  }
5233
5279
  }
@@ -5242,7 +5288,15 @@ program
5242
5288
  .description('Install AgentVibes voice commands')
5243
5289
  .option('-d, --directory <path>', 'Installation directory (default: current directory)')
5244
5290
  .option('-y, --yes', 'Skip confirmation prompt (auto-confirm)')
5291
+ .option('--non-interactive', 'Skip TUI and install with defaults (for AI agents and CI pipelines). Also triggered by AGENT_VIBES_NON_INTERACTIVE=1 env var.')
5245
5292
  .action(async (options) => {
5293
+ // Merge env var trigger into options
5294
+ if (process.env.AGENT_VIBES_NON_INTERACTIVE === '1') {
5295
+ options.nonInteractive = true;
5296
+ }
5297
+ if (options.nonInteractive) {
5298
+ options.yes = true;
5299
+ }
5246
5300
  await install(options);
5247
5301
  });
5248
5302
 
@@ -0,0 +1,47 @@
1
+ /**
2
+ * LanguageService — single source of truth for the selected UI language.
3
+ *
4
+ * Persists the selection to ~/.claude/config/language.txt so it survives
5
+ * process restarts. Notifies registered listeners on every change so the
6
+ * TUI can re-render dynamic labels immediately.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import os from 'node:os';
12
+ import { t, SUPPORTED_LANGUAGES } from '../i18n/strings.js';
13
+
14
+ const LANG_FILE = path.join(os.homedir(), '.claude', 'config', 'language.txt');
15
+ const VALID_LANGS = new Set(SUPPORTED_LANGUAGES.map(l => l.value));
16
+
17
+ export class LanguageService {
18
+ constructor() {
19
+ this._lang = this._load();
20
+ this._listeners = [];
21
+ }
22
+
23
+ _load() {
24
+ try {
25
+ const val = fs.readFileSync(LANG_FILE, 'utf8').trim();
26
+ return VALID_LANGS.has(val) ? val : 'en';
27
+ } catch {
28
+ return 'en';
29
+ }
30
+ }
31
+
32
+ getLang() { return this._lang; }
33
+
34
+ setLang(lang) {
35
+ if (!VALID_LANGS.has(lang)) return;
36
+ this._lang = lang;
37
+ try {
38
+ fs.mkdirSync(path.dirname(LANG_FILE), { recursive: true });
39
+ fs.writeFileSync(LANG_FILE, lang, { mode: 0o600 });
40
+ } catch { /* non-fatal */ }
41
+ this._listeners.forEach(fn => fn(lang));
42
+ }
43
+
44
+ onChange(fn) { this._listeners.push(fn); }
45
+
46
+ t(key) { return t(this._lang, key); }
47
+ }
@@ -250,9 +250,9 @@ function checkIsNetworkMount(filePath) {
250
250
 
251
251
  if (process.platform === 'win32') {
252
252
  // Windows: check for UNC paths (\\server\share) or mapped drives
253
- // Also check for paths under %APPDATA% or similar which might be network-synced
253
+ // Coerce to boolean match() returns array|null, not boolean
254
254
  return resolvedPath.startsWith('\\\\') ||
255
- resolvedPath.match(/^[A-Z]:\\[^\\]*\\netshare/i);
255
+ !!resolvedPath.match(/^[A-Z]:\\[^\\]*\\netshare/i);
256
256
  } else {
257
257
  // Unix: check for common network mount prefixes
258
258
  // /mnt, /media, NFS mount points
@@ -3,7 +3,6 @@
3
3
  * Validates TTS provider availability at installation, switch, and runtime
4
4
  */
5
5
 
6
- import { execSync } from 'node:child_process';
7
6
  import { spawnSync } from 'node:child_process';
8
7
  import path from 'node:path'; // For safe path operations and traversal prevention
9
8
  import fs from 'node:fs'; // For checking file/directory existence
@@ -15,16 +14,12 @@ import os from 'node:os'; // For os.homedir() to prevent HOME injection attacks
15
14
  * @returns {boolean} True if command exists in PATH
16
15
  */
17
16
  function commandExistsInPath(command) {
18
- try {
19
- execSync(`which "${command}" 2>/dev/null`, {
20
- encoding: 'utf8',
21
- shell: true,
22
- stdio: ['pipe', 'pipe', 'pipe']
23
- });
24
- return true;
25
- } catch {
26
- return false;
27
- }
17
+ // SECURITY: Use spawnSync instead of execSync+shell to prevent command injection (#126)
18
+ const result = spawnSync('which', [command], {
19
+ encoding: 'utf8',
20
+ stdio: ['pipe', 'pipe', 'pipe']
21
+ });
22
+ return result.status === 0;
28
23
  }
29
24
 
30
25
  /**
@@ -180,11 +175,12 @@ export async function validateMacOSProvider() {
180
175
  }
181
176
 
182
177
  try {
183
- execSync('which say 2>/dev/null', {
178
+ // SECURITY: Use spawnSync instead of execSync+shell to prevent command injection (#126)
179
+ const result = spawnSync('which', ['say'], {
184
180
  encoding: 'utf8',
185
- shell: true,
186
181
  stdio: ['pipe', 'pipe', 'pipe']
187
182
  });
183
+ if (result.status !== 0) throw new Error('say not found');
188
184
  return { installed: true, message: 'macOS Say detected' };
189
185
  } catch (error) {
190
186
  // say command not found