verbalcoding 0.2.9 → 0.2.11

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.
@@ -30,7 +30,10 @@ 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\]/);
34
+ assert.match(cli, /vc setup channels \[voice-channel/);
33
35
  assert.match(cli, /command === 'setup'/);
36
+ assert.match(cli, /install\.mjs'\), \.\.\.argv\.slice\(1\)/);
34
37
  assert.match(cli, /VERBALCODING_SKIP_CLI_LINK/);
35
38
  assert.match(cli, /command === 'start'/);
36
39
  assert.match(cli, /run\.sh/);
@@ -53,8 +56,15 @@ test('npm setup supports non-interactive --yes mode', () => {
53
56
  const config = fs.readFileSync(path.join(ROOT, 'app-node', 'install_config.mjs'), 'utf8');
54
57
 
55
58
  assert.match(installer, /args\.includes\('--yes'\)/);
59
+ assert.match(installer, /configureDiscordToken/);
60
+ assert.match(installer, /configureAutoJoinChannels/);
61
+ assert.match(installer, /DISCORD_BOT_TOKEN: token/);
62
+ assert.match(installer, /AUTO_JOIN_VOICE_CHANNELS: channels/);
63
+ assert.match(installer, /vc setup token/);
64
+ assert.match(installer, /vc setup channels/);
56
65
  assert.match(installer, /normalizeInstallAnswers\(process\.env\)/);
57
66
  assert.match(config, /vc start/);
67
+ assert.match(config, /vc setup channels/);
58
68
  assert.doesNotMatch(config, /npm install -g \.\s+#/);
59
69
  });
60
70
 
@@ -86,6 +96,7 @@ test('doctor auto-bootstraps fixable prerequisites by default', () => {
86
96
  assert.match(doctor, /NousResearch\/hermes-agent\/main\/scripts\/install\.sh/);
87
97
  assert.match(doctor, /VERBALCODING_DOCTOR_INSTALL_HERMES/);
88
98
  assert.match(doctor, /Discord bot setup:/);
99
+ assert.match(doctor, /vc setup token/);
89
100
  assert.match(doctor, /discord\.com\/developers\/applications/);
90
101
  assert.match(cli, /doctor\.mjs'\), \.\.\.argv\.slice\(1\)/);
91
102
  });
@@ -99,6 +110,7 @@ test('setup summary guides Discord app creation and records client id', () => {
99
110
  assert.match(config, /Discord app setup:/);
100
111
  assert.match(config, /https:\/\/discord\.com\/developers\/applications/);
101
112
  assert.match(config, /vc bot invite <client-id>/);
113
+ assert.match(config, /vc setup token/);
102
114
  assert.match(config, /buildDiscordBotInviteUrl\(\{ clientId: values\.DISCORD_CLIENT_ID \}\)/);
103
115
  });
104
116
 
@@ -252,12 +252,15 @@ export function renderInstallSummary(values = {}) {
252
252
  'Discord app setup:',
253
253
  ' 1. Create an app: https://discord.com/developers/applications',
254
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="...".',
255
+ ' 3. Register the token with `vc setup token` (or `vc setup token <token>`).',
256
256
  inviteUrl ? ` 4. Invite URL: ${inviteUrl}` : ' 4. Invite URL: vc bot invite <client-id>',
257
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.',
258
259
  '',
259
260
  'Next commands:',
260
261
  ' vc doctor',
262
+ ' vc setup token # register/update Discord bot token when ready',
263
+ ' vc setup channels # set auto-join voice channel names',
261
264
  ' vc start',
262
265
  '',
263
266
  'Legacy project-local equivalents still work:',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verbalcoding",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Discord voice bridge for CLI coding agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/scripts/cli.mjs CHANGED
@@ -31,6 +31,8 @@ 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>]
35
+ vc setup channels [voice-channel[,voice-channel...]]
34
36
  vc start
35
37
  vc status
36
38
  vc language <ko|en|auto>
@@ -48,6 +50,8 @@ Usage:
48
50
  Examples:
49
51
  npx verbalcoding setup --yes
50
52
  vc setup --yes
53
+ vc setup token
54
+ vc setup channels "General,Team Voice"
51
55
  vc start
52
56
  vc language en
53
57
  vc language ko
@@ -275,6 +279,11 @@ async function main(argv = process.argv.slice(2)) {
275
279
  }
276
280
  if (command === 'setup' || command === 'install') {
277
281
  const { spawnSync } = await import('node:child_process');
282
+ if (['token', 'discord', 'bot-token', 'channels', 'channel', 'voice'].includes(argv[1])) {
283
+ const result = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'install.mjs'), ...argv.slice(1)], { stdio: 'inherit', cwd: ROOT });
284
+ process.exitCode = result.status ?? 1;
285
+ return;
286
+ }
278
287
  const script = path.join(ROOT, 'scripts', 'install.sh');
279
288
  const result = spawnSync('bash', [script, ...argv.slice(1)], {
280
289
  stdio: 'inherit',
@@ -141,7 +141,7 @@ function discordSetupGuidance(env) {
141
141
  'Discord bot setup:',
142
142
  ' 1. Open https://discord.com/developers/applications and create an application.',
143
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="...".`,
144
+ ` 3. Register it with \`vc setup token\` (or \`vc setup token <token>\`).`,
145
145
  ' 4. OAuth2 tab: copy the Application/Client ID.',
146
146
  clientId
147
147
  ? ` 5. Invite the bot: vc bot invite ${clientId}`
@@ -15,8 +15,102 @@ 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
+
79
+ async function configureAutoJoinChannels(args) {
80
+ const envPath = path.join(ROOT, '.env');
81
+ let channels = args.find((arg, idx) => idx > 0 && !arg.startsWith('--')) || argValue(args, '--channels') || argValue(args, '--voice');
82
+ if (!channels) {
83
+ globalThis.__rl = readline.createInterface({ input, output });
84
+ try {
85
+ console.log('Discord auto-join voice channel setup');
86
+ console.log('Enter one or more voice channel names, comma-separated. Example: General,Team Voice,일반');
87
+ channels = await ask('Auto-join voice channel names', process.env.AUTO_JOIN_VOICE_CHANNELS || 'General,general');
88
+ } finally {
89
+ globalThis.__rl.close();
90
+ globalThis.__rl = null;
91
+ }
92
+ }
93
+ if (!channels) {
94
+ console.error('No voice channel names provided. Nothing changed.');
95
+ process.exitCode = 2;
96
+ return;
97
+ }
98
+ upsertEnvFile(envPath, { AUTO_JOIN_VOICE_CHANNELS: channels });
99
+ console.log(`Updated ${envPath}`);
100
+ console.log(`Auto-join voice channels: ${channels}`);
101
+ console.log('Restart the bridge for this to take effect. You can update it anytime with `vc setup channels`.');
102
+ }
103
+
18
104
  async function main() {
19
105
  const args = process.argv.slice(2);
106
+ if (args[0] === 'token' || args[0] === 'discord' || args[0] === 'bot-token') {
107
+ await configureDiscordToken(args);
108
+ return;
109
+ }
110
+ if (args[0] === 'channels' || args[0] === 'channel' || args[0] === 'voice') {
111
+ await configureAutoJoinChannels(args);
112
+ return;
113
+ }
20
114
  const yes = args.includes('--yes') || args.includes('-y');
21
115
  if (args[0] === 'instance' || args.includes('--instance')) {
22
116
  const { spawnSync } = await import('node:child_process');