hermes-launch 1.0.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.
Files changed (2) hide show
  1. package/bin/hermes-launch.js +371 -166
  2. package/package.json +1 -1
@@ -10,34 +10,41 @@
10
10
  */
11
11
 
12
12
  import { execSync, spawn } from 'child_process';
13
- import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
14
- import { homedir, platform, hostname } from 'os';
13
+ import { readFileSync, existsSync } from 'fs';
14
+ import { homedir, platform as osPlatform } from 'os';
15
15
  import { resolve } from 'path';
16
16
  import { createInterface } from 'readline';
17
17
 
18
- const HERMES_HOME = resolve(homedir(), '.hermes');
18
+ const PLATFORM = osPlatform(); // win32, darwin, linux
19
+ const IS_WIN = PLATFORM === 'win32';
20
+ 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');
19
26
  const CONFIG_PATH = resolve(HERMES_HOME, 'config.yaml');
20
27
  const INSTALL_URL = 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh';
21
28
 
22
- const BOLD = '\x1b[1m';
23
- const DIM = '\x1b[2m';
24
- const GREEN = '\x1b[32m';
25
- const YELLOW = '\x1b[33m';
26
- const RED = '\x1b[31m';
27
- const CYAN = '\x1b[36m';
28
- const RESET = '\x1b[0m';
29
- const CLEAR_LINE = '\x1b[K';
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' : '';
30
38
 
31
39
  // ——— helpers ———
32
40
 
33
41
  function printBanner() {
34
- const banner = `
35
- ╔════════════════════════════════════════╗
36
- ${CYAN}🐚 Hermes Launch ${RESET}
37
- ${DIM}zero-to-AI-agent in seconds${RESET}
38
- ╚════════════════════════════════════════╝
39
- `;
40
- console.log(banner);
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
+ `);
41
48
  }
42
49
 
43
50
  function ask(query) {
@@ -46,7 +53,12 @@ function ask(query) {
46
53
  }
47
54
 
48
55
  function run(cmd, opts = {}) {
49
- const defaults = { stdio: opts.silent ? 'pipe' : 'inherit', timeout: 120_000, ...opts };
56
+ const defaults = {
57
+ stdio: opts.silent ? 'pipe' : 'inherit',
58
+ timeout: 120_000,
59
+ shell: IS_WIN ? true : false,
60
+ ...opts,
61
+ };
50
62
  try {
51
63
  const out = execSync(cmd, defaults);
52
64
  return { ok: true, out: out?.toString()?.trim() || '' };
@@ -55,6 +67,10 @@ function run(cmd, opts = {}) {
55
67
  }
56
68
  }
57
69
 
70
+ function runPwsh(cmd, opts = {}) {
71
+ return run(`powershell -NoProfile -Command "${cmd}"`, opts);
72
+ }
73
+
58
74
  function spinner(ms) {
59
75
  return new Promise(resolve => setTimeout(resolve, ms));
60
76
  }
@@ -68,89 +84,259 @@ function step(msg) {
68
84
  };
69
85
  }
70
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
+
71
99
  function printSummary(info) {
100
+ const dashUrl = `http://localhost:${info.port || 9119}`;
101
+
72
102
  console.log(`\n${BOLD}${GREEN} ✅ Hermes Agent is live!${RESET}\n`);
73
- console.log(` ${BOLD}Web Dashboard${RESET}`);
74
- console.log(` ${CYAN}http://localhost:${info.port || 9119}${RESET}\n`);
75
- 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}`);
76
112
  console.log(` ${CYAN}hermes${RESET} interactive chat`);
77
113
  console.log(` ${CYAN}hermes chat -q "..."${RESET} one-shot query`);
78
114
  console.log(` ${CYAN}hermes --continue${RESET} resume last session\n`);
79
- console.log(` ${BOLD}Key commands${RESET}`);
115
+
116
+ console.log(` ${BOLD}⚡ Key commands${RESET}`);
80
117
  console.log(` ${CYAN}hermes model${RESET} change model/provider`);
81
118
  console.log(` ${CYAN}hermes gateway setup${RESET} connect Telegram/Discord`);
82
119
  console.log(` ${CYAN}hermes tools list${RESET} see available tools`);
83
- console.log(` ${CYAN}hermes cron${RESET} schedule recurring tasks`);
84
120
  console.log(` ${CYAN}hermes doctor${RESET} health check\n`);
85
- console.log(` ${BOLD}Docs${RESET}`);
121
+
122
+ console.log(` ${BOLD}📖 Docs${RESET}`);
86
123
  console.log(` ${CYAN}https://hermes-agent.nousresearch.com/docs${RESET}\n`);
124
+
87
125
  if (info.messaging) {
88
- console.log(` ${BOLD}Messaging${RESET}`);
126
+ console.log(` ${BOLD}💬 Messaging${RESET}`);
89
127
  for (const [platform, status] of Object.entries(info.messaging)) {
90
128
  console.log(` ${platform}: ${status}`);
91
129
  }
92
130
  console.log();
93
131
  }
132
+ if (info.windowsNote) {
133
+ console.log(` ${YELLOW} ${info.windowsNote}${RESET}\n`);
134
+ }
94
135
  }
95
136
 
96
- function printMiniBanner() {
97
- console.log(`
98
- ${CYAN} 🐚 Hermes Launch${RESET} ${DIM}— zero-to-agent${RESET}
99
- `);
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;
100
155
  }
101
156
 
102
- // ——— core steps ———
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 };
249
+ }
250
+
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
+ }
103
272
 
104
273
  async function checkPrereqs() {
105
274
  const s = step('Checking prerequisites');
106
- const checks = {
107
- node: run('node --version', { silent: true }).ok,
108
- python3: run('python3 --version', { silent: true }).ok,
109
- curl: run('curl --version', { silent: true }).ok,
110
- git: run('git --version', { silent: true }).ok,
111
- };
112
275
 
113
- const missing = Object.entries(checks).filter(([, ok]) => !ok).map(([name]) => name);
114
- if (missing.length) {
115
- s.fail(`Missing: ${missing.join(', ')}`);
116
- console.log(` Install missing tools and try again.`);
117
- process.exit(1);
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
+ }
118
298
  }
119
299
 
120
- // Check Python version >= 3.10 — try multiple binaries
121
- const pyCandidates = ['python3.11', 'python3.12', 'python3.13', 'python3.14', 'python3.10', 'python3'];
122
- let pyVer = '';
123
- let pyBin = 'python3';
124
- for (const bin of pyCandidates) {
125
- const r = run(`${bin} --version`, { silent: true });
126
- if (r.ok) {
127
- pyVer = r.out;
128
- pyBin = bin;
129
- break;
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);
130
308
  }
131
309
  }
132
310
 
133
- const match = pyVer.match(/(\d+)\.(\d+)/);
134
- if (match) {
135
- const major = parseInt(match[1]), minor = parseInt(match[2]);
136
- if (major < 3 || (major === 3 && minor < 10)) {
137
- s.fail(`Python 3.10+ required, got ${major}.${minor} (${pyBin})`);
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');
138
318
  process.exit(1);
139
319
  }
140
320
  }
141
321
 
142
322
  s.ok();
143
- checks.pythonBin = pyBin;
144
- return checks;
323
+ return { ...py, pkgMan, curlBin: curlCheck.bin || '', gitBin: gitCheck.bin || '' };
145
324
  }
146
325
 
147
- async function installHermes(quick) {
326
+ // ——— Hermes install ———
327
+
328
+ async function installHermes(quick, platformInfo) {
148
329
  const s = step('Checking Hermes installation');
149
- const hasHermes = run('hermes --version', { silent: true }).ok;
150
330
 
151
- if (hasHermes) {
152
- const ver = run('hermes --version', { silent: true }).out;
153
- s.skip(`v${ver} already installed`);
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`);
154
340
  return true;
155
341
  }
156
342
 
@@ -158,27 +344,58 @@ async function installHermes(quick) {
158
344
  console.log();
159
345
  const ans = await ask(` Hermes not found. Install it now? [Y/n] `);
160
346
  if (ans.toLowerCase() === 'n') {
161
- console.log(` ${YELLOW}Aborted. Install manually: curl -fsSL ${INSTALL_URL} | bash${RESET}`);
347
+ console.log(` ${YELLOW}Aborted.${RESET}`);
162
348
  process.exit(0);
163
349
  }
164
350
  }
165
351
 
166
352
  const si = step('Downloading and installing Hermes');
167
- try {
168
- const cmd = `curl -fsSL ${INSTALL_URL} | bash`;
169
- const result = run(cmd, { timeout: 180_000, silent: false });
170
- if (!result.ok) {
171
- si.fail(result.out);
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);
172
392
  process.exit(1);
173
393
  }
174
- si.ok();
175
- return true;
176
- } catch (e) {
177
- si.fail(e.message);
178
- process.exit(1);
179
394
  }
180
395
  }
181
396
 
397
+ // ——— config ———
398
+
182
399
  async function checkConfig(quick) {
183
400
  const s = step('Checking Hermes configuration');
184
401
 
@@ -189,7 +406,6 @@ async function checkConfig(quick) {
189
406
 
190
407
  const raw = readFileSync(CONFIG_PATH, 'utf-8');
191
408
 
192
- // Check if config has a model set (not commented out)
193
409
  const hasModel = /^model:\s*\n/m.test(raw);
194
410
 
195
411
  if (!hasModel || raw.includes('default: ""') || raw.includes("default: ''")) {
@@ -204,17 +420,13 @@ async function checkConfig(quick) {
204
420
  async function runSetup(quick) {
205
421
  if (quick) {
206
422
  const s = step('Running headless setup');
207
- // For quick mode, just enable the gateway and set up with defaults
208
- const setCmds = [
209
- 'hermes tools enable web terminal file skills session_search delegate_task memory clarify vision 2>/dev/null || true',
210
- `hermes config set display.streaming.enabled true 2>/dev/null || true`,
211
- ];
212
- 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 });
213
426
  s.ok();
214
427
  return;
215
428
  }
216
429
 
217
- // Interactive: guide the user through setup
218
430
  console.log(`\n ${BOLD}Let's configure your Hermes Agent.${RESET}\n`);
219
431
  console.log(` ${DIM}(You can skip any step and configure later with \`hermes setup\`)${RESET}\n`);
220
432
 
@@ -223,18 +435,15 @@ async function runSetup(quick) {
223
435
  const s = step('Running setup wizard');
224
436
  try {
225
437
  const result = run('hermes setup', { stdio: 'inherit', timeout: 300_000 });
226
- if (!result.ok) {
227
- s.fail(result.out);
228
- } else {
229
- s.ok();
230
- }
438
+ if (!result.ok) s.fail(result.out);
439
+ else s.ok();
231
440
  } catch (e) {
232
441
  s.fail(e.message);
233
442
  }
234
443
  } else {
235
- // Minimal quick config
236
444
  const s = step('Applying minimal configuration');
237
- run('hermes tools enable web terminal file skills session_search delegate_task memory 2>/dev/null || true', { 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 });
238
447
  s.skip('minimal config applied');
239
448
  }
240
449
  }
@@ -242,70 +451,71 @@ async function runSetup(quick) {
242
451
  async function enableToolsets(quick) {
243
452
  const s = step('Enabling tool sets');
244
453
 
245
- // Core toolsets that make Hermes useful
246
- const toolsets = [
247
- 'web', // web search + content extraction
248
- 'terminal', // shell commands
249
- 'file', // file read/write/search
250
- 'skills', // skill management
251
- 'session_search', // cross-session recall
252
- 'delegate_task', // spawn subagents
253
- 'memory', // cross-session memory
254
- 'vision', // image analysis
255
- 'tts', // text-to-speech
256
- 'clarify', // ask questions
257
- ];
258
-
259
- let ok = true;
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';
260
456
  for (const t of toolsets) {
261
- const r = run(`hermes tools enable ${t}`, { silent: true });
262
- if (!r.ok) ok = false;
263
- }
264
-
265
- if (ok) {
266
- s.ok();
267
- } else {
268
- // Some may have failed if already enabled — that's fine
269
- s.ok();
457
+ run(`hermes tools enable ${t} ${noop}`, { silent: true });
270
458
  }
271
459
 
272
- // Enable gateway streaming
273
- run('hermes config set display.streaming.enabled true 2>/dev/null || true', { silent: true });
460
+ run(`hermes config set display.streaming.enabled true ${noop}`, { silent: true });
461
+ s.ok();
274
462
  }
275
463
 
464
+ // ——— FIXED: dashboard launches as a detached process that survives after hermes-launch exits ———
465
+
276
466
  async function startDashboard(quick) {
277
467
  const s = step('Starting web dashboard');
278
- const port = quick ? '9119' : '9119';
468
+ const port = '9119';
469
+ const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
470
+ const dashUrl = `http://localhost:${port}`;
279
471
 
280
- // Check if dashboard is already running
281
- const statusCheck = run('hermes dashboard --status', { silent: true });
472
+ // Check if already running
473
+ const statusCheck = run(`hermes dashboard --status ${noop}`, { silent: true });
282
474
  if (statusCheck.ok && statusCheck.out.length > 0 && !statusCheck.out.includes('No running')) {
283
475
  s.skip('already running');
476
+ if (!quick) openBrowser(dashUrl);
284
477
  return { port, alreadyRunning: true };
285
478
  }
286
479
 
287
480
  try {
481
+ // Spawn detached — survives after this script exits
288
482
  const proc = spawn(
289
483
  'hermes',
290
- ['dashboard', '--port', port, '--tui', quick ? '--no-open' : ''],
484
+ ['dashboard', '--port', port, '--tui', '--no-open', '--skip-build'],
291
485
  {
292
- stdio: 'inherit',
293
- detached: false,
486
+ stdio: ['ignore', 'ignore', 'ignore'],
487
+ detached: true,
488
+ shell: IS_WIN,
294
489
  env: { ...process.env },
295
490
  }
296
491
  );
297
492
 
298
- // Give it a moment to start
299
- 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 });
300
505
 
301
- // Verify it's up
302
- const verify = run(`curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:${port}/ 2>/dev/null || echo "0"`, { silent: true });
303
506
  if (verify.out === '0') {
304
- s.skip('dashboard launched in background');
507
+ s.skip('started in background (may take a few seconds)');
305
508
  } else {
306
509
  s.ok();
307
510
  }
308
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
+
309
519
  return { port, alreadyRunning: false };
310
520
  } catch (e) {
311
521
  s.fail(e.message);
@@ -315,7 +525,8 @@ async function startDashboard(quick) {
315
525
 
316
526
  async function checkGatewayStatus() {
317
527
  const s = step('Checking messaging gateway');
318
- const result = run('hermes gateway status', { silent: true });
528
+ const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
529
+ const result = run(`hermes gateway status ${noop}`, { silent: true });
319
530
  if (result.ok) {
320
531
  const out = result.out;
321
532
  const hasTelegram = out.includes('telegram') && (out.includes('running') || out.includes('connected'));
@@ -332,27 +543,6 @@ async function checkGatewayStatus() {
332
543
  return [];
333
544
  }
334
545
 
335
- async function showQuickStart() {
336
- console.log(`\n ${BOLD}${GREEN}Hermes Agent is ready to use!${RESET}\n`);
337
-
338
- console.log(` ${BOLD}Start a conversation:${RESET}`);
339
- console.log(` ${CYAN}hermes${RESET} interactive chat`);
340
- console.log(` ${CYAN}hermes -q "summarize my repo"${RESET} one-shot query\n`);
341
-
342
- console.log(` ${BOLD}First things to try:${RESET}`);
343
- console.log(` • ${CYAN}hermes model${RESET} — pick your LLM provider`);
344
- console.log(` • ${CYAN}hermes gateway setup${RESET} — connect Telegram/Discord`);
345
- console.log(` • ${CYAN}hermes dashboard${RESET} — open the web UI`);
346
- console.log(` • ${CYAN}hermes skills browse${RESET} — discover skills\n`);
347
-
348
- console.log(` ${BOLD}Pro tips:${RESET}`);
349
- console.log(` • ${DIM}Your config lives at ~/.hermes/config.yaml${RESET}`);
350
- console.log(` • ${DIM}API keys go in ~/.hermes/.env${RESET}`);
351
- console.log(` • ${DIM}Type /help in a session for all slash commands${RESET}`);
352
- console.log(` • ${DIM}Run \`hermes doctor\` if something isn't working${RESET}`);
353
- console.log();
354
- }
355
-
356
546
  // ——— main ———
357
547
 
358
548
  async function main() {
@@ -376,16 +566,24 @@ async function main() {
376
566
 
377
567
  ${BOLD}What it does:${RESET}
378
568
  1. Check prerequisites (Python, curl, git)
379
- 2. Install Hermes Agent (if not present)
380
- 3. Run setup wizard to configure model/tools
381
- 4. Enable useful toolsets
382
- 5. Start the web dashboard on port 9119
383
- 6. Print next steps
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
384
575
 
385
576
  ${BOLD}After launch:${RESET}
386
577
  hermes start chatting
387
578
  hermes gateway setup add Telegram/Discord
388
579
  hermes model change AI provider
580
+
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
389
587
  `);
390
588
  process.exit(0);
391
589
  }
@@ -397,11 +595,8 @@ async function main() {
397
595
  }
398
596
 
399
597
  if (flags.dashboard) {
400
- printMiniBanner();
401
- const result = await startDashboard(true);
402
- if (result.error) {
403
- process.exit(1);
404
- }
598
+ const result = await startDashboard(false);
599
+ if (result.error) process.exit(1);
405
600
  printSummary({ port: result.port, messaging: {} });
406
601
  process.exit(0);
407
602
  }
@@ -409,11 +604,15 @@ async function main() {
409
604
  // ——— full flow ———
410
605
  printBanner();
411
606
 
412
- // 1. Prerequisites
413
- await checkPrereqs();
607
+ if (IS_WIN) {
608
+ console.log(` ${DIM}Windows detected — auto-installing missing deps${RESET}\n`);
609
+ }
610
+
611
+ // 1. Prerequisites (auto-installs Python, git, curl if missing)
612
+ const platformInfo = await checkPrereqs();
414
613
 
415
- // 2. Install
416
- await installHermes(flags.quick);
614
+ // 2. Install Hermes
615
+ await installHermes(flags.quick, platformInfo);
417
616
 
418
617
  // 3. Check config
419
618
  const needsSetup = await checkConfig(flags.quick);
@@ -426,19 +625,25 @@ async function main() {
426
625
  // 5. Enable tools
427
626
  await enableToolsets(flags.quick);
428
627
 
429
- // 6. Start dashboard
628
+ // 6. Start dashboard (detached — survives after script exits)
430
629
  const dashResult = await startDashboard(flags.quick);
431
630
 
432
631
  // 7. Check gateway
433
632
  const platforms = await checkGatewayStatus();
434
633
 
435
634
  // 8. Show summary
436
- printSummary({
635
+ const summary = {
437
636
  port: dashResult.port,
438
637
  messaging: platforms.length > 0
439
638
  ? Object.fromEntries(platforms.map(p => [p, `${GREEN}connected${RESET}`]))
440
639
  : { none: `${YELLOW}not configured${RESET} (run \`hermes gateway setup\`)` },
441
- });
640
+ };
641
+
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
+ }
645
+
646
+ printSummary(summary);
442
647
  }
443
648
 
444
649
  main().catch(e => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermes-launch",
3
- "version": "1.0.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"