verbalcoding 0.2.7 → 0.2.8

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 (44) hide show
  1. package/README.md +12 -27
  2. package/app-node/cli_install.test.mjs +15 -0
  3. package/docs/FRESH_INSTALL.md +8 -2
  4. package/docs/assets/figures/verbalcoding-flow.svg +45 -30
  5. package/docs/i18n/CONFIGURATION.es.md +138 -49
  6. package/docs/i18n/CONFIGURATION.fr.md +138 -49
  7. package/docs/i18n/CONFIGURATION.ja.md +137 -48
  8. package/docs/i18n/CONFIGURATION.ko.md +137 -48
  9. package/docs/i18n/CONFIGURATION.ru.md +138 -49
  10. package/docs/i18n/CONFIGURATION.zh.md +137 -48
  11. package/docs/i18n/FRESH_INSTALL.es.md +115 -32
  12. package/docs/i18n/FRESH_INSTALL.fr.md +115 -32
  13. package/docs/i18n/FRESH_INSTALL.ja.md +119 -36
  14. package/docs/i18n/FRESH_INSTALL.ko.md +120 -37
  15. package/docs/i18n/FRESH_INSTALL.ru.md +115 -32
  16. package/docs/i18n/FRESH_INSTALL.zh.md +119 -36
  17. package/docs/i18n/MULTI_INSTANCE.es.md +85 -26
  18. package/docs/i18n/MULTI_INSTANCE.fr.md +85 -26
  19. package/docs/i18n/MULTI_INSTANCE.ja.md +87 -29
  20. package/docs/i18n/MULTI_INSTANCE.ko.md +87 -29
  21. package/docs/i18n/MULTI_INSTANCE.ru.md +84 -26
  22. package/docs/i18n/MULTI_INSTANCE.zh.md +87 -29
  23. package/docs/i18n/README.es.md +109 -45
  24. package/docs/i18n/README.fr.md +109 -45
  25. package/docs/i18n/README.ja.md +109 -45
  26. package/docs/i18n/README.ko.md +108 -45
  27. package/docs/i18n/README.ru.md +109 -45
  28. package/docs/i18n/README.zh.md +108 -45
  29. package/docs/i18n/RELEASE.es.md +53 -37
  30. package/docs/i18n/RELEASE.fr.md +53 -37
  31. package/docs/i18n/RELEASE.ja.md +52 -36
  32. package/docs/i18n/RELEASE.ko.md +52 -36
  33. package/docs/i18n/RELEASE.ru.md +53 -37
  34. package/docs/i18n/RELEASE.zh.md +53 -37
  35. package/docs/i18n/USAGE.es.md +91 -64
  36. package/docs/i18n/USAGE.fr.md +91 -64
  37. package/docs/i18n/USAGE.ja.md +90 -63
  38. package/docs/i18n/USAGE.ko.md +90 -63
  39. package/docs/i18n/USAGE.ru.md +91 -64
  40. package/docs/i18n/USAGE.zh.md +90 -63
  41. package/package.json +1 -1
  42. package/scripts/bootstrap_prereqs.sh +15 -3
  43. package/scripts/cli.mjs +1 -1
  44. package/scripts/doctor.mjs +114 -8
@@ -1,71 +1,78 @@
1
1
  # VerbalCoding 使用指南
2
2
 
3
- Operational details for 中文 users.
3
+ 本页面保存曾经让 README 过长的运维细节。
4
4
 
5
- ## CLI Commands
5
+ ## CLI 命令
6
6
 
7
7
  ```bash
8
- vc status
9
- vc language en
10
- vc language ko
11
- vc language auto
12
- vc restart auto status
13
- vc restart auto on
14
- vc restart auto off
15
- vc bot invite CLIENT_ID
16
- vc instance status
17
- vc instance setup NAME
18
- vc instance start NAME
19
- vc instance stop NAME
20
- vc doctor
21
- npm run mcp
8
+ vc status # 显示 STT 语言、进度语言和 TTS 声音
9
+ vc language en # 英语 STT + 英语进度/TTS 声音
10
+ vc language ko # 韩语 STT + 韩语进度/TTS 声音
11
+ vc language auto # Whisper 自动检测 STT + 英语进度/TTS 声音
12
+ vc restart auto status # 显示提交时语音机器人自动重启设置
13
+ vc restart auto on # 启用提交时语音机器人自动重启
14
+ vc restart auto off # 禁用它;这是默认值
15
+ vc bot invite CLIENT_ID # 打印带所需权限的 Discord 邀请 URL
16
+ vc instance status # 列出每实例桥接配置和进程状态
17
+ vc instance setup NAME # 写入 instances/NAME.env 并创建 ~/.hermes/profiles/NAME
18
+ vc instance start NAME # 分离启动 ./run.sh instances/NAME.env
19
+ vc instance stop NAME # 停止分离的实例并移除其 pid 文件
20
+ vc doctor # 运行脱敏 doctor 检查
21
+ npm run mcp # 运行 stdio MCP 服务器
22
22
  ```
23
23
 
24
- Language commands update `.env`; restart with `vc start`, `./run.sh`, or your process manager.
24
+ 语言更改会更新 `.env`;请使用 `./run.sh` 或你的进程管理器重启桥接,让更改生效。
25
+
26
+ ## 运行模式
25
27
 
26
- ## Run Modes
28
+ 单实例桥接:
27
29
 
28
30
  ```bash
29
- vc start
30
31
  ./run.sh
32
+ ```
33
+
34
+ 使用本地覆盖 env 的每实例桥接:
35
+
36
+ ```bash
31
37
  ./run.sh instances/my-project.env
38
+ # 或
32
39
  VERBALCODING_INSTANCE_ENV=instances/my-project.env ./run.sh
33
40
  ```
34
41
 
35
- The bot auto-joins the first configured channel name, defaulting to `일반,General,general`.
42
+ 机器人会自动加入第一个已配置的频道名称,默认是 `일반,General,general`。
36
43
 
37
- ## Discord Commands
44
+ ## Discord 命令
38
45
 
39
- Before using commands, set up the Discord application/bot:
46
+ 在接线命令之前,请使用上游指南设置 Discord 应用/机器人:
40
47
 
41
- - Hermes Agent Discord guide: <https://hermes-agent.nousresearch.com/docs/user-guide/messaging/discord>
42
- - Discord official bot docs: <https://docs.discord.com/developers/bots/overview>
48
+ - Hermes Agent Discord 指南:<https://hermes-agent.nousresearch.com/docs/user-guide/messaging/discord>
49
+ - Discord 官方机器人文档:<https://docs.discord.com/developers/bots/overview>
43
50
 
44
- Then run `vc bot invite CLIENT_ID` for the VerbalCoding permissions.
51
+ 然后使用 `vc bot invite CLIENT_ID` 生成带文本和语音权限的 VerbalCoding 专用邀请 URL。
45
52
 
46
- | Command | Purpose |
53
+ | 命令 | 用途 |
47
54
  |---|---|
48
- | `!ping` | Basic bot check |
49
- | `!join` / `!leave` | Join or leave voice |
50
- | `!say <text>` | Speak text directly through TTS |
51
- | `!voice-test <text>` | Test the active TTS backend/voice |
52
- | `!voice-clone capture` | Save the next valid utterance as an OpenVoice reference sample |
53
- | `!voice-clone status` / `!voice-clone cancel` | Inspect or cancel capture |
54
- | `!ask <prompt>` | Send text through the same harness adapter as voice |
55
- | `!session status` | Show current project/default adapter session |
56
- | `!session new <name> <workdir> [context] --voice <voice-channel>` | Create a project-scoped Hermes session |
57
- | `!session attach-voice [sessionName] --voice <voice-channel>` | Bind a text channel/thread to a voice channel |
58
- | `!session list` | List configured project sessions |
59
- | `!session reset` / `!reset-session` | Clear the current session file |
60
- | `!verbose on/off` | Toggle detailed progress updates |
61
- | `!latency` / `!metrics` | Show recent latency summary |
62
- | `!sensitivity normal/conservative` | Switch barge-in sensitivity |
63
-
64
- Voice equivalents such as “외부 모드”, “보수 모드”, “실내”, “기본 감도”, “상세 진행 켜”, and clear stop phrases like “잠깐”, “멈춰”, “그만” are handled by the bridge.
65
-
66
- ## Changing the Voice
67
-
68
- `vc language ko|en|auto` changes STT language, progress language, and the matching default TTS voice together. Live voice commands can change the speaker without restart:
55
+ | `!ping` | 基本机器人检查 |
56
+ | `!join` / `!leave` | 加入或离开语音 |
57
+ | `!say <text>` | 直接通过 TTS 朗读文本 |
58
+ | `!voice-test <text>` | 测试当前 TTS 后端/声音 |
59
+ | `!voice-clone capture` | 将下一个有效发言保存为 OpenVoice 参考样本 |
60
+ | `!voice-clone status` / `!voice-clone cancel` | 查看或取消采集 |
61
+ | `!ask <prompt>` | 通过与语音相同的已选择驱动适配器发送文本 |
62
+ | `!session status` | 显示当前项目/默认适配器会话 |
63
+ | `!session new <name> <workdir> [context] --voice <voice-channel>` | 创建项目范围的 Hermes 会话 |
64
+ | `!session attach-voice [sessionName] --voice <voice-channel>` | 将文本频道/thread 绑定到语音频道 |
65
+ | `!session list` | 列出已配置的项目会话 |
66
+ | `!session reset` / `!reset-session` | 清除当前项目/默认适配器会话文件 |
67
+ | `!verbose on/off` | 切换详细进度更新 |
68
+ | `!latency` / `!metrics` | 显示最近延迟摘要 |
69
+ | `!sensitivity normal/conservative` | 切换插话灵敏度 |
70
+
71
+ 桥接会处理诸如“외부 모드”、“보수 모드”、“실내”、“기본 감도”等语音等价命令,以及“잠깐”、“멈춰”、“그만”等明确停止短语。你也可以说“상세 진행 켜” / “상세 진행 꺼”来通过语音切换详细进度。
72
+
73
+ ## 更改声音
74
+
75
+ `vc language ko|en|auto` 会同时更改 STT 语言、进度语言和匹配的默认 TTS 声音。如果只想在桥接运行时更换说话人/声音,请在 Discord 语音中说:
69
76
 
70
77
  ```text
71
78
  남자 한국어 목소리로 바꿔
@@ -74,9 +81,11 @@ change voice to Korean female
74
81
  switch speaker to English
75
82
  ```
76
83
 
77
- Built-in Edge types:
84
+ 实时桥接会将这些识别为语音控制命令,更新 `config/tts-voices.json`,更新运行进程的有效 TTS env,并用类似“목소리를 Korean male로 바꿨어.”的简短确认作答。更改后立即使用 `!voice-test <text>` 来听当前后端和声音。
85
+
86
+ 内置 Edge 声音类型:
78
87
 
79
- | Voice type | Edge voice |
88
+ | 声音类型 | Edge 声音 |
80
89
  |---|---|
81
90
  | `korean_male` | `ko-KR-InJoonNeural` |
82
91
  | `korean_female` | `ko-KR-SunHiNeural` |
@@ -84,26 +93,32 @@ Built-in Edge types:
84
93
  | `english_male` | `en-US-GuyNeural` |
85
94
  | `english_female` | `en-US-AriaNeural` |
86
95
 
87
- Backend voice settings:
96
+ 对于持久手动配置,请在 `.env` 中设置 `TTS_BACKEND=edge`、`TTS_VOICE_TYPE=<voice-type>`,并可选设置 `TTS_VOICE=<edge-voice>`;也可以编辑 `config/tts-voices.json` 以使用自定义声音目录。
88
97
 
89
- | Backend | Voice setting | Common choices |
98
+ 后端专用声音旋钮:
99
+
100
+ | 后端 | 声音设置 | 常见选择 |
90
101
  |---|---|---|
91
- | Edge | `TTS_VOICE_TYPE`, `TTS_VOICE` | Built-in types or any Edge voice from `edge-tts --list-voices` |
92
- | Supertonic | `SUPERTONIC_VOICE` | `M1`–`M5`, `F1`–`F5`; `SUPERTONIC_LANGUAGE=ko|en|es|pt|fr` |
93
- | OpenVoice | `OPENVOICE_REF_AUDIO`, `OPENVOICE_STYLE` | A permitted reference WAV plus style such as `default` |
94
- | SpeechSwift / CosyVoice | `SPEECHSWIFT_REF_AUDIO`, `SPEECHSWIFT_ENGINE`, `SPEECHSWIFT_SPEAKER` | Reference WAV or backend speaker/model values |
102
+ | Edge | `TTS_VOICE_TYPE`, `TTS_VOICE` | `korean_male`, `korean_female`, `korean_multilingual_male`, `english_male`, `english_female`;来自 `edge-tts --list-voices` 的任何 Edge 声音 |
103
+ | Supertonic | `SUPERTONIC_VOICE` | `M1`–`M5`, `F1`–`F5`;设置 `SUPERTONIC_LANGUAGE=ko|en|es|pt|fr` |
104
+ | OpenVoice | `OPENVOICE_REF_AUDIO`, `OPENVOICE_STYLE` | 获准使用的参考 WAV,以及如 `default` 的风格 |
105
+ | SpeechSwift / CosyVoice | `SPEECHSWIFT_REF_AUDIO`, `SPEECHSWIFT_ENGINE`, `SPEECHSWIFT_SPEAKER` | CosyVoice 的参考 WAV,或后端支持的说话人/模型值 |
106
+
107
+ 对于 Supertonic 和本地克隆后端,请使用上面的后端 env vars 加 `!voice-test <text>` 来试听更改。语音命令切换目前映射到内置 Edge 风格声音类型;更丰富的后端目录可以添加到 `config/tts-voices.json`。
95
108
 
96
- ## Long Dictation and Pauses
109
+ ## 长听写和停顿
97
110
 
98
- The default `UTTERANCE_IDLE_MS=4500` waits long enough to keep natural pauses inside one spoken instruction. Lower it for faster short commands or raise it for long dictation:
111
+ VerbalCoding 会等待一个空闲窗口后再把语音发送给 STT。默认 `UTTERANCE_IDLE_MS=4500` 有意稍微耐心一些,这样长指令中的自然停顿不会拆分句子、过早启动代理轮次,并把剩余语音当作处理期间的打断。
112
+
113
+ 如果你偏好更快的短命令,请在 `.env` 中降低它;如果长韩语听写仍被拆分,请提高它:
99
114
 
100
115
  ```bash
101
116
  UTTERANCE_IDLE_MS="6000"
102
117
  ```
103
118
 
104
- ## Verbose Progress Mode
119
+ ## 详细进度模式
105
120
 
106
- Enable with `!verbose on`, `AGENT_VERBOSE_PROGRESS=1`, or “상세 진행 켜”. Progress lines look like:
121
+ 除非设置了 `AGENT_VERBOSE_PROGRESS=1`,详细进度默认关闭。可用 `!verbose on` 或“상세 진행 켜”等语音命令启用。它可以输出如下简短进度行:
107
122
 
108
123
  ```text
109
124
  🤖 Hermes Agent 호출 시작
@@ -113,18 +128,28 @@ Enable with `!verbose on`, `AGENT_VERBOSE_PROGRESS=1`, or “상세 진행 켜
113
128
  🤖 Hermes Agent 응답 수신
114
129
  ```
115
130
 
116
- Secret-looking fields are redacted and progress lines are removed from final spoken answers.
131
+ 该模式会要求所选 CLI 驱动输出 `VERBALCODING_PROGRESS: ...` 行,并在可用时汇总流式 stdout/stderr 中的常见工具标记。看起来像密钥的字段会被脱敏,进度行会从最终朗读答案中移除。
132
+
133
+ ## 延迟指标
134
+
135
+ VerbalCoding 会按轮次将延迟记录写为 JSONL。默认路径:
136
+
137
+ ```text
138
+ ./.logs/latency.jsonl
139
+ ```
117
140
 
118
- ## Latency Metrics
141
+ 每条记录包含状态、总耗时、语音采集时间、发言空闲等待、STT 时间、代理时间、TTS 合成/播放时间、分块数量、转写长度、答案长度,以及可用时的音频电平。
119
142
 
120
- Latency records are written to `./.logs/latency.jsonl`. In Discord, run:
143
+ Discord 中:
121
144
 
122
145
  ```text
123
146
  !latency
124
147
  !metrics
125
148
  ```
126
149
 
127
- ## Testing
150
+ 摘要使用最新 200 条记录:数量、平均值、p95、最大值和非 OK 状态。
151
+
152
+ ## 测试
128
153
 
129
154
  ```bash
130
155
  node --check app-node/main.mjs
@@ -132,3 +157,5 @@ npm test
132
157
  bash -n run.sh scripts/install.sh
133
158
  vc doctor
134
159
  ```
160
+
161
+ `vc doctor` 会有意脱敏密钥,并只报告必需值是否已配置。它还会检查 `instances/*.env` 中是否存在重复令牌指纹和冲突的运行时路径。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verbalcoding",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Discord voice bridge for CLI coding agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -79,13 +79,25 @@ install_system_linux_packages() {
79
79
  if has_cmd apt-get; then
80
80
  log 'Installing Linux packages with apt-get'
81
81
  run_sudo apt-get update
82
- run_sudo apt-get install -y curl ca-certificates git python3 python3-venv python3-pip build-essential cmake pkg-config ffmpeg nodejs npm
82
+ local packages=(curl ca-certificates git python3 python3-venv python3-pip build-essential cmake pkg-config ffmpeg)
83
+ # npm installs often run under NodeSource Node.js, whose `nodejs` package conflicts
84
+ # with Ubuntu's separate `npm` package. Do not request nodejs/npm again when the
85
+ # current npm-based installer is already running with working node and npm.
86
+ has_cmd node || packages+=(nodejs)
87
+ has_cmd npm || packages+=(npm)
88
+ run_sudo apt-get install -y "${packages[@]}"
83
89
  elif has_cmd dnf; then
84
90
  log 'Installing Linux packages with dnf'
85
- run_sudo dnf install -y curl ca-certificates git python3 python3-pip gcc gcc-c++ make cmake pkgconf-pkg-config ffmpeg nodejs npm
91
+ local packages=(curl ca-certificates git python3 python3-pip gcc gcc-c++ make cmake pkgconf-pkg-config ffmpeg)
92
+ has_cmd node || packages+=(nodejs)
93
+ has_cmd npm || packages+=(npm)
94
+ run_sudo dnf install -y "${packages[@]}"
86
95
  elif has_cmd pacman; then
87
96
  log 'Installing Linux packages with pacman'
88
- run_sudo pacman -Sy --needed --noconfirm curl ca-certificates git python python-pip base-devel cmake pkgconf ffmpeg nodejs npm
97
+ local packages=(curl ca-certificates git python python-pip base-devel cmake pkgconf ffmpeg)
98
+ has_cmd node || packages+=(nodejs)
99
+ has_cmd npm || packages+=(npm)
100
+ run_sudo pacman -Sy --needed --noconfirm "${packages[@]}"
89
101
  else
90
102
  warn 'No supported Linux package manager found. Install node/npm, ffmpeg, python3, git, cmake, and a C++ toolchain manually.'
91
103
  return 1
package/scripts/cli.mjs CHANGED
@@ -292,7 +292,7 @@ async function main(argv = process.argv.slice(2)) {
292
292
  }
293
293
  if (command === 'doctor') {
294
294
  const { spawnSync } = await import('node:child_process');
295
- const result = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'doctor.mjs')], { stdio: 'inherit', cwd: ROOT });
295
+ const result = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'doctor.mjs'), ...argv.slice(1)], { stdio: 'inherit', cwd: ROOT });
296
296
  process.exitCode = result.status ?? 1;
297
297
  return;
298
298
  }
@@ -7,6 +7,9 @@ import { checkInstanceConfigs, formatInstanceDoctor } from '../app-node/instance
7
7
  import { autoRestartVoiceBotEnabled } from '../app-node/restart_policy.mjs';
8
8
 
9
9
  const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
10
+ const args = process.argv.slice(2);
11
+ const autoFixEnabled = !args.includes('--no-fix') && !['0', 'false', 'no', 'off'].includes(String(process.env.VERBALCODING_DOCTOR_AUTO_FIX || '1').toLowerCase());
12
+ let autoFixAttempted = false;
10
13
 
11
14
  function readEnvFile(file) {
12
15
  try {
@@ -25,14 +28,56 @@ function mergeEnv() {
25
28
  };
26
29
  }
27
30
 
31
+ function quoteEnv(value) {
32
+ return JSON.stringify(String(value ?? ''));
33
+ }
34
+
35
+ function upsertEnvFile(file, updates) {
36
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
37
+ const seen = new Set();
38
+ const lines = existing.split(/\r?\n/).map(raw => {
39
+ const line = raw.trim();
40
+ if (!line || line.startsWith('#') || !line.includes('=')) return raw;
41
+ const idx = line.indexOf('=');
42
+ const key = line.slice(0, idx).trim().replace(/^export\s+/, '');
43
+ if (!(key in updates)) return raw;
44
+ seen.add(key);
45
+ return `${key}=${quoteEnv(updates[key])}`;
46
+ });
47
+ for (const [key, value] of Object.entries(updates)) {
48
+ if (!seen.has(key)) lines.push(`${key}=${quoteEnv(value)}`);
49
+ }
50
+ const text = `${lines.filter((line, index, arr) => line !== '' || index < arr.length - 1).join('\n')}\n`;
51
+ fs.writeFileSync(file, text, { mode: 0o600 });
52
+ }
53
+
54
+ function isExecutable(file) {
55
+ try {
56
+ fs.accessSync(file, fs.constants.X_OK);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
28
63
  function commandExists(command) {
29
64
  const result = spawnSync('bash', ['-lc', `command -v ${JSON.stringify(command)}`], {
30
65
  cwd: ROOT,
31
66
  encoding: 'utf8',
67
+ env: { ...process.env, PATH: `${path.join(ROOT, '.local', 'bin')}:${process.env.PATH || ''}` },
32
68
  });
33
69
  return result.status === 0 ? result.stdout.trim() : '';
34
70
  }
35
71
 
72
+ function resolveCommand(command, fallbackPaths = []) {
73
+ const found = commandExists(command);
74
+ if (found) return found;
75
+ for (const candidate of fallbackPaths) {
76
+ if (isExecutable(candidate)) return candidate;
77
+ }
78
+ return '';
79
+ }
80
+
36
81
  function check(label, ok, detail = '') {
37
82
  const mark = ok ? '✓' : '✗';
38
83
  console.log(`${mark} ${label}${detail ? ` — ${detail}` : ''}`);
@@ -43,7 +88,60 @@ function note(label, detail = '') {
43
88
  console.log(`• ${label}${detail ? ` — ${detail}` : ''}`);
44
89
  }
45
90
 
46
- const env = mergeEnv();
91
+ function fixablePrerequisites(env) {
92
+ const ttsBackend = (env.TTS_BACKEND || 'edge').toLowerCase();
93
+ const missing = [];
94
+ if (!commandExists('ffmpeg')) missing.push('ffmpeg');
95
+ if (!resolveCommand(env.WHISPER_CPP_BIN || 'whisper-cli', [path.join(ROOT, '.local', 'bin', 'whisper-cli')])) missing.push('whisper-cli');
96
+ const modelPath = path.resolve(ROOT, env.WHISPER_CPP_MODEL || 'models/ggml-small-q5_1.bin');
97
+ if (!fs.existsSync(modelPath)) missing.push('whisper.cpp model');
98
+ if (ttsBackend === 'edge') {
99
+ const edgeCommand = env.EDGE_TTS_COMMAND || env.TTS_EDGE_COMMAND || 'edge-tts';
100
+ if (!resolveCommand(edgeCommand, [path.join(ROOT, '.venv-tts', 'bin', 'edge-tts')])) missing.push('edge-tts');
101
+ }
102
+ return missing;
103
+ }
104
+
105
+ function persistDiscoveredLocalHelpers(env) {
106
+ const updates = {};
107
+ const localWhisper = path.join(ROOT, '.local', 'bin', 'whisper-cli');
108
+ if (!commandExists(env.WHISPER_CPP_BIN || 'whisper-cli') && isExecutable(localWhisper)) {
109
+ updates.WHISPER_CPP_BIN = localWhisper;
110
+ }
111
+ const ttsBackend = (env.TTS_BACKEND || 'edge').toLowerCase();
112
+ const localEdge = path.join(ROOT, '.venv-tts', 'bin', 'edge-tts');
113
+ const edgeCommand = env.EDGE_TTS_COMMAND || env.TTS_EDGE_COMMAND || 'edge-tts';
114
+ if (ttsBackend === 'edge' && !commandExists(edgeCommand) && isExecutable(localEdge)) {
115
+ updates.EDGE_TTS_COMMAND = localEdge;
116
+ }
117
+ if (Object.keys(updates).length > 0) {
118
+ upsertEnvFile(path.join(ROOT, '.env'), updates);
119
+ return updates;
120
+ }
121
+ return {};
122
+ }
123
+
124
+ let env = mergeEnv();
125
+ const missingBeforeFix = fixablePrerequisites(env);
126
+ if (autoFixEnabled && missingBeforeFix.length > 0) {
127
+ console.log(`VerbalCoding doctor: missing ${missingBeforeFix.join(', ')}; running automatic prerequisite bootstrap...`);
128
+ const result = spawnSync('bash', [path.join(ROOT, 'scripts', 'bootstrap_prereqs.sh'), '--yes'], {
129
+ cwd: ROOT,
130
+ stdio: 'inherit',
131
+ env: { ...process.env, VERBALCODING_SKIP_CLI_LINK: process.env.VERBALCODING_SKIP_CLI_LINK || '1' },
132
+ });
133
+ autoFixAttempted = true;
134
+ const persisted = persistDiscoveredLocalHelpers(mergeEnv());
135
+ if (Object.keys(persisted).length > 0) {
136
+ console.log(`Doctor recorded local helper paths in .env: ${Object.keys(persisted).join(', ')}`);
137
+ }
138
+ if (result.status !== 0) {
139
+ console.log(`Doctor bootstrap exited with status ${result.status}. Continuing with checks.`);
140
+ }
141
+ console.log('');
142
+ env = mergeEnv();
143
+ }
144
+
47
145
  const backend = (env.AGENT_BACKEND || 'hermes').toLowerCase();
48
146
  const ttsBackend = (env.TTS_BACKEND || 'edge').toLowerCase();
49
147
  let ok = true;
@@ -52,12 +150,18 @@ console.log('VerbalCoding doctor');
52
150
  console.log(`Project: ${ROOT}`);
53
151
  console.log(`Backend: ${backend}`);
54
152
  console.log(`TTS backend: ${ttsBackend}`);
153
+ if (!autoFixEnabled) note('Automatic prerequisite bootstrap', 'off');
154
+ if (autoFixAttempted) note('Automatic prerequisite bootstrap', 'attempted');
55
155
  console.log('');
56
156
 
57
- ok = check('Node.js', commandExists('node'), commandExists('node') || 'missing') && ok;
58
- ok = check('npm', commandExists('npm'), commandExists('npm') || 'missing') && ok;
59
- ok = check('ffmpeg', commandExists('ffmpeg'), commandExists('ffmpeg') || 'missing') && ok;
60
- ok = check('whisper-cli', commandExists(env.WHISPER_CPP_BIN || 'whisper-cli'), commandExists(env.WHISPER_CPP_BIN || 'whisper-cli') || 'missing') && ok;
157
+ const nodeCommand = commandExists('node');
158
+ const npmCommand = commandExists('npm');
159
+ const ffmpegCommand = commandExists('ffmpeg');
160
+ const whisperCommand = resolveCommand(env.WHISPER_CPP_BIN || 'whisper-cli', [path.join(ROOT, '.local', 'bin', 'whisper-cli')]);
161
+ ok = check('Node.js', nodeCommand, nodeCommand || 'missing') && ok;
162
+ ok = check('npm', npmCommand, npmCommand || 'missing') && ok;
163
+ ok = check('ffmpeg', ffmpegCommand, ffmpegCommand || 'missing') && ok;
164
+ ok = check('whisper-cli', whisperCommand, whisperCommand || 'missing') && ok;
61
165
 
62
166
  const modelPath = path.resolve(ROOT, env.WHISPER_CPP_MODEL || 'models/ggml-small-q5_1.bin');
63
167
  ok = check('whisper.cpp model', fs.existsSync(modelPath), path.relative(ROOT, modelPath)) && ok;
@@ -77,7 +181,8 @@ if (!['edge', 'openvoice', 'speechswift', 'supertonic'].includes(ttsBackend)) {
77
181
  }
78
182
  if (ttsBackend === 'edge') {
79
183
  const edgeCommand = env.EDGE_TTS_COMMAND || env.TTS_EDGE_COMMAND || 'edge-tts';
80
- ok = check('edge-tts', commandExists(edgeCommand), commandExists(edgeCommand) || 'missing') && ok;
184
+ const edgeFound = resolveCommand(edgeCommand, [path.join(ROOT, '.venv-tts', 'bin', 'edge-tts')]);
185
+ ok = check('edge-tts', edgeFound, edgeFound || 'missing') && ok;
81
186
  } else if (ttsBackend === 'openvoice') {
82
187
  ok = check('Python for OpenVoice', commandExists('python3'), commandExists('python3') || 'missing') && ok;
83
188
  const openvoiceDir = path.resolve(ROOT, env.OPENVOICE_DIR || './vendor/OpenVoice');
@@ -127,8 +232,9 @@ ok = instanceResult.errors.length === 0 && ok;
127
232
 
128
233
  console.log('');
129
234
  if (ok) {
130
- console.log('Doctor passed. Run ./run.sh to start VerbalCoding.');
235
+ console.log('Doctor passed. Run vc start to start VerbalCoding.');
131
236
  } else {
132
- console.log('Doctor found missing prerequisites. Fix the ✗ items, then rerun npm run doctor.');
237
+ const suffix = autoFixEnabled ? 'Fix the remaining ✗ items, then rerun vc doctor.' : 'Fix the ✗ items, then rerun vc doctor.';
238
+ console.log(`Doctor found missing prerequisites. ${suffix}`);
133
239
  process.exitCode = 1;
134
240
  }