hermes-launch 1.2.0 → 2.1.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.
- package/README.md +60 -48
- package/bin/hermes-launch.js +127 -583
- package/package.json +2 -2
- package/src/server.js +651 -0
package/bin/hermes-launch.js
CHANGED
|
@@ -1,89 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* hermes-launch —
|
|
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 #
|
|
7
|
-
* npx hermes-launch --
|
|
8
|
-
* npx hermes-launch --
|
|
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();
|
|
21
|
+
const PLATFORM = osPlatform();
|
|
19
22
|
const IS_WIN = PLATFORM === 'win32';
|
|
20
23
|
const IS_MAC = PLATFORM === 'darwin';
|
|
21
|
-
const
|
|
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,
|
|
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 (_) {
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
hermes
|
|
578
|
-
hermes
|
|
579
|
-
hermes
|
|
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
|
-
|
|
582
|
-
|
|
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(
|
|
593
|
-
console.log(
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
//
|
|
612
|
-
|
|
109
|
+
// Print banner
|
|
110
|
+
console.log(`
|
|
111
|
+
🐚 Hermes Launch
|
|
112
|
+
${'─'.repeat(30)}
|
|
113
|
+
Starting the web setup wizard...
|
|
114
|
+
`);
|
|
613
115
|
|
|
614
|
-
//
|
|
615
|
-
|
|
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
|
-
|
|
618
|
-
|
|
154
|
+
proc.stdout.on('data', () => {
|
|
155
|
+
if (started) {
|
|
156
|
+
clearTimeout(timeout);
|
|
157
|
+
resolve();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
619
161
|
|
|
620
|
-
//
|
|
621
|
-
if (
|
|
622
|
-
|
|
162
|
+
// Save pid
|
|
163
|
+
if (proc.pid) {
|
|
164
|
+
try { writeFileSync(PID_FILE, String(proc.pid)); } catch {}
|
|
623
165
|
}
|
|
624
166
|
|
|
625
|
-
//
|
|
626
|
-
|
|
167
|
+
// Detach — let the server live on
|
|
168
|
+
proc.unref();
|
|
627
169
|
|
|
628
|
-
|
|
629
|
-
|
|
170
|
+
if (started) {
|
|
171
|
+
// Remove pipe listeners so event loop can exit
|
|
172
|
+
proc.stdout.removeAllListeners();
|
|
173
|
+
proc.stderr.removeAllListeners();
|
|
630
174
|
|
|
631
|
-
|
|
632
|
-
const platforms = await checkGatewayStatus();
|
|
175
|
+
console.log(`\n ${'✅'} Wizard ready at ${'http://localhost:' + flags.port}`);
|
|
633
176
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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(
|
|
194
|
+
console.error('Error:', e.message);
|
|
651
195
|
process.exit(1);
|
|
652
196
|
});
|