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.
- package/app-node/cli_install.test.mjs +17 -0
- package/app-node/install_config.mjs +10 -0
- package/package.json +1 -1
- package/scripts/doctor.mjs +60 -1
- package/scripts/install.mjs +2 -0
|
@@ -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
package/scripts/doctor.mjs
CHANGED
|
@@ -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: `${
|
|
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');
|
package/scripts/install.mjs
CHANGED
|
@@ -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,
|