hermes-launch 1.1.0 → 1.2.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.
@@ -26,8 +26,7 @@ const HERMES_HOME = IS_WIN
26
26
  const CONFIG_PATH = resolve(HERMES_HOME, 'config.yaml');
27
27
  const INSTALL_URL = 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh';
28
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.)
29
+ // Windows Terminal / VS Code support ansi; raw cmd.exe doesn't
31
30
  const USE_COLOR = !IS_WIN || (process.env.TERM_PROGRAM || '').includes('vscode') || process.env.WT_SESSION;
32
31
  const BOLD = USE_COLOR ? '\x1b[1m' : '';
33
32
  const DIM = USE_COLOR ? '\x1b[2m' : '';
@@ -57,7 +56,7 @@ function run(cmd, opts = {}) {
57
56
  const defaults = {
58
57
  stdio: opts.silent ? 'pipe' : 'inherit',
59
58
  timeout: 120_000,
60
- shell: IS_WIN ? true : false, // Windows needs shell for PATH resolution
59
+ shell: IS_WIN ? true : false,
61
60
  ...opts,
62
61
  };
63
62
  try {
@@ -69,7 +68,6 @@ function run(cmd, opts = {}) {
69
68
  }
70
69
 
71
70
  function runPwsh(cmd, opts = {}) {
72
- // Run a PowerShell command — used on Windows for winget, etc.
73
71
  return run(`powershell -NoProfile -Command "${cmd}"`, opts);
74
72
  }
75
73
 
@@ -86,30 +84,53 @@ function step(msg) {
86
84
  };
87
85
  }
88
86
 
87
+ function openBrowser(url) {
88
+ try {
89
+ if (IS_WIN) {
90
+ execSync(`start "" "${url}"`, { shell: 'cmd.exe', timeout: 5000, stdio: 'ignore' });
91
+ } else if (IS_MAC) {
92
+ execSync(`open "${url}"`, { timeout: 5000, stdio: 'ignore' });
93
+ } else {
94
+ execSync(`xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null || true`, { timeout: 5000, stdio: 'ignore' });
95
+ }
96
+ } catch (_) { /* best-effort */ }
97
+ }
98
+
89
99
  function printSummary(info) {
100
+ const dashUrl = `http://localhost:${info.port || 9119}`;
101
+
90
102
  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}`);
103
+
104
+ // Big visible dashboard URL
105
+ console.log(` ${BOLD}🌐 Web Dashboard${RESET}`);
106
+ console.log(` ${CYAN} ┌──────────────────────────────────────────┐${RESET}`);
107
+ console.log(` ${CYAN} │${RESET} ${BOLD}${dashUrl}${RESET} ${CYAN}│${RESET}`);
108
+ console.log(` ${CYAN} └──────────────────────────────────────────┘${RESET}`);
109
+ console.log(` ${DIM} (Opened in your browser)${RESET}\n`);
110
+
111
+ console.log(` ${BOLD}📟 CLI${RESET}`);
94
112
  console.log(` ${CYAN}hermes${RESET} interactive chat`);
95
113
  console.log(` ${CYAN}hermes chat -q "..."${RESET} one-shot query`);
96
114
  console.log(` ${CYAN}hermes --continue${RESET} resume last session\n`);
97
- console.log(` ${BOLD}Key commands${RESET}`);
115
+
116
+ console.log(` ${BOLD}⚡ Key commands${RESET}`);
98
117
  console.log(` ${CYAN}hermes model${RESET} change model/provider`);
99
118
  console.log(` ${CYAN}hermes gateway setup${RESET} connect Telegram/Discord`);
100
119
  console.log(` ${CYAN}hermes tools list${RESET} see available tools`);
101
120
  console.log(` ${CYAN}hermes doctor${RESET} health check\n`);
102
- console.log(` ${BOLD}Docs${RESET}`);
121
+
122
+ console.log(` ${BOLD}📖 Docs${RESET}`);
103
123
  console.log(` ${CYAN}https://hermes-agent.nousresearch.com/docs${RESET}\n`);
124
+
104
125
  if (info.messaging) {
105
- console.log(` ${BOLD}Messaging${RESET}`);
126
+ console.log(` ${BOLD}💬 Messaging${RESET}`);
106
127
  for (const [platform, status] of Object.entries(info.messaging)) {
107
128
  console.log(` ${platform}: ${status}`);
108
129
  }
109
130
  console.log();
110
131
  }
111
132
  if (info.windowsNote) {
112
- console.log(` ${YELLOW}${info.windowsNote}${RESET}\n`);
133
+ console.log(` ${YELLOW} ${info.windowsNote}${RESET}\n`);
113
134
  }
114
135
  }
115
136
 
@@ -117,7 +138,6 @@ function printSummary(info) {
117
138
 
118
139
  function detectPackageManager() {
119
140
  if (IS_WIN) {
120
- // winget ships with Windows 11 and recent Win10
121
141
  if (run('winget --version', { silent: true }).ok) return 'winget';
122
142
  if (run('choco --version', { silent: true }).ok) return 'choco';
123
143
  if (run('scoop --version', { silent: true }).ok) return 'scoop';
@@ -134,14 +154,9 @@ function detectPackageManager() {
134
154
  return null;
135
155
  }
136
156
 
137
- function winHasWsl() {
138
- return run('wsl --version', { silent: true }).ok || run('wsl -l', { silent: true }).ok;
139
- }
140
-
141
157
  // ——— prerequisite checks + auto-install ———
142
158
 
143
159
  async function checkPython() {
144
- // Windows uses "python" and "py", not "python3"
145
160
  const pyCandidates = IS_WIN
146
161
  ? ['python', 'py -3', 'python3']
147
162
  : ['python3.14', 'python3.13', 'python3.12', 'python3.11', 'python3.10', 'python3'];
@@ -176,14 +191,11 @@ async function installPython(pkgMan) {
176
191
  s.fail('No package manager found. Install Python from https://python.org');
177
192
  return false;
178
193
  }
179
- // Refresh PATH so we can find python immediately
180
194
  process.env.PATH = `${process.env.USERPROFILE}\\AppData\\Local\\Programs\\Python\\Python312\\;${process.env.USERPROFILE}\\AppData\\Local\\Programs\\Python\\Python312\\Scripts\\;${process.env.PATH}`;
181
195
  } else if (IS_MAC) {
182
196
  run('brew install python@3.12', { timeout: 180_000 });
183
- // brew python3.12 is often keg-only — symlink or use full path
184
197
  run('brew link --overwrite python@3.12', { silent: true, timeout: 30_000 });
185
198
  } else {
186
- // Linux
187
199
  if (pkgMan === 'apt') {
188
200
  run('apt-get update -qq && apt-get install -y python3 python3-pip python3-venv', { timeout: 180_000 });
189
201
  } else if (pkgMan === 'dnf') {
@@ -212,14 +224,12 @@ async function installCurl(pkgMan) {
212
224
  const s = step('Installing curl');
213
225
  try {
214
226
  if (IS_WIN) {
215
- // Windows 10+ has curl.exe built-in. If missing, use winget.
216
227
  if (pkgMan === 'winget') {
217
228
  runPwsh('winget install --accept-source-agreements cURL.cURL', { timeout: 120_000 });
218
229
  } else if (pkgMan === 'choco') {
219
230
  run('choco install curl --yes', { timeout: 120_000 });
220
231
  }
221
232
  } else {
222
- // macOS/Linux — curl should be present but just in case
223
233
  run('command -v curl || (apt-get install -y curl 2>/dev/null || yum install -y curl 2>/dev/null)', { timeout: 120_000 });
224
234
  }
225
235
  s.ok();
@@ -318,7 +328,6 @@ async function checkPrereqs() {
318
328
  async function installHermes(quick, platformInfo) {
319
329
  const s = step('Checking Hermes installation');
320
330
 
321
- // On Windows, try both "hermes" and "hermes.exe"
322
331
  const hermesCheck = IS_WIN
323
332
  ? (run('hermes --version', { silent: true }).ok || run('hermes.exe --version', { silent: true }).ok)
324
333
  : run('hermes --version', { silent: true }).ok;
@@ -343,17 +352,13 @@ async function installHermes(quick, platformInfo) {
343
352
  const si = step('Downloading and installing Hermes');
344
353
 
345
354
  if (IS_WIN) {
346
- // Windows: install via pip (the bash script won't work without WSL)
347
355
  try {
348
- // Ensure pip is available
349
356
  const pyBin = platformInfo.bin || 'python';
350
357
  run(`${pyBin} -m pip install --upgrade pip`, { silent: true, timeout: 60_000 });
351
358
 
352
359
  const r = run(`${pyBin} -m pip install hermes-agent`, { timeout: 180_000 });
353
360
  if (!r.ok) throw new Error(r.out);
354
361
 
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
362
  const pathsToAdd = [
358
363
  resolve(process.env.APPDATA || '', 'Python', 'Scripts'),
359
364
  resolve(homedir(), '.local', 'bin'),
@@ -367,21 +372,12 @@ async function installHermes(quick, platformInfo) {
367
372
  si.ok();
368
373
  return true;
369
374
  } catch (e) {
370
- // If pip fails, suggest WSL which is the recommended path
371
375
  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
- }
376
+ console.log(`\n ${YELLOW}Pip install failed. Try inside WSL instead:${RESET}`);
377
+ console.log(` ${CYAN}wsl -d Ubuntu -e bash -c "curl -fsSL ${INSTALL_URL} | bash"${RESET}\n`);
381
378
  process.exit(1);
382
379
  }
383
380
  } else {
384
- // macOS / Linux
385
381
  try {
386
382
  const cmd = `curl -fsSL ${INSTALL_URL} | bash`;
387
383
  const result = run(cmd, { timeout: 180_000, silent: false });
@@ -410,7 +406,6 @@ async function checkConfig(quick) {
410
406
 
411
407
  const raw = readFileSync(CONFIG_PATH, 'utf-8');
412
408
 
413
- // Check if config has a model set (not commented out)
414
409
  const hasModel = /^model:\s*\n/m.test(raw);
415
410
 
416
411
  if (!hasModel || raw.includes('default: ""') || raw.includes("default: ''")) {
@@ -425,11 +420,9 @@ async function checkConfig(quick) {
425
420
  async function runSetup(quick) {
426
421
  if (quick) {
427
422
  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 });
423
+ const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
424
+ run(`hermes tools enable web terminal file skills session_search delegate_task memory clarify vision ${noop}`, { silent: true });
425
+ run(`hermes config set display.streaming.enabled true ${noop}`, { silent: true });
433
426
  s.ok();
434
427
  return;
435
428
  }
@@ -449,7 +442,8 @@ async function runSetup(quick) {
449
442
  }
450
443
  } else {
451
444
  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 });
445
+ const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
446
+ run(`hermes tools enable web terminal file skills session_search delegate_task memory ${noop}`, { silent: true });
453
447
  s.skip('minimal config applied');
454
448
  }
455
449
  }
@@ -458,8 +452,6 @@ async function enableToolsets(quick) {
458
452
  const s = step('Enabling tool sets');
459
453
 
460
454
  const toolsets = ['web', 'terminal', 'file', 'skills', 'session_search', 'delegate_task', 'memory', 'vision', 'tts', 'clarify'];
461
-
462
- // Windows: hermes commands use shell redirect
463
455
  const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
464
456
  for (const t of toolsets) {
465
457
  run(`hermes tools enable ${t} ${noop}`, { silent: true });
@@ -469,41 +461,61 @@ async function enableToolsets(quick) {
469
461
  s.ok();
470
462
  }
471
463
 
464
+ // ——— FIXED: dashboard launches as a detached process that survives after hermes-launch exits ———
465
+
472
466
  async function startDashboard(quick) {
473
467
  const s = step('Starting web dashboard');
474
468
  const port = '9119';
475
-
476
469
  const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
470
+ const dashUrl = `http://localhost:${port}`;
477
471
 
472
+ // Check if already running
478
473
  const statusCheck = run(`hermes dashboard --status ${noop}`, { silent: true });
479
474
  if (statusCheck.ok && statusCheck.out.length > 0 && !statusCheck.out.includes('No running')) {
480
475
  s.skip('already running');
476
+ if (!quick) openBrowser(dashUrl);
481
477
  return { port, alreadyRunning: true };
482
478
  }
483
479
 
484
480
  try {
481
+ // Spawn detached — survives after this script exits
485
482
  const proc = spawn(
486
483
  'hermes',
487
- ['dashboard', '--port', port, '--tui', quick ? '--no-open' : ''],
484
+ ['dashboard', '--port', port, '--tui', '--no-open', '--skip-build'],
488
485
  {
489
- stdio: 'inherit',
490
- detached: false,
486
+ stdio: ['ignore', 'ignore', 'ignore'],
487
+ detached: true,
491
488
  shell: IS_WIN,
492
489
  env: { ...process.env },
493
490
  }
494
491
  );
495
492
 
496
- await spinner(3000);
493
+ // Release the child — it lives on its own
494
+ proc.on('error', () => {});
495
+ if (proc.unref) proc.unref();
496
+
497
+ // Give it time to boot
498
+ await spinner(4000);
499
+
500
+ // Verify it's actually responding
501
+ const curlBin = IS_WIN
502
+ ? 'curl.exe -s -o nul -w "%{http_code}"'
503
+ : 'curl -s -o /dev/null -w "%{http_code}"';
504
+ const verify = run(`${curlBin} ${dashUrl}/ 2>/dev/null || echo 0`, { silent: true });
497
505
 
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
506
  if (verify.out === '0') {
502
- s.skip('dashboard launched in background');
507
+ s.skip('started in background (may take a few seconds)');
503
508
  } else {
504
509
  s.ok();
505
510
  }
506
511
 
512
+ // Open browser (unless --quick)
513
+ if (!quick) {
514
+ process.stdout.write(` ${CYAN}→${RESET} Opening web dashboard in browser ... `);
515
+ openBrowser(dashUrl);
516
+ process.stdout.write(`${GREEN}done${RESET}\n`);
517
+ }
518
+
507
519
  return { port, alreadyRunning: false };
508
520
  } catch (e) {
509
521
  s.fail(e.message);
@@ -554,12 +566,12 @@ async function main() {
554
566
 
555
567
  ${BOLD}What it does:${RESET}
556
568
  1. Check prerequisites (Python, curl, git)
557
- 2. Install missing dependencies automatically
569
+ 2. Install missing dependencies automatically (Windows: winget/choco)
558
570
  3. Install Hermes Agent (if not present)
559
571
  4. Run setup wizard to configure model/tools
560
- 5. Enable useful toolsets
572
+ 5. Enable useful toolsets (web, terminal, file, skills, memory, ...)
561
573
  6. Start the web dashboard on port 9119
562
- 7. Print next steps
574
+ 7. Open browser, print next steps
563
575
 
564
576
  ${BOLD}After launch:${RESET}
565
577
  hermes start chatting
@@ -567,8 +579,8 @@ async function main() {
567
579
  hermes model change AI provider
568
580
 
569
581
  ${BOLD}Windows notes:${RESET}
570
- - Python is auto-installed via winget (or choco/scoop) if missing
571
- - Recommended: use Windows Terminal for best experience
582
+ - Python auto-installed via winget if missing
583
+ - Web dashboard opens in browser
572
584
  - For full functionality, WSL2 is recommended:
573
585
  wsl --install -d Ubuntu
574
586
  Then run: npx hermes-launch inside WSL
@@ -583,7 +595,7 @@ async function main() {
583
595
  }
584
596
 
585
597
  if (flags.dashboard) {
586
- const result = await startDashboard(true);
598
+ const result = await startDashboard(false);
587
599
  if (result.error) process.exit(1);
588
600
  printSummary({ port: result.port, messaging: {} });
589
601
  process.exit(0);
@@ -613,7 +625,7 @@ async function main() {
613
625
  // 5. Enable tools
614
626
  await enableToolsets(flags.quick);
615
627
 
616
- // 6. Start dashboard
628
+ // 6. Start dashboard (detached — survives after script exits)
617
629
  const dashResult = await startDashboard(flags.quick);
618
630
 
619
631
  // 7. Check gateway
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermes-launch",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "One command to launch Hermes Agent — zero-to-AI-agent in seconds",
5
5
  "bin": {
6
6
  "hermes-launch": "./bin/hermes-launch.js"