verbalcoding 0.2.8 → 0.2.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.
@@ -82,9 +82,26 @@ test('doctor auto-bootstraps fixable prerequisites by default', () => {
82
82
  assert.match(doctor, /--no-fix/);
83
83
  assert.match(doctor, /WHISPER_CPP_BIN/);
84
84
  assert.match(doctor, /EDGE_TTS_COMMAND/);
85
+ assert.match(doctor, /installHermesCliIfNeeded/);
86
+ assert.match(doctor, /NousResearch\/hermes-agent\/main\/scripts\/install\.sh/);
87
+ assert.match(doctor, /VERBALCODING_DOCTOR_INSTALL_HERMES/);
88
+ assert.match(doctor, /Discord bot setup:/);
89
+ assert.match(doctor, /discord\.com\/developers\/applications/);
85
90
  assert.match(cli, /doctor\.mjs'\), \.\.\.argv\.slice\(1\)/);
86
91
  });
87
92
 
93
+ test('setup summary guides Discord app creation and records client id', () => {
94
+ const installer = fs.readFileSync(path.join(ROOT, 'scripts', 'install.mjs'), 'utf8');
95
+ const config = fs.readFileSync(path.join(ROOT, 'app-node', 'install_config.mjs'), 'utf8');
96
+
97
+ assert.match(installer, /Discord application\/client ID for invite URL/);
98
+ assert.match(config, /DISCORD_CLIENT_ID/);
99
+ assert.match(config, /Discord app setup:/);
100
+ assert.match(config, /https:\/\/discord\.com\/developers\/applications/);
101
+ assert.match(config, /vc bot invite <client-id>/);
102
+ assert.match(config, /buildDiscordBotInviteUrl\(\{ clientId: values\.DISCORD_CLIENT_ID \}\)/);
103
+ });
104
+
88
105
  test('Ubuntu Docker smoke script validates clean install without secrets', () => {
89
106
  const script = fs.readFileSync(path.join(ROOT, 'scripts', 'docker_ubuntu_smoke.sh'), 'utf8');
90
107
 
@@ -26,6 +26,7 @@ export function normalizeInstallAnswers(input = {}) {
26
26
  const out = {
27
27
  AGENT_BACKEND: normalizedHarness,
28
28
  DISCORD_BOT_TOKEN: clean(input.discordBotToken || input.DISCORD_BOT_TOKEN),
29
+ DISCORD_CLIENT_ID: clean(input.discordClientId || input.DISCORD_CLIENT_ID || input.applicationId || input.APPLICATION_ID),
29
30
  DISCORD_ALLOWED_USERS: clean(input.allowedUsers || input.DISCORD_ALLOWED_USERS),
30
31
  AUTO_JOIN_VOICE_CHANNELS: clean(input.autoJoinVoiceChannels || input.AUTO_JOIN_VOICE_CHANNELS, '일반,General,general'),
31
32
  TRANSCRIPT_CHANNEL_ID: clean(input.transcriptChannelId || input.TRANSCRIPT_CHANNEL_ID),
@@ -101,6 +102,7 @@ export function slugifyInstanceName(name) {
101
102
  export function buildEnvFile(values = {}) {
102
103
  const ordered = [
103
104
  'DISCORD_BOT_TOKEN',
105
+ 'DISCORD_CLIENT_ID',
104
106
  'DISCORD_ALLOWED_USERS',
105
107
  'AUTO_JOIN_VOICE_CHANNELS',
106
108
  'TRANSCRIPT_CHANNEL_ID',
@@ -243,9 +245,17 @@ export function parseKeyValueEnv(text) {
243
245
 
244
246
  export function renderInstallSummary(values = {}) {
245
247
  const backend = values.AGENT_BACKEND || 'hermes';
248
+ const inviteUrl = values.DISCORD_CLIENT_ID ? buildDiscordBotInviteUrl({ clientId: values.DISCORD_CLIENT_ID }) : '';
246
249
  return [
247
250
  `Configured Discord voice bridge for harness: ${backend}`,
248
251
  '',
252
+ 'Discord app setup:',
253
+ ' 1. Create an app: https://discord.com/developers/applications',
254
+ ' 2. Bot tab: Add Bot, enable Message Content Intent, copy/reset the token.',
255
+ ' 3. Put the token in .env as DISCORD_BOT_TOKEN="...".',
256
+ inviteUrl ? ` 4. Invite URL: ${inviteUrl}` : ' 4. Invite URL: vc bot invite <client-id>',
257
+ ' 5. Make sure the bot can read/send text and connect/speak in voice.',
258
+ '',
249
259
  'Next commands:',
250
260
  ' vc doctor',
251
261
  ' vc start',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verbalcoding",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Discord voice bridge for CLI coding agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -61,10 +61,14 @@ function isExecutable(file) {
61
61
  }
62
62
 
63
63
  function commandExists(command) {
64
+ const extraPath = [
65
+ path.join(ROOT, '.local', 'bin'),
66
+ path.join(process.env.HOME || '', '.local', 'bin'),
67
+ ].filter(Boolean).join(':');
64
68
  const result = spawnSync('bash', ['-lc', `command -v ${JSON.stringify(command)}`], {
65
69
  cwd: ROOT,
66
70
  encoding: 'utf8',
67
- env: { ...process.env, PATH: `${path.join(ROOT, '.local', 'bin')}:${process.env.PATH || ''}` },
71
+ env: { ...process.env, PATH: `${extraPath}:${process.env.PATH || ''}` },
68
72
  });
69
73
  return result.status === 0 ? result.stdout.trim() : '';
70
74
  }
@@ -90,6 +94,7 @@ function note(label, detail = '') {
90
94
 
91
95
  function fixablePrerequisites(env) {
92
96
  const ttsBackend = (env.TTS_BACKEND || 'edge').toLowerCase();
97
+ const backend = (env.AGENT_BACKEND || 'hermes').toLowerCase();
93
98
  const missing = [];
94
99
  if (!commandExists('ffmpeg')) missing.push('ffmpeg');
95
100
  if (!resolveCommand(env.WHISPER_CPP_BIN || 'whisper-cli', [path.join(ROOT, '.local', 'bin', 'whisper-cli')])) missing.push('whisper-cli');
@@ -99,9 +104,53 @@ function fixablePrerequisites(env) {
99
104
  const edgeCommand = env.EDGE_TTS_COMMAND || env.TTS_EDGE_COMMAND || 'edge-tts';
100
105
  if (!resolveCommand(edgeCommand, [path.join(ROOT, '.venv-tts', 'bin', 'edge-tts')])) missing.push('edge-tts');
101
106
  }
107
+ if (backend === 'hermes' && !commandExists('hermes')) missing.push('hermes CLI');
102
108
  return missing;
103
109
  }
104
110
 
111
+ function installHermesCliIfNeeded(env) {
112
+ const backend = (env.AGENT_BACKEND || 'hermes').toLowerCase();
113
+ if (backend !== 'hermes' || commandExists('hermes')) return false;
114
+ if (process.platform === 'win32') {
115
+ console.log('Skipping Hermes CLI auto-install: Windows is not supported by VerbalCoding yet.');
116
+ return false;
117
+ }
118
+ if (['0', 'false', 'no', 'off'].includes(String(process.env.VERBALCODING_DOCTOR_INSTALL_HERMES || '1').toLowerCase())) {
119
+ console.log('Skipping Hermes CLI auto-install because VERBALCODING_DOCTOR_INSTALL_HERMES is off.');
120
+ return false;
121
+ }
122
+ if (!commandExists('curl')) {
123
+ console.log('Skipping Hermes CLI auto-install: curl is missing.');
124
+ return false;
125
+ }
126
+ console.log('VerbalCoding doctor: missing hermes CLI; installing Hermes Agent...');
127
+ const result = spawnSync('bash', ['-lc', 'curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash'], {
128
+ cwd: ROOT,
129
+ stdio: 'inherit',
130
+ env: process.env,
131
+ });
132
+ if (result.status !== 0) {
133
+ console.log(`Hermes installer exited with status ${result.status}. Continuing with checks.`);
134
+ }
135
+ return true;
136
+ }
137
+
138
+ function discordSetupGuidance(env) {
139
+ const clientId = env.DISCORD_CLIENT_ID || env.APPLICATION_ID || '';
140
+ const lines = [
141
+ 'Discord bot setup:',
142
+ ' 1. Open https://discord.com/developers/applications and create an application.',
143
+ ' 2. Bot tab: Add Bot, enable Message Content Intent, then Reset/Copy Token.',
144
+ ` 3. Save it in ${path.join(ROOT, '.env')} as DISCORD_BOT_TOKEN="...".`,
145
+ ' 4. OAuth2 tab: copy the Application/Client ID.',
146
+ clientId
147
+ ? ` 5. Invite the bot: vc bot invite ${clientId}`
148
+ : ' 5. Invite the bot: vc bot invite <client-id>',
149
+ ' 6. Give it text channel send/read plus voice connect/speak permissions, then rerun vc doctor.',
150
+ ];
151
+ return lines.join('\n');
152
+ }
153
+
105
154
  function persistDiscoveredLocalHelpers(env) {
106
155
  const updates = {};
107
156
  const localWhisper = path.join(ROOT, '.local', 'bin', 'whisper-cli');
@@ -141,6 +190,13 @@ if (autoFixEnabled && missingBeforeFix.length > 0) {
141
190
  console.log('');
142
191
  env = mergeEnv();
143
192
  }
193
+ if (autoFixEnabled) {
194
+ const hermesAttempted = installHermesCliIfNeeded(env);
195
+ if (hermesAttempted) {
196
+ console.log('');
197
+ env = mergeEnv();
198
+ }
199
+ }
144
200
 
145
201
  const backend = (env.AGENT_BACKEND || 'hermes').toLowerCase();
146
202
  const ttsBackend = (env.TTS_BACKEND || 'edge').toLowerCase();
@@ -166,6 +222,9 @@ ok = check('whisper-cli', whisperCommand, whisperCommand || 'missing') && ok;
166
222
  const modelPath = path.resolve(ROOT, env.WHISPER_CPP_MODEL || 'models/ggml-small-q5_1.bin');
167
223
  ok = check('whisper.cpp model', fs.existsSync(modelPath), path.relative(ROOT, modelPath)) && ok;
168
224
  ok = check('Discord bot token configured', Boolean(env.DISCORD_BOT_TOKEN || env.DISCORD_TOKEN), (env.DISCORD_BOT_TOKEN || env.DISCORD_TOKEN) ? '[REDACTED]' : 'missing DISCORD_BOT_TOKEN') && ok;
225
+ if (!(env.DISCORD_BOT_TOKEN || env.DISCORD_TOKEN)) {
226
+ console.log(discordSetupGuidance(env));
227
+ }
169
228
  note('Allowed users configured', env.DISCORD_ALLOWED_USERS ? '[REDACTED]' : 'not set; bot may accept all users depending on config');
170
229
  note('Auto-join channels', env.AUTO_JOIN_VOICE_CHANNELS || 'default: 일반,General,general');
171
230
  note('Verbose progress default', ['1', 'true', 'yes', 'on'].includes(String(env.AGENT_VERBOSE_PROGRESS || env.VERBALCODING_VERBOSE_PROGRESS || '0').toLowerCase()) ? 'on' : 'off');
@@ -53,6 +53,7 @@ async function main() {
53
53
  }
54
54
  const existingDiscordBotToken = process.env.DISCORD_BOT_TOKEN || '';
55
55
  const discordBotToken = await ask('Discord bot token (DISCORD_BOT_TOKEN)', existingDiscordBotToken, { fallbackLabel: existingDiscordBotToken ? 'keep existing' : '' });
56
+ const discordClientId = await ask('Discord application/client ID for invite URL', process.env.DISCORD_CLIENT_ID || '');
56
57
  const allowedUsers = await ask('Allowed Discord user IDs, comma-separated', process.env.DISCORD_ALLOWED_USERS || '');
57
58
  const autoJoinVoiceChannels = await ask('Auto-join voice channel names', process.env.AUTO_JOIN_VOICE_CHANNELS || '일반,General,general');
58
59
  const transcriptChannelId = await ask('Transcript text channel/thread ID', process.env.TRANSCRIPT_CHANNEL_ID || '');
@@ -80,6 +81,7 @@ async function main() {
80
81
  agentLabel,
81
82
  agentCommand,
82
83
  discordBotToken,
84
+ discordClientId,
83
85
  allowedUsers,
84
86
  autoJoinVoiceChannels,
85
87
  transcriptChannelId,