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