verbalcoding 0.2.8 → 0.2.10
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 +24 -0
- package/app-node/install_config.mjs +11 -0
- package/package.json +1 -1
- package/scripts/cli.mjs +7 -0
- package/scripts/doctor.mjs +60 -1
- package/scripts/install.mjs +67 -0
|
@@ -30,7 +30,9 @@ test('CLI includes npm-friendly setup and start commands', () => {
|
|
|
30
30
|
const cli = fs.readFileSync(path.join(ROOT, 'scripts', 'cli.mjs'), 'utf8');
|
|
31
31
|
|
|
32
32
|
assert.match(cli, /vc setup \[--yes\]/);
|
|
33
|
+
assert.match(cli, /vc setup token \[bot-token\]/);
|
|
33
34
|
assert.match(cli, /command === 'setup'/);
|
|
35
|
+
assert.match(cli, /install\.mjs'\), \.\.\.argv\.slice\(1\)/);
|
|
34
36
|
assert.match(cli, /VERBALCODING_SKIP_CLI_LINK/);
|
|
35
37
|
assert.match(cli, /command === 'start'/);
|
|
36
38
|
assert.match(cli, /run\.sh/);
|
|
@@ -53,6 +55,9 @@ test('npm setup supports non-interactive --yes mode', () => {
|
|
|
53
55
|
const config = fs.readFileSync(path.join(ROOT, 'app-node', 'install_config.mjs'), 'utf8');
|
|
54
56
|
|
|
55
57
|
assert.match(installer, /args\.includes\('--yes'\)/);
|
|
58
|
+
assert.match(installer, /configureDiscordToken/);
|
|
59
|
+
assert.match(installer, /DISCORD_BOT_TOKEN: token/);
|
|
60
|
+
assert.match(installer, /vc setup token/);
|
|
56
61
|
assert.match(installer, /normalizeInstallAnswers\(process\.env\)/);
|
|
57
62
|
assert.match(config, /vc start/);
|
|
58
63
|
assert.doesNotMatch(config, /npm install -g \.\s+#/);
|
|
@@ -82,9 +87,28 @@ test('doctor auto-bootstraps fixable prerequisites by default', () => {
|
|
|
82
87
|
assert.match(doctor, /--no-fix/);
|
|
83
88
|
assert.match(doctor, /WHISPER_CPP_BIN/);
|
|
84
89
|
assert.match(doctor, /EDGE_TTS_COMMAND/);
|
|
90
|
+
assert.match(doctor, /installHermesCliIfNeeded/);
|
|
91
|
+
assert.match(doctor, /NousResearch\/hermes-agent\/main\/scripts\/install\.sh/);
|
|
92
|
+
assert.match(doctor, /VERBALCODING_DOCTOR_INSTALL_HERMES/);
|
|
93
|
+
assert.match(doctor, /Discord bot setup:/);
|
|
94
|
+
assert.match(doctor, /vc setup token/);
|
|
95
|
+
assert.match(doctor, /discord\.com\/developers\/applications/);
|
|
85
96
|
assert.match(cli, /doctor\.mjs'\), \.\.\.argv\.slice\(1\)/);
|
|
86
97
|
});
|
|
87
98
|
|
|
99
|
+
test('setup summary guides Discord app creation and records client id', () => {
|
|
100
|
+
const installer = fs.readFileSync(path.join(ROOT, 'scripts', 'install.mjs'), 'utf8');
|
|
101
|
+
const config = fs.readFileSync(path.join(ROOT, 'app-node', 'install_config.mjs'), 'utf8');
|
|
102
|
+
|
|
103
|
+
assert.match(installer, /Discord application\/client ID for invite URL/);
|
|
104
|
+
assert.match(config, /DISCORD_CLIENT_ID/);
|
|
105
|
+
assert.match(config, /Discord app setup:/);
|
|
106
|
+
assert.match(config, /https:\/\/discord\.com\/developers\/applications/);
|
|
107
|
+
assert.match(config, /vc bot invite <client-id>/);
|
|
108
|
+
assert.match(config, /vc setup token/);
|
|
109
|
+
assert.match(config, /buildDiscordBotInviteUrl\(\{ clientId: values\.DISCORD_CLIENT_ID \}\)/);
|
|
110
|
+
});
|
|
111
|
+
|
|
88
112
|
test('Ubuntu Docker smoke script validates clean install without secrets', () => {
|
|
89
113
|
const script = fs.readFileSync(path.join(ROOT, 'scripts', 'docker_ubuntu_smoke.sh'), 'utf8');
|
|
90
114
|
|
|
@@ -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,18 @@ 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. Register the token with `vc setup token` (or `vc setup token <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
|
+
'You may skip the token now and run `vc setup token` anytime later.',
|
|
259
|
+
'',
|
|
249
260
|
'Next commands:',
|
|
250
261
|
' vc doctor',
|
|
251
262
|
' vc start',
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -31,6 +31,7 @@ function usage() {
|
|
|
31
31
|
|
|
32
32
|
Usage:
|
|
33
33
|
vc setup [--yes] [--no-wizard] [--skip-system] [--skip-model] [--skip-edge-tts]
|
|
34
|
+
vc setup token [bot-token] [--client-id <client-id>]
|
|
34
35
|
vc start
|
|
35
36
|
vc status
|
|
36
37
|
vc language <ko|en|auto>
|
|
@@ -48,6 +49,7 @@ Usage:
|
|
|
48
49
|
Examples:
|
|
49
50
|
npx verbalcoding setup --yes
|
|
50
51
|
vc setup --yes
|
|
52
|
+
vc setup token
|
|
51
53
|
vc start
|
|
52
54
|
vc language en
|
|
53
55
|
vc language ko
|
|
@@ -275,6 +277,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
275
277
|
}
|
|
276
278
|
if (command === 'setup' || command === 'install') {
|
|
277
279
|
const { spawnSync } = await import('node:child_process');
|
|
280
|
+
if (argv[1] === 'token' || argv[1] === 'discord' || argv[1] === 'bot-token') {
|
|
281
|
+
const result = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'install.mjs'), ...argv.slice(1)], { stdio: 'inherit', cwd: ROOT });
|
|
282
|
+
process.exitCode = result.status ?? 1;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
278
285
|
const script = path.join(ROOT, 'scripts', 'install.sh');
|
|
279
286
|
const result = spawnSync('bash', [script, ...argv.slice(1)], {
|
|
280
287
|
stdio: 'inherit',
|
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. Register it with \`vc setup token\` (or \`vc setup token <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
|
@@ -15,8 +15,73 @@ async function ask(question, fallback = '', options = {}) {
|
|
|
15
15
|
return answer || fallback;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function quoteEnv(value) {
|
|
19
|
+
return JSON.stringify(String(value ?? ''));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function upsertEnvFile(file, updates) {
|
|
23
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const lines = existing.split(/\r?\n/).map(raw => {
|
|
26
|
+
const line = raw.trim();
|
|
27
|
+
if (!line || line.startsWith('#') || !line.includes('=')) return raw;
|
|
28
|
+
const idx = line.indexOf('=');
|
|
29
|
+
const key = line.slice(0, idx).trim().replace(/^export\s+/, '');
|
|
30
|
+
if (!(key in updates)) return raw;
|
|
31
|
+
seen.add(key);
|
|
32
|
+
return `${key}=${quoteEnv(updates[key])}`;
|
|
33
|
+
});
|
|
34
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
35
|
+
if (!seen.has(key)) lines.push(`${key}=${quoteEnv(value)}`);
|
|
36
|
+
}
|
|
37
|
+
const text = `${lines.filter((line, index, arr) => line !== '' || index < arr.length - 1).join('\n')}\n`;
|
|
38
|
+
fs.writeFileSync(file, text, { mode: 0o600 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function argValue(args, name) {
|
|
42
|
+
const idx = args.indexOf(name);
|
|
43
|
+
if (idx < 0) return '';
|
|
44
|
+
const value = args[idx + 1] || '';
|
|
45
|
+
return value.startsWith('--') ? '' : value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function configureDiscordToken(args) {
|
|
49
|
+
const envPath = path.join(ROOT, '.env');
|
|
50
|
+
const tokenArg = args.find((arg, idx) => idx > 0 && !arg.startsWith('--')) || argValue(args, '--token');
|
|
51
|
+
const clientIdArg = argValue(args, '--client-id') || argValue(args, '--application-id');
|
|
52
|
+
let token = tokenArg || process.env.DISCORD_BOT_TOKEN || '';
|
|
53
|
+
let clientId = clientIdArg || process.env.DISCORD_CLIENT_ID || '';
|
|
54
|
+
if (!token) {
|
|
55
|
+
globalThis.__rl = readline.createInterface({ input, output });
|
|
56
|
+
try {
|
|
57
|
+
console.log('Discord bot token setup');
|
|
58
|
+
console.log('Create/copy the token at https://discord.com/developers/applications → your app → Bot.');
|
|
59
|
+
token = await ask('Discord bot token (DISCORD_BOT_TOKEN)', '', { fallbackLabel: '' });
|
|
60
|
+
clientId = await ask('Discord application/client ID for invite URL (optional)', clientId, { fallbackLabel: clientId ? 'keep existing' : 'skip' });
|
|
61
|
+
} finally {
|
|
62
|
+
globalThis.__rl.close();
|
|
63
|
+
globalThis.__rl = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!token) {
|
|
67
|
+
console.error('No Discord bot token provided. Nothing changed.');
|
|
68
|
+
process.exitCode = 2;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const updates = { DISCORD_BOT_TOKEN: token };
|
|
72
|
+
if (clientId) updates.DISCORD_CLIENT_ID = clientId;
|
|
73
|
+
upsertEnvFile(envPath, updates);
|
|
74
|
+
console.log(`Updated ${envPath}`);
|
|
75
|
+
console.log('Discord bot token saved. Run `vc doctor` to verify. You can update it anytime with `vc setup token`.');
|
|
76
|
+
if (clientId) console.log(`Invite URL: vc bot invite ${clientId}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
18
79
|
async function main() {
|
|
19
80
|
const args = process.argv.slice(2);
|
|
81
|
+
if (args[0] === 'token' || args[0] === 'discord' || args[0] === 'bot-token') {
|
|
82
|
+
await configureDiscordToken(args);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
20
85
|
const yes = args.includes('--yes') || args.includes('-y');
|
|
21
86
|
if (args[0] === 'instance' || args.includes('--instance')) {
|
|
22
87
|
const { spawnSync } = await import('node:child_process');
|
|
@@ -53,6 +118,7 @@ async function main() {
|
|
|
53
118
|
}
|
|
54
119
|
const existingDiscordBotToken = process.env.DISCORD_BOT_TOKEN || '';
|
|
55
120
|
const discordBotToken = await ask('Discord bot token (DISCORD_BOT_TOKEN)', existingDiscordBotToken, { fallbackLabel: existingDiscordBotToken ? 'keep existing' : '' });
|
|
121
|
+
const discordClientId = await ask('Discord application/client ID for invite URL', process.env.DISCORD_CLIENT_ID || '');
|
|
56
122
|
const allowedUsers = await ask('Allowed Discord user IDs, comma-separated', process.env.DISCORD_ALLOWED_USERS || '');
|
|
57
123
|
const autoJoinVoiceChannels = await ask('Auto-join voice channel names', process.env.AUTO_JOIN_VOICE_CHANNELS || '일반,General,general');
|
|
58
124
|
const transcriptChannelId = await ask('Transcript text channel/thread ID', process.env.TRANSCRIPT_CHANNEL_ID || '');
|
|
@@ -80,6 +146,7 @@ async function main() {
|
|
|
80
146
|
agentLabel,
|
|
81
147
|
agentCommand,
|
|
82
148
|
discordBotToken,
|
|
149
|
+
discordClientId,
|
|
83
150
|
allowedUsers,
|
|
84
151
|
autoJoinVoiceChannels,
|
|
85
152
|
transcriptChannelId,
|