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