verbalcoding 0.2.6 → 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 -22
  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 +239 -0
  6. package/docs/i18n/CONFIGURATION.fr.md +239 -0
  7. package/docs/i18n/CONFIGURATION.ja.md +239 -0
  8. package/docs/i18n/CONFIGURATION.ko.md +66 -74
  9. package/docs/i18n/CONFIGURATION.ru.md +239 -0
  10. package/docs/i18n/CONFIGURATION.zh.md +239 -0
  11. package/docs/i18n/FRESH_INSTALL.es.md +207 -0
  12. package/docs/i18n/FRESH_INSTALL.fr.md +207 -0
  13. package/docs/i18n/FRESH_INSTALL.ja.md +207 -0
  14. package/docs/i18n/FRESH_INSTALL.ko.md +60 -54
  15. package/docs/i18n/FRESH_INSTALL.ru.md +207 -0
  16. package/docs/i18n/FRESH_INSTALL.zh.md +207 -0
  17. package/docs/i18n/MULTI_INSTANCE.es.md +180 -0
  18. package/docs/i18n/MULTI_INSTANCE.fr.md +180 -0
  19. package/docs/i18n/MULTI_INSTANCE.ja.md +179 -0
  20. package/docs/i18n/MULTI_INSTANCE.ko.md +46 -46
  21. package/docs/i18n/MULTI_INSTANCE.ru.md +179 -0
  22. package/docs/i18n/MULTI_INSTANCE.zh.md +179 -0
  23. package/docs/i18n/README.es.md +83 -55
  24. package/docs/i18n/README.fr.md +85 -57
  25. package/docs/i18n/README.ja.md +83 -55
  26. package/docs/i18n/README.ko.md +47 -56
  27. package/docs/i18n/README.ru.md +86 -58
  28. package/docs/i18n/README.zh.md +83 -56
  29. package/docs/i18n/RELEASE.es.md +74 -0
  30. package/docs/i18n/RELEASE.fr.md +74 -0
  31. package/docs/i18n/RELEASE.ja.md +74 -0
  32. package/docs/i18n/RELEASE.ko.md +38 -36
  33. package/docs/i18n/RELEASE.ru.md +74 -0
  34. package/docs/i18n/RELEASE.zh.md +74 -0
  35. package/docs/i18n/USAGE.es.md +161 -0
  36. package/docs/i18n/USAGE.fr.md +161 -0
  37. package/docs/i18n/USAGE.ja.md +161 -0
  38. package/docs/i18n/USAGE.ko.md +61 -72
  39. package/docs/i18n/USAGE.ru.md +161 -0
  40. package/docs/i18n/USAGE.zh.md +161 -0
  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
@@ -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
  }