hermes-launch 1.1.0 → 2.0.0

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.
@@ -1,640 +1,196 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * hermes-launch — one command to install, configure, and run Hermes Agent.
3
+ * hermes-launch — starts the web onboarding wizard and opens your browser.
4
+ *
5
+ * The web server handles everything: install, config, tools, dashboard.
6
+ * This CLI just launches it and gets out of the way.
4
7
  *
5
8
  * Usage:
6
- * npx hermes-launch # full interactive flow
7
- * npx hermes-launch --quick # skip prompts, use smart defaults
8
- * npx hermes-launch --dashboard # just start the web dashboard
9
+ * npx hermes-launch # start wizard, open browser
10
+ * npx hermes-launch --port 8080 # custom port
11
+ * npx hermes-launch --stop # stop running instance
12
+ * npx hermes-launch --status # check if running
9
13
  * npx hermes-launch --help # this message
10
14
  */
11
15
 
12
- import { execSync, spawn } from 'child_process';
13
- import { readFileSync, existsSync } from 'fs';
14
- import { homedir, platform as osPlatform } from 'os';
15
- import { resolve } from 'path';
16
- import { createInterface } from 'readline';
16
+ import { execSync, spawn } from 'node:child_process';
17
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
18
+ import { homedir, platform as osPlatform } from 'node:os';
19
+ import { resolve } from 'node:path';
17
20
 
18
- const PLATFORM = osPlatform(); // win32, darwin, linux
21
+ const PLATFORM = osPlatform();
19
22
  const IS_WIN = PLATFORM === 'win32';
20
23
  const IS_MAC = PLATFORM === 'darwin';
21
- const IS_LINUX = PLATFORM === 'linux';
22
-
23
- const HERMES_HOME = IS_WIN
24
- ? resolve(process.env.USERPROFILE || homedir(), '.hermes')
25
- : resolve(homedir(), '.hermes');
26
- const CONFIG_PATH = resolve(HERMES_HOME, 'config.yaml');
27
- const INSTALL_URL = 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh';
28
-
29
- // Windows: node strips ansi codes in cmd/powershell by default, but keep for
30
- // terminals that support them (Windows Terminal, VS Code terminal, etc.)
31
- const USE_COLOR = !IS_WIN || (process.env.TERM_PROGRAM || '').includes('vscode') || process.env.WT_SESSION;
32
- const BOLD = USE_COLOR ? '\x1b[1m' : '';
33
- const DIM = USE_COLOR ? '\x1b[2m' : '';
34
- const GREEN = USE_COLOR ? '\x1b[32m' : '';
35
- const YELLOW = USE_COLOR ? '\x1b[33m' : '';
36
- const RED = USE_COLOR ? '\x1b[31m' : '';
37
- const CYAN = USE_COLOR ? '\x1b[36m' : '';
38
- const RESET = USE_COLOR ? '\x1b[0m' : '';
39
-
40
- // ——— helpers ———
41
-
42
- function printBanner() {
43
- console.log(`
44
- ${CYAN}╔════════════════════════════════════════╗${RESET}
45
- ${CYAN}║${RESET} 🐚 Hermes Launch ${CYAN}║${RESET}
46
- ${CYAN}║${RESET} ${DIM}zero-to-AI-agent in seconds${RESET} ${CYAN}║${RESET}
47
- ${CYAN}╚════════════════════════════════════════╝${RESET}
48
- `);
49
- }
50
-
51
- function ask(query) {
52
- const rl = createInterface({ input: process.stdin, output: process.stdout });
53
- return new Promise(resolve => rl.question(query, ans => { rl.close(); resolve(ans.trim()); }));
54
- }
24
+ const PID_FILE = resolve(homedir(), '.hermes-launch.pid');
55
25
 
56
26
  function run(cmd, opts = {}) {
57
- const defaults = {
58
- stdio: opts.silent ? 'pipe' : 'inherit',
59
- timeout: 120_000,
60
- shell: IS_WIN ? true : false, // Windows needs shell for PATH resolution
61
- ...opts,
62
- };
63
27
  try {
64
- const out = execSync(cmd, defaults);
28
+ const out = execSync(cmd, { stdio: 'pipe', timeout: opts.timeout || 30_000, shell: IS_WIN, ...opts });
65
29
  return { ok: true, out: out?.toString()?.trim() || '' };
66
30
  } catch (e) {
67
31
  return { ok: false, out: e.stderr?.toString()?.trim() || e.message };
68
32
  }
69
33
  }
70
34
 
71
- function runPwsh(cmd, opts = {}) {
72
- // Run a PowerShell command — used on Windows for winget, etc.
73
- return run(`powershell -NoProfile -Command "${cmd}"`, opts);
74
- }
75
-
76
- function spinner(ms) {
77
- return new Promise(resolve => setTimeout(resolve, ms));
78
- }
79
-
80
- function step(msg) {
81
- process.stdout.write(` ${CYAN}→${RESET} ${msg} ... `);
82
- return {
83
- ok: () => { process.stdout.write(`${GREEN}done${RESET}\n`); },
84
- skip: (reason) => { process.stdout.write(`${YELLOW}skipped${RESET} (${reason})\n`); },
85
- fail: (err) => { process.stdout.write(`${RED}failed${RESET}\n ${DIM}${err}${RESET}\n`); },
86
- };
87
- }
88
-
89
- function printSummary(info) {
90
- console.log(`\n${BOLD}${GREEN} ✅ Hermes Agent is live!${RESET}\n`);
91
- console.log(` ${BOLD}Web Dashboard${RESET}`);
92
- console.log(` ${CYAN}http://localhost:${info.port || 9119}${RESET}\n`);
93
- console.log(` ${BOLD}CLI${RESET}`);
94
- console.log(` ${CYAN}hermes${RESET} interactive chat`);
95
- console.log(` ${CYAN}hermes chat -q "..."${RESET} one-shot query`);
96
- console.log(` ${CYAN}hermes --continue${RESET} resume last session\n`);
97
- console.log(` ${BOLD}Key commands${RESET}`);
98
- console.log(` ${CYAN}hermes model${RESET} change model/provider`);
99
- console.log(` ${CYAN}hermes gateway setup${RESET} connect Telegram/Discord`);
100
- console.log(` ${CYAN}hermes tools list${RESET} see available tools`);
101
- console.log(` ${CYAN}hermes doctor${RESET} health check\n`);
102
- console.log(` ${BOLD}Docs${RESET}`);
103
- console.log(` ${CYAN}https://hermes-agent.nousresearch.com/docs${RESET}\n`);
104
- if (info.messaging) {
105
- console.log(` ${BOLD}Messaging${RESET}`);
106
- for (const [platform, status] of Object.entries(info.messaging)) {
107
- console.log(` ${platform}: ${status}`);
108
- }
109
- console.log();
110
- }
111
- if (info.windowsNote) {
112
- console.log(` ${YELLOW}${info.windowsNote}${RESET}\n`);
113
- }
114
- }
115
-
116
- // ——— platform detection ———
117
-
118
- function detectPackageManager() {
119
- if (IS_WIN) {
120
- // winget ships with Windows 11 and recent Win10
121
- if (run('winget --version', { silent: true }).ok) return 'winget';
122
- if (run('choco --version', { silent: true }).ok) return 'choco';
123
- if (run('scoop --version', { silent: true }).ok) return 'scoop';
124
- return null;
125
- }
126
- if (IS_MAC) {
127
- if (run('brew --version', { silent: true }).ok) return 'brew';
128
- return null;
129
- }
130
- // Linux
131
- if (run('apt-get --version', { silent: true }).ok) return 'apt';
132
- if (run('dnf --version', { silent: true }).ok) return 'dnf';
133
- if (run('pacman --version', { silent: true }).ok) return 'pacman';
134
- return null;
135
- }
136
-
137
- function winHasWsl() {
138
- return run('wsl --version', { silent: true }).ok || run('wsl -l', { silent: true }).ok;
139
- }
140
-
141
- // ——— prerequisite checks + auto-install ———
142
-
143
- async function checkPython() {
144
- // Windows uses "python" and "py", not "python3"
145
- const pyCandidates = IS_WIN
146
- ? ['python', 'py -3', 'python3']
147
- : ['python3.14', 'python3.13', 'python3.12', 'python3.11', 'python3.10', 'python3'];
148
-
149
- for (const bin of pyCandidates) {
150
- const r = run(`${bin} --version`, { silent: true });
151
- if (r.ok) {
152
- const match = r.out.match(/(\d+)\.(\d+)/);
153
- if (match) {
154
- const major = parseInt(match[1]), minor = parseInt(match[2]);
155
- if (major >= 3 && minor >= 10) {
156
- return { ok: true, bin, version: `${major}.${minor}` };
157
- }
158
- }
159
- }
160
- }
161
- return { ok: false, bin: null, version: null };
162
- }
163
-
164
- async function installPython(pkgMan) {
165
- const s = step('Installing Python 3.12+');
35
+ function openBrowser(url) {
166
36
  try {
167
37
  if (IS_WIN) {
168
- if (pkgMan === 'winget') {
169
- const r = runPwsh('winget install --accept-source-agreements --accept-package-agreements Python.Python.3.12', { timeout: 180_000 });
170
- if (!r.ok) throw new Error(r.out);
171
- } else if (pkgMan === 'choco') {
172
- run('choco install python --yes', { timeout: 180_000 });
173
- } else if (pkgMan === 'scoop') {
174
- run('scoop install python', { timeout: 180_000 });
175
- } else {
176
- s.fail('No package manager found. Install Python from https://python.org');
177
- return false;
178
- }
179
- // Refresh PATH so we can find python immediately
180
- process.env.PATH = `${process.env.USERPROFILE}\\AppData\\Local\\Programs\\Python\\Python312\\;${process.env.USERPROFILE}\\AppData\\Local\\Programs\\Python\\Python312\\Scripts\\;${process.env.PATH}`;
38
+ execSync(`start "" "${url}"`, { shell: 'cmd.exe', timeout: 5000, stdio: 'ignore' });
181
39
  } else if (IS_MAC) {
182
- run('brew install python@3.12', { timeout: 180_000 });
183
- // brew python3.12 is often keg-only — symlink or use full path
184
- run('brew link --overwrite python@3.12', { silent: true, timeout: 30_000 });
185
- } else {
186
- // Linux
187
- if (pkgMan === 'apt') {
188
- run('apt-get update -qq && apt-get install -y python3 python3-pip python3-venv', { timeout: 180_000 });
189
- } else if (pkgMan === 'dnf') {
190
- run('dnf install -y python3 python3-pip', { timeout: 180_000 });
191
- } else if (pkgMan === 'pacman') {
192
- run('pacman -S --noconfirm python python-pip', { timeout: 180_000 });
193
- }
194
- }
195
- s.ok();
196
- return true;
197
- } catch (e) {
198
- s.fail(e.message);
199
- return false;
200
- }
201
- }
202
-
203
- async function checkCurl() {
204
- const candidates = IS_WIN ? ['curl.exe', 'curl'] : ['curl'];
205
- for (const bin of candidates) {
206
- if (run(`${bin} --version`, { silent: true }).ok) return { ok: true, bin };
207
- }
208
- return { ok: false, bin: null };
209
- }
210
-
211
- async function installCurl(pkgMan) {
212
- const s = step('Installing curl');
213
- try {
214
- if (IS_WIN) {
215
- // Windows 10+ has curl.exe built-in. If missing, use winget.
216
- if (pkgMan === 'winget') {
217
- runPwsh('winget install --accept-source-agreements cURL.cURL', { timeout: 120_000 });
218
- } else if (pkgMan === 'choco') {
219
- run('choco install curl --yes', { timeout: 120_000 });
220
- }
221
- } else {
222
- // macOS/Linux — curl should be present but just in case
223
- run('command -v curl || (apt-get install -y curl 2>/dev/null || yum install -y curl 2>/dev/null)', { timeout: 120_000 });
224
- }
225
- s.ok();
226
- return true;
227
- } catch (e) {
228
- s.fail(e.message);
229
- return false;
230
- }
231
- }
232
-
233
- async function checkGit() {
234
- const candidates = IS_WIN ? ['git.exe', 'git'] : ['git'];
235
- for (const bin of candidates) {
236
- if (run(`${bin} --version`, { silent: true }).ok) return { ok: true, bin };
237
- }
238
- return { ok: false, bin: null };
239
- }
240
-
241
- async function installGit(pkgMan) {
242
- const s = step('Installing git');
243
- try {
244
- if (IS_WIN) {
245
- if (pkgMan === 'winget') {
246
- runPwsh('winget install --accept-source-agreements --accept-package-agreements Git.Git', { timeout: 180_000 });
247
- } else if (pkgMan === 'choco') {
248
- run('choco install git --yes', { timeout: 180_000 });
249
- } else if (pkgMan === 'scoop') {
250
- run('scoop install git', { timeout: 180_000 });
251
- }
252
- } else {
253
- run('command -v git || (apt-get install -y git 2>/dev/null || brew install git 2>/dev/null)', { timeout: 120_000 });
254
- }
255
- s.ok();
256
- return true;
257
- } catch (e) {
258
- s.fail(e.message);
259
- return false;
260
- }
261
- }
262
-
263
- async function checkPrereqs() {
264
- const s = step('Checking prerequisites');
265
-
266
- const pkgMan = detectPackageManager();
267
-
268
- // 1. Python
269
- const py = await checkPython();
270
- if (!py.ok) {
271
- if (pkgMan) {
272
- process.stdout.write(`\n`);
273
- const installed = await installPython(pkgMan);
274
- if (!installed) {
275
- process.exit(1);
276
- }
277
- } else {
278
- s.fail('Python 3.10+ is required but not found');
279
- if (IS_WIN) {
280
- console.log(` ${YELLOW}Install: https://python.org or run: winget install Python.Python.3.12${RESET}`);
281
- } else if (IS_MAC) {
282
- console.log(` ${YELLOW}Install: brew install python@3.12${RESET}`);
283
- } else {
284
- console.log(` ${YELLOW}Install: sudo apt install python3 python3-pip python3-venv${RESET}`);
285
- }
286
- process.exit(1);
287
- }
288
- }
289
-
290
- // 2. curl
291
- const curlCheck = await checkCurl();
292
- if (!curlCheck.ok) {
293
- if (pkgMan) {
294
- await installCurl(pkgMan);
295
- } else {
296
- s.fail('curl is required');
297
- process.exit(1);
298
- }
299
- }
300
-
301
- // 3. git
302
- const gitCheck = await checkGit();
303
- if (!gitCheck.ok) {
304
- if (pkgMan) {
305
- await installGit(pkgMan);
306
- } else {
307
- s.fail('git is required');
308
- process.exit(1);
309
- }
310
- }
311
-
312
- s.ok();
313
- return { ...py, pkgMan, curlBin: curlCheck.bin || '', gitBin: gitCheck.bin || '' };
314
- }
315
-
316
- // ——— Hermes install ———
317
-
318
- async function installHermes(quick, platformInfo) {
319
- const s = step('Checking Hermes installation');
320
-
321
- // On Windows, try both "hermes" and "hermes.exe"
322
- const hermesCheck = IS_WIN
323
- ? (run('hermes --version', { silent: true }).ok || run('hermes.exe --version', { silent: true }).ok)
324
- : run('hermes --version', { silent: true }).ok;
325
-
326
- if (hermesCheck) {
327
- const ver = run('hermes --version', { silent: true }).ok
328
- ? run('hermes --version', { silent: true }).out
329
- : 'installed';
330
- s.skip(`${ver} already installed`);
331
- return true;
332
- }
333
-
334
- if (!quick) {
335
- console.log();
336
- const ans = await ask(` Hermes not found. Install it now? [Y/n] `);
337
- if (ans.toLowerCase() === 'n') {
338
- console.log(` ${YELLOW}Aborted.${RESET}`);
339
- process.exit(0);
340
- }
341
- }
342
-
343
- const si = step('Downloading and installing Hermes');
344
-
345
- if (IS_WIN) {
346
- // Windows: install via pip (the bash script won't work without WSL)
347
- try {
348
- // Ensure pip is available
349
- const pyBin = platformInfo.bin || 'python';
350
- run(`${pyBin} -m pip install --upgrade pip`, { silent: true, timeout: 60_000 });
351
-
352
- const r = run(`${pyBin} -m pip install hermes-agent`, { timeout: 180_000 });
353
- if (!r.ok) throw new Error(r.out);
354
-
355
- // After pip install, check if "hermes" command is available
356
- // On Windows, scripts may be in %APPDATA%\Python\Scripts or %USERPROFILE%\.local\bin
357
- const pathsToAdd = [
358
- resolve(process.env.APPDATA || '', 'Python', 'Scripts'),
359
- resolve(homedir(), '.local', 'bin'),
360
- ];
361
- for (const p of pathsToAdd) {
362
- if (!process.env.PATH?.includes(p)) {
363
- process.env.PATH = `${p};${process.env.PATH}`;
364
- }
365
- }
366
-
367
- si.ok();
368
- return true;
369
- } catch (e) {
370
- // If pip fails, suggest WSL which is the recommended path
371
- si.fail(e.message);
372
- if (winHasWsl()) {
373
- console.log(`\n ${YELLOW}Pip install failed. Try installing inside WSL instead:${RESET}`);
374
- console.log(` ${CYAN}wsl -d Ubuntu -e bash -c "curl -fsSL ${INSTALL_URL} | bash"${RESET}\n`);
375
- } else {
376
- console.log(`\n ${YELLOW}Install WSL + Ubuntu, then run this inside WSL:${RESET}`);
377
- console.log(` ${CYAN}curl -fsSL ${INSTALL_URL} | bash${RESET}`);
378
- console.log(` ${YELLOW}Or install WSL first:${RESET}`);
379
- console.log(` ${CYAN}wsl --install${RESET}\n`);
380
- }
381
- process.exit(1);
382
- }
383
- } else {
384
- // macOS / Linux
385
- try {
386
- const cmd = `curl -fsSL ${INSTALL_URL} | bash`;
387
- const result = run(cmd, { timeout: 180_000, silent: false });
388
- if (!result.ok) {
389
- si.fail(result.out);
390
- process.exit(1);
391
- }
392
- si.ok();
393
- return true;
394
- } catch (e) {
395
- si.fail(e.message);
396
- process.exit(1);
397
- }
398
- }
399
- }
400
-
401
- // ——— config ———
402
-
403
- async function checkConfig(quick) {
404
- const s = step('Checking Hermes configuration');
405
-
406
- if (!existsSync(CONFIG_PATH)) {
407
- s.skip('no config yet — will be generated');
408
- return false;
409
- }
410
-
411
- const raw = readFileSync(CONFIG_PATH, 'utf-8');
412
-
413
- // Check if config has a model set (not commented out)
414
- const hasModel = /^model:\s*\n/m.test(raw);
415
-
416
- if (!hasModel || raw.includes('default: ""') || raw.includes("default: ''")) {
417
- s.skip('needs model configuration');
418
- return false;
419
- }
420
-
421
- s.ok();
422
- return true;
423
- }
424
-
425
- async function runSetup(quick) {
426
- if (quick) {
427
- const s = step('Running headless setup');
428
- const setCmds = [
429
- 'hermes tools enable web terminal file skills session_search delegate_task memory clarify vision 2>/dev/null || ver>nul',
430
- `hermes config set display.streaming.enabled true 2>/dev/null || cmd /c ver>nul`,
431
- ];
432
- for (const cmd of setCmds) run(cmd, { silent: true });
433
- s.ok();
434
- return;
435
- }
436
-
437
- console.log(`\n ${BOLD}Let's configure your Hermes Agent.${RESET}\n`);
438
- console.log(` ${DIM}(You can skip any step and configure later with \`hermes setup\`)${RESET}\n`);
439
-
440
- const doSetup = await ask(` Run the Hermes setup wizard? (recommended) [Y/n] `);
441
- if (doSetup.toLowerCase() !== 'n') {
442
- const s = step('Running setup wizard');
443
- try {
444
- const result = run('hermes setup', { stdio: 'inherit', timeout: 300_000 });
445
- if (!result.ok) s.fail(result.out);
446
- else s.ok();
447
- } catch (e) {
448
- s.fail(e.message);
449
- }
450
- } else {
451
- const s = step('Applying minimal configuration');
452
- run('hermes tools enable web terminal file skills session_search delegate_task memory 2>/dev/null || ver>nul', { silent: true });
453
- s.skip('minimal config applied');
454
- }
455
- }
456
-
457
- async function enableToolsets(quick) {
458
- const s = step('Enabling tool sets');
459
-
460
- const toolsets = ['web', 'terminal', 'file', 'skills', 'session_search', 'delegate_task', 'memory', 'vision', 'tts', 'clarify'];
461
-
462
- // Windows: hermes commands use shell redirect
463
- const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
464
- for (const t of toolsets) {
465
- run(`hermes tools enable ${t} ${noop}`, { silent: true });
466
- }
467
-
468
- run(`hermes config set display.streaming.enabled true ${noop}`, { silent: true });
469
- s.ok();
470
- }
471
-
472
- async function startDashboard(quick) {
473
- const s = step('Starting web dashboard');
474
- const port = '9119';
475
-
476
- const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
477
-
478
- const statusCheck = run(`hermes dashboard --status ${noop}`, { silent: true });
479
- if (statusCheck.ok && statusCheck.out.length > 0 && !statusCheck.out.includes('No running')) {
480
- s.skip('already running');
481
- return { port, alreadyRunning: true };
482
- }
483
-
484
- try {
485
- const proc = spawn(
486
- 'hermes',
487
- ['dashboard', '--port', port, '--tui', quick ? '--no-open' : ''],
488
- {
489
- stdio: 'inherit',
490
- detached: false,
491
- shell: IS_WIN,
492
- env: { ...process.env },
493
- }
494
- );
495
-
496
- await spinner(3000);
497
-
498
- // Verify — use curl or curl.exe
499
- const curlBin = IS_WIN ? 'curl.exe -s -o nul -w "%{http_code}"' : 'curl -s -o /dev/null -w "%{http_code}"';
500
- const verify = run(`${curlBin} http://127.0.0.1:${port}/ 2>/dev/null || echo 0`, { silent: true });
501
- if (verify.out === '0') {
502
- s.skip('dashboard launched in background');
40
+ execSync(`open "${url}"`, { timeout: 5000, stdio: 'ignore' });
503
41
  } else {
504
- s.ok();
505
- }
506
-
507
- return { port, alreadyRunning: false };
508
- } catch (e) {
509
- s.fail(e.message);
510
- return { port, alreadyRunning: false, error: e.message };
511
- }
512
- }
513
-
514
- async function checkGatewayStatus() {
515
- const s = step('Checking messaging gateway');
516
- const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
517
- const result = run(`hermes gateway status ${noop}`, { silent: true });
518
- if (result.ok) {
519
- const out = result.out;
520
- const hasTelegram = out.includes('telegram') && (out.includes('running') || out.includes('connected'));
521
- const hasDiscord = out.includes('discord') && (out.includes('running') || out.includes('connected'));
522
- const platforms = [];
523
- if (hasTelegram) platforms.push('telegram');
524
- if (hasDiscord) platforms.push('discord');
525
- if (platforms.length > 0) {
526
- s.ok();
527
- return platforms;
42
+ execSync(`xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null || true`, { timeout: 5000, stdio: 'ignore' });
528
43
  }
529
- }
530
- s.skip('not configured');
531
- return [];
44
+ } catch (_) {}
532
45
  }
533
46
 
534
- // ——— main ———
535
-
536
47
  async function main() {
537
48
  const args = process.argv.slice(2);
538
49
  const flags = {
539
- quick: args.includes('--quick') || args.includes('-q'),
540
- dashboard: args.includes('--dashboard'),
50
+ port: parseInt(args.find(a => a.startsWith('--port='))?.split('=')[1] || args[args.indexOf('--port') + 1]) || 5050,
51
+ stop: args.includes('--stop'),
52
+ status: args.includes('--status'),
541
53
  help: args.includes('--help') || args.includes('-h'),
542
54
  version: args.includes('--version') || args.includes('-v'),
543
55
  };
544
56
 
545
57
  if (flags.help) {
546
58
  console.log(`
547
- ${BOLD}hermes-launch${RESET}one command to install and run Hermes Agent
548
-
549
- ${BOLD}Usage:${RESET}
550
- npx hermes-launch full interactive setup
551
- npx hermes-launch --quick non-interactive, smart defaults
552
- npx hermes-launch --dashboard just start the web dashboard
553
- npx hermes-launch --help this message
59
+ hermes-launch — web onboarding for Hermes Agent
554
60
 
555
- ${BOLD}What it does:${RESET}
556
- 1. Check prerequisites (Python, curl, git)
557
- 2. Install missing dependencies automatically
558
- 3. Install Hermes Agent (if not present)
559
- 4. Run setup wizard to configure model/tools
560
- 5. Enable useful toolsets
561
- 6. Start the web dashboard on port 9119
562
- 7. Print next steps
61
+ Usage:
62
+ npx hermes-launch start wizard + open browser
63
+ npx hermes-launch --port 8080 custom port
64
+ npx hermes-launch --stop stop running instance
65
+ npx hermes-launch --status check if running
66
+ npx hermes-launch --help this message
563
67
 
564
- ${BOLD}After launch:${RESET}
565
- hermes start chatting
566
- hermes gateway setup add Telegram/Discord
567
- hermes model change AI provider
568
-
569
- ${BOLD}Windows notes:${RESET}
570
- - Python is auto-installed via winget (or choco/scoop) if missing
571
- - Recommended: use Windows Terminal for best experience
572
- - For full functionality, WSL2 is recommended:
573
- wsl --install -d Ubuntu
574
- Then run: npx hermes-launch inside WSL
68
+ The web wizard handles everything: install, config, tools, dashboard.
69
+ Open http://localhost:5050 in your browser if it doesn't open automatically.
575
70
  `);
576
71
  process.exit(0);
577
72
  }
578
73
 
579
74
  if (flags.version) {
580
- const pkg = JSON.parse(readFileSync(resolve(import.meta.dirname, '../package.json'), 'utf-8'));
581
- console.log(`hermes-launch v${pkg.version}`);
75
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
76
+ console.log('hermes-launch v' + pkg.version);
582
77
  process.exit(0);
583
78
  }
584
79
 
585
- if (flags.dashboard) {
586
- const result = await startDashboard(true);
587
- if (result.error) process.exit(1);
588
- printSummary({ port: result.port, messaging: {} });
80
+ // Handle stop/status regardless of pid file
81
+ if (flags.stop) {
82
+ try {
83
+ const curPid = process.pid;
84
+ const pids = run(`ps aux | grep -E 'node.*hermes-launch|node.*server.js' | grep -v grep | awk '{print $2}' | grep -v "${curPid}"`, { silent: true, shell: true });
85
+ if (pids.ok && pids.out) {
86
+ const pidList = pids.out.split('\n').filter(Boolean);
87
+ for (const pid of pidList) {
88
+ try { process.kill(parseInt(pid), 'SIGTERM'); } catch {}
89
+ }
90
+ console.log(' Stopped ' + pidList.length + ' instance(s)');
91
+ } else {
92
+ console.log(' No running instances found');
93
+ }
94
+ } catch {}
95
+ try { writeFileSync(PID_FILE, ''); } catch {}
589
96
  process.exit(0);
590
97
  }
591
98
 
592
- // ——— full flow ———
593
- printBanner();
594
-
595
- if (IS_WIN) {
596
- console.log(` ${DIM}Windows detected — auto-installing missing deps${RESET}\n`);
99
+ if (flags.status) {
100
+ const check = run('curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:' + flags.port + '/ 2>/dev/null || echo 0', { silent: true });
101
+ if (check.out === '200') {
102
+ console.log(' Running at http://localhost:' + flags.port);
103
+ } else {
104
+ console.log(' Not running');
105
+ }
106
+ process.exit(0);
597
107
  }
598
108
 
599
- // 1. Prerequisites (auto-installs Python, git, curl if missing)
600
- const platformInfo = await checkPrereqs();
109
+ // Print banner
110
+ console.log(`
111
+ 🐚 Hermes Launch
112
+ ${'─'.repeat(30)}
113
+ Starting the web setup wizard...
114
+ `);
601
115
 
602
- // 2. Install Hermes
603
- await installHermes(flags.quick, platformInfo);
116
+ // Start the server in a detached child process
117
+ const serverPath = resolve(import.meta.dirname, '../src/server.js');
118
+ const proc = spawn(process.execPath, [serverPath, '--port', String(flags.port)], {
119
+ stdio: ['ignore', 'pipe', 'pipe'],
120
+ detached: true,
121
+ shell: IS_WIN,
122
+ env: { ...process.env },
123
+ });
124
+
125
+ // Collect startup output
126
+ let started = false;
127
+ proc.stdout.on('data', (data) => {
128
+ const text = data.toString();
129
+ process.stdout.write(' ' + text);
130
+ if (text.includes('Setup wizard at')) {
131
+ started = true;
132
+ }
133
+ });
134
+ proc.stderr.on('data', (data) => {
135
+ process.stderr.write(' ' + data.toString());
136
+ });
137
+
138
+ proc.on('error', (err) => {
139
+ console.error(' Failed to start:', err.message);
140
+ process.exit(1);
141
+ });
142
+
143
+ // Wait for the server to signal ready or timeout
144
+ await new Promise((resolve) => {
145
+ const timeout = setTimeout(() => {
146
+ if (!started) {
147
+ // Server may have started without printing — check if listening
148
+ const check = run('curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:' + flags.port + '/ 2>/dev/null || echo 0', { silent: true });
149
+ if (check.out === '200') started = true;
150
+ }
151
+ resolve();
152
+ }, 8000);
604
153
 
605
- // 3. Check config
606
- const needsSetup = await checkConfig(flags.quick);
154
+ proc.stdout.on('data', () => {
155
+ if (started) {
156
+ clearTimeout(timeout);
157
+ resolve();
158
+ }
159
+ });
160
+ });
607
161
 
608
- // 4. Setup if needed
609
- if (!needsSetup) {
610
- await runSetup(flags.quick);
162
+ // Save pid
163
+ if (proc.pid) {
164
+ try { writeFileSync(PID_FILE, String(proc.pid)); } catch {}
611
165
  }
612
166
 
613
- // 5. Enable tools
614
- await enableToolsets(flags.quick);
167
+ // Detach let the server live on
168
+ proc.unref();
615
169
 
616
- // 6. Start dashboard
617
- const dashResult = await startDashboard(flags.quick);
170
+ if (started) {
171
+ // Remove pipe listeners so event loop can exit
172
+ proc.stdout.removeAllListeners();
173
+ proc.stderr.removeAllListeners();
618
174
 
619
- // 7. Check gateway
620
- const platforms = await checkGatewayStatus();
175
+ console.log(`\n ${'✅'} Wizard ready at ${'http://localhost:' + flags.port}`);
621
176
 
622
- // 8. Show summary
623
- const summary = {
624
- port: dashResult.port,
625
- messaging: platforms.length > 0
626
- ? Object.fromEntries(platforms.map(p => [p, `${GREEN}connected${RESET}`]))
627
- : { none: `${YELLOW}not configured${RESET} (run \`hermes gateway setup\`)` },
628
- };
177
+ // Auto-open browser
178
+ const url = 'http://localhost:' + flags.port;
179
+ openBrowser(url);
629
180
 
630
- if (IS_WIN) {
631
- summary.windowsNote = 'On Windows, some tools work best in WSL2. Run `wsl --install -d Ubuntu` for the full experience.';
632
- }
181
+ console.log(`\n Browser should open automatically. If not, visit:`);
182
+ console.log(` ${'http://localhost:' + flags.port}`);
183
+ console.log(`\n Run ${'npx hermes-launch --stop'} to stop the server.`);
184
+ console.log(` This terminal is free now — the wizard runs in the background.\n`);
633
185
 
634
- printSummary(summary);
186
+ // Explicit exit — the server is detached and will keep running
187
+ process.exit(0);
188
+ } else {
189
+ console.log(`\n Server started in background. Open ${'http://localhost:' + flags.port} in your browser.\n`);
190
+ }
635
191
  }
636
192
 
637
193
  main().catch(e => {
638
- console.error(`${RED}Error:${RESET}`, e.message);
194
+ console.error('Error:', e.message);
639
195
  process.exit(1);
640
196
  });