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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hermes-launch",
3
- "version": "1.1.0",
4
- "description": "One command to launch Hermes Agent — zero-to-AI-agent in seconds",
3
+ "version": "2.0.0",
4
+ "description": "Web onboarding wizard for Hermes Agent — install, configure, and launch from your browser",
5
5
  "bin": {
6
6
  "hermes-launch": "./bin/hermes-launch.js"
7
7
  },
package/src/server.js ADDED
@@ -0,0 +1,599 @@
1
+ /**
2
+ * hermes-launch web server — serves the onboarding wizard + API endpoints.
3
+ * Zero npm deps — uses Node.js built-in http module.
4
+ */
5
+
6
+ import http from 'node:http';
7
+ import { execSync, spawn } from 'node:child_process';
8
+ import { readFileSync, existsSync } from 'node:fs';
9
+ import { homedir, platform as osPlatform } from 'node:os';
10
+ import { resolve } from 'node:path';
11
+
12
+ const PLATFORM = osPlatform();
13
+ const IS_WIN = PLATFORM === 'win32';
14
+ const IS_MAC = PLATFORM === 'darwin';
15
+ const IS_LINUX = PLATFORM === 'linux';
16
+
17
+ const HERMES_HOME = IS_WIN
18
+ ? resolve(process.env.USERPROFILE || homedir(), '.hermes')
19
+ : resolve(homedir(), '.hermes');
20
+ const CONFIG_PATH = resolve(HERMES_HOME, 'config.yaml');
21
+ const INSTALL_URL = 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh';
22
+
23
+ // ——— helpers ———
24
+
25
+ function run(cmd, opts = {}) {
26
+ const defaults = { stdio: 'pipe', timeout: opts.timeout || 120_000, shell: IS_WIN };
27
+ try {
28
+ const out = execSync(cmd, { ...defaults, ...opts });
29
+ return { ok: true, out: out?.toString()?.trim() || '', code: 0 };
30
+ } catch (e) {
31
+ return { ok: false, out: e.stderr?.toString()?.trim() || e.message, code: e.status || 1 };
32
+ }
33
+ }
34
+
35
+ // ——— API handlers ———
36
+
37
+ const api = {};
38
+
39
+ api.status = async () => {
40
+ const py = findPython();
41
+ const hasHermes = run('hermes --version', { silent: true }).ok;
42
+ let hermesVersion = '';
43
+ if (hasHermes) {
44
+ const r = run('hermes --version', { silent: true });
45
+ hermesVersion = r.out.split('\n')[0] || 'installed';
46
+ }
47
+ const hasConfig = existsSync(CONFIG_PATH);
48
+ let configOk = false;
49
+ if (hasConfig) {
50
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
51
+ configOk = /^model:\s*\n/m.test(raw) && !raw.includes('default: ""');
52
+ }
53
+
54
+ // Check dashboard
55
+ const dashRunning = run('curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9119/ 2>/dev/null || echo 0', { silent: true }).out === '200';
56
+
57
+ const tools = ['web', 'terminal', 'file', 'skills', 'session_search', 'delegate_task', 'memory', 'vision', 'tts', 'clarify'];
58
+
59
+ return {
60
+ platform: PLATFORM,
61
+ python: py,
62
+ hermes: { installed: hasHermes, version: hermesVersion },
63
+ config: { exists: hasConfig, configured: configOk },
64
+ dashboard: { running: dashRunning, url: 'http://localhost:9119' },
65
+ tools: tools.map(t => ({
66
+ name: t,
67
+ enabled: run(`hermes tools list 2>/dev/null | grep -q "${t}.*enabled" || echo no`, { silent: true }).out.includes('no') === false,
68
+ })),
69
+ };
70
+ };
71
+
72
+ api.install = async () => {
73
+ if (IS_WIN) {
74
+ const py = findPython();
75
+ if (!py.found) return { ok: false, step: 'python', msg: 'Python not found. Install it first.' };
76
+ run(`${py.bin} -m pip install --upgrade pip`, { silent: true, timeout: 60_000 });
77
+ const r = run(`${py.bin} -m pip install hermes-agent`, { timeout: 180_000 });
78
+ if (!r.ok) return { ok: false, step: 'pip', msg: r.out };
79
+ return { ok: true, msg: 'Hermes installed via pip' };
80
+ }
81
+ // macOS / Linux
82
+ const r = run(`curl -fsSL ${INSTALL_URL} | bash`, { timeout: 180_000 });
83
+ if (!r.ok) return { ok: false, step: 'install', msg: r.out };
84
+ return { ok: true, msg: 'Hermes installed successfully' };
85
+ };
86
+
87
+ api.setupModel = async (model, provider) => {
88
+ let r;
89
+ if (provider) {
90
+ r = run(`hermes config set model.provider "${provider}"`, { silent: true });
91
+ if (!r.ok) return { ok: false, msg: r.out };
92
+ }
93
+ if (model) {
94
+ r = run(`hermes config set model.default "${model}"`, { silent: true });
95
+ if (!r.ok) return { ok: false, msg: r.out };
96
+ }
97
+ return { ok: true, msg: `Model configured: ${provider || 'default'} / ${model || 'default'}` };
98
+ };
99
+
100
+ api.listModels = async () => {
101
+ const r = run('hermes model --help 2>/dev/null || echo "picker"', { silent: true });
102
+ return {
103
+ providers: [
104
+ { name: 'opencode-go', label: 'OpenCode Go', models: ['deepseek-v4-flash', 'big-pickle', 'deepseek-v3.1'] },
105
+ { name: 'openrouter', label: 'OpenRouter', models: ['anthropic/claude-sonnet-4', 'deepseek/deepseek-v4-flash', 'google/gemini-2.5-pro-preview-03-25', 'openai/gpt-5.4'] },
106
+ { name: 'anthropic', label: 'Anthropic', models: ['claude-sonnet-4-20250514', 'claude-3.5-haiku'] },
107
+ { name: 'deepseek', label: 'DeepSeek', models: ['deepseek-chat', 'deepseek-reasoner'] },
108
+ ],
109
+ };
110
+ };
111
+
112
+ api.enableTools = async (tools) => {
113
+ const noop = IS_WIN ? 'ver>nul' : '2>/dev/null || true';
114
+ const results = [];
115
+ for (const t of tools) {
116
+ const r = run(`hermes tools enable ${t} ${noop}`, { silent: true });
117
+ results.push({ name: t, ok: r.ok });
118
+ }
119
+ return { ok: true, results };
120
+ };
121
+
122
+ api.startDashboard = async () => {
123
+ const running = run('curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9119/ 2>/dev/null || echo 0', { silent: true });
124
+ if (running.out === '200') return { ok: true, alreadyRunning: true, url: 'http://localhost:9119' };
125
+
126
+ const proc = spawn('hermes', ['dashboard', '--port', '9119', '--tui', '--no-open', '--skip-build'], {
127
+ stdio: ['ignore', 'ignore', 'ignore'],
128
+ detached: true,
129
+ shell: IS_WIN,
130
+ env: { ...process.env },
131
+ });
132
+ proc.on('error', () => {});
133
+ if (proc.unref) proc.unref();
134
+
135
+ // Wait for it to come up
136
+ for (let i = 0; i < 15; i++) {
137
+ await new Promise(r => setTimeout(r, 1000));
138
+ const check = run('curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9119/ 2>/dev/null || echo 0', { silent: true });
139
+ if (check.out === '200') return { ok: true, alreadyRunning: false, url: 'http://localhost:9119' };
140
+ }
141
+ return { ok: true, alreadyRunning: false, url: 'http://localhost:9119', note: 'started — may take a moment' };
142
+ };
143
+
144
+ api.gatewayStatus = async () => {
145
+ const r = run('hermes gateway status 2>/dev/null || echo not-configured', { silent: true });
146
+ return { raw: r.out };
147
+ };
148
+
149
+ api.runCommand = async (cmd) => {
150
+ const r = run(cmd, { timeout: 60_000 });
151
+ return { ok: r.ok, out: r.out, code: r.code };
152
+ };
153
+
154
+ // ——— python detection ———
155
+
156
+ function findPython() {
157
+ const candidates = IS_WIN
158
+ ? ['python', 'py -3', 'python3']
159
+ : ['python3.14', 'python3.13', 'python3.12', 'python3.11', 'python3.10', 'python3'];
160
+
161
+ for (const bin of candidates) {
162
+ const r = run(`${bin} --version`, { silent: true });
163
+ if (r.ok) return { found: true, bin, version: r.out };
164
+ }
165
+ return { found: false, bin: null, version: null };
166
+ }
167
+
168
+ // ——— parse request body ———
169
+
170
+ function parseBody(req) {
171
+ return new Promise(resolve => {
172
+ let body = '';
173
+ req.on('data', c => body += c);
174
+ req.on('end', () => {
175
+ try { resolve(JSON.parse(body)); }
176
+ catch { resolve({}); }
177
+ });
178
+ });
179
+ }
180
+
181
+ // ——— send JSON ———
182
+
183
+ function json(res, code, data) {
184
+ const str = JSON.stringify(data);
185
+ res.writeHead(code, {
186
+ 'Content-Type': 'application/json',
187
+ 'Access-Control-Allow-Origin': '*',
188
+ 'Content-Length': Buffer.byteLength(str),
189
+ });
190
+ res.end(str);
191
+ }
192
+
193
+ function html(res, content) {
194
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
195
+ res.end(content);
196
+ }
197
+
198
+ // ——— serve the onboarding page ———
199
+
200
+ function serveOnboarding() {
201
+ return `<!DOCTYPE html>
202
+ <html lang="en">
203
+ <head>
204
+ <meta charset="UTF-8">
205
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
206
+ <title>Hermes Launch</title>
207
+ <style>
208
+ :root { --bg: #0a0a0f; --card: #12121a; --border: #1e1e2e; --text: #cdd6f4; --dim: #6c7086; --accent: #89b4fa; --green: #a6e3a1; --yellow: #f9e2af; --red: #f38ba8; --radius: 12px; }
209
+ * { margin: 0; padding: 0; box-sizing: border-box; }
210
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
211
+ .app { max-width: 720px; margin: 0 auto; padding: 32px 16px; }
212
+ h1 { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
213
+ .subtitle { color: var(--dim); font-size: 14px; margin-bottom: 32px; }
214
+ .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 16px; }
215
+ .card h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
216
+ .step-num { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; background: var(--border); font-size: 12px; font-weight: 700; }
217
+ .step-done .step-num { background: var(--green); color: #000; }
218
+ .step-active .step-num { background: var(--accent); color: #000; }
219
+ .step-active { border-color: var(--accent); }
220
+ .step-done { border-color: var(--green); opacity: 0.8; }
221
+ .status { font-size: 13px; color: var(--dim); margin-bottom: 8px; }
222
+ .status.ok { color: var(--green); }
223
+ .status.fail { color: var(--red); }
224
+ .status.pending { color: var(--yellow); }
225
+ .btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border); background: var(--card); color: var(--text); font-size: 14px; cursor: pointer; transition: all .15s; }
226
+ .btn:hover { border-color: var(--accent); background: #1a1a2e; }
227
+ .btn-primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 600; }
228
+ .btn-primary:hover { background: #9bbafc; }
229
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
230
+ .btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
231
+ .log { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 12px; background: #050508; border-radius: 8px; padding: 12px; max-height: 200px; overflow-y: auto; white-space: pre-wrap; color: var(--dim); margin-top: 8px; }
232
+ .log .ok { color: var(--green); }
233
+ .log .fail { color: var(--red); }
234
+ .log .info { color: var(--accent); }
235
+ select, input[type="text"] { width: 100%; padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: 14px; margin-bottom: 8px; }
236
+ select { cursor: pointer; }
237
+ label { display: block; font-size: 13px; color: var(--dim); margin-bottom: 4px; margin-top: 8px; }
238
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
239
+ .tag { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 20px; font-size: 12px; background: var(--border); }
240
+ .tag.enabled { background: #1a3a1a; color: var(--green); border: 1px solid #2a5a2a; }
241
+ .tag.disabled { background: #3a1a1a; color: var(--red); border: 1px solid #5a2a2a; }
242
+ .link { color: var(--accent); text-decoration: none; }
243
+ .link:hover { text-decoration: underline; }
244
+ .flex { display: flex; align-items: center; gap: 8px; }
245
+ .spinner { width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .6s linear infinite; display: inline-block; }
246
+ @keyframes spin { to { transform: rotate(360deg); } }
247
+ .dash-url { font-family: 'SF Mono', monospace; font-size: 18px; font-weight: 700; color: var(--accent); text-align: center; padding: 16px; background: #050508; border-radius: 8px; margin-top: 8px; }
248
+ .footer { text-align: center; color: var(--dim); font-size: 13px; margin-top: 32px; }
249
+ </style>
250
+ </head>
251
+ <body>
252
+ <div class="app" id="app">
253
+ <h1>🐚 Hermes Launch</h1>
254
+ <p class="subtitle">Set up your AI agent in minutes — no terminal fiddling.</p>
255
+
256
+ <div id="steps"></div>
257
+ <div id="footer" class="footer"></div>
258
+ </div>
259
+
260
+ <script>
261
+ const $ = s => document.querySelector(s);
262
+ const $$ = s => document.querySelectorAll(s);
263
+
264
+ async function api(path, body) {
265
+ const opts = { headers: { 'Accept': 'application/json' } };
266
+ if (body) { opts.method = 'POST'; opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
267
+ const r = await fetch(path, opts);
268
+ return r.json();
269
+ }
270
+
271
+ function log(msg, type = 'info') {
272
+ const el = document.getElementById('activity-log');
273
+ if (!el) return;
274
+ el.innerHTML += '<span class="' + type + '">' + msg.replace(/</g, '&lt;') + '</span>\\n';
275
+ el.scrollTop = el.scrollHeight;
276
+ }
277
+
278
+ const STEPS = [
279
+ { id: 'prereqs', label: 'Checking prerequisites', run: async () => {
280
+ const s = await api('/api/status');
281
+ const ok = s.python.found;
282
+ document.getElementById('prereqs-status').innerHTML = ok
283
+ ? '<span class="status ok">✅ Python ' + (s.python.version || '') + '</span>'
284
+ : '<span class="status fail">❌ Python not found</span>';
285
+ return ok;
286
+ }},
287
+ { id: 'install', label: 'Install Hermes Agent', run: async () => {
288
+ const s = await api('/api/status');
289
+ if (s.hermes.installed) {
290
+ document.getElementById('install-status').innerHTML = '✅ Already installed <span class="tag">' + (s.hermes.version || '') + '</span>';
291
+ return true;
292
+ }
293
+ const btn = document.getElementById('install-btn');
294
+ const st = document.getElementById('install-status');
295
+ if (btn) btn.disabled = true;
296
+ st.innerHTML = '⏳ Installing... <span class="spinner"></span>';
297
+ const r = await api('/api/install');
298
+ st.innerHTML = r.ok ? '✅ Installed successfully' : '❌ Failed: ' + r.msg;
299
+ if (r.ok && btn) btn.style.display = 'none';
300
+ return r.ok;
301
+ }},
302
+ { id: 'config', label: 'Configure model & provider', run: async () => {
303
+ const s = await api('/api/status');
304
+ if (s.config.configured) {
305
+ document.getElementById('config-status').innerHTML = '✅ Already configured';
306
+ document.getElementById('config-area').style.display = 'none';
307
+ return true;
308
+ }
309
+ document.getElementById('config-area').style.display = 'block';
310
+ return null; // wait for user
311
+ }},
312
+ { id: 'tools', label: 'Enable tools', run: async () => {
313
+ const s = await api('/api/status');
314
+ const toolEl = document.getElementById('tools-area');
315
+ const allOn = s.tools.every(t => t.enabled);
316
+ if (allOn) {
317
+ document.getElementById('tools-status').innerHTML = '✅ All enabled';
318
+ return true;
319
+ }
320
+ toolEl.style.display = 'block';
321
+ renderTools(s.tools);
322
+ return null;
323
+ }},
324
+ { id: 'dashboard', label: 'Start web dashboard', run: async () => {
325
+ const s = await api('/api/status');
326
+ if (s.dashboard.running) {
327
+ document.getElementById('dash-status').innerHTML = '✅ Running at <a href="http://localhost:9119" class="link" target="_blank">localhost:9119</a>';
328
+ document.getElementById('dash-url').textContent = 'http://localhost:9119';
329
+ document.getElementById('dash-area').style.display = 'block';
330
+ return true;
331
+ }
332
+ const btn = document.getElementById('dash-btn');
333
+ const st = document.getElementById('dash-status');
334
+ if (btn) btn.disabled = true;
335
+ st.innerHTML = '⏳ Starting dashboard... <span class="spinner"></span>';
336
+ const r = await api('/api/start-dashboard');
337
+ st.innerHTML = '✅ Running at <a href="http://localhost:9119" class="link" target="_blank">localhost:9119</a>';
338
+ document.getElementById('dash-url').textContent = 'http://localhost:9119';
339
+ document.getElementById('dash-area').style.display = 'block';
340
+ return true;
341
+ }},
342
+ ];
343
+
344
+ async function renderSteps() {
345
+ const container = document.getElementById('steps');
346
+ container.innerHTML = '';
347
+ for (let i = 0; i < STEPS.length; i++) {
348
+ const step = STEPS[i];
349
+ const card = document.createElement('div');
350
+ card.className = 'card';
351
+ card.id = 'card-' + step.id;
352
+ card.innerHTML = '<h2><span class="step-num">' + (i + 1) + '</span>' + step.label + '</h2>'
353
+ + '<div id="' + step.id + '-status" class="status pending">⏳ pending</div>'
354
+ + '<div id="' + step.id + '-area" style="display:none"></div>';
355
+ container.appendChild(card);
356
+ }
357
+
358
+ // Config area
359
+ const configArea = document.getElementById('config-area');
360
+ configArea.innerHTML = '<label>Provider</label><select id="provider-select"></select>'
361
+ + '<label>Model</label><select id="model-select"></select>'
362
+ + '<div class="btn-group"><button class="btn btn-primary" onclick="saveConfig()">Save Configuration</button><button class="btn" onclick="skipConfig()">Skip</button></div>'
363
+ + '<div id="config-result"></div>'
364
+ + '<div class="log" id="activity-log" style="margin-top:12px"></div>';
365
+
366
+ // Tools area
367
+ document.getElementById('tools-area').innerHTML = '<div id="tools-grid" class="grid-2"></div>'
368
+ + '<div class="btn-group"><button class="btn btn-primary" onclick="saveTools()">Enable Selected</button><button class="btn" onclick="enableAllTools()">Enable All</button><button class="btn" onclick="skipTools()">Skip</button></div>'
369
+ + '<div id="tools-result"></div>';
370
+
371
+ // Dashboard area
372
+ document.getElementById('dash-area').innerHTML = '<div class="dash-url" id="dash-url">starting...</div>'
373
+ + '<div class="btn-group"><a href="http://localhost:9119" class="btn btn-primary" target="_blank">Open Dashboard</a></div>';
374
+
375
+ // Load providers
376
+ try {
377
+ const models = await api('/api/models');
378
+ const prov = document.getElementById('provider-select');
379
+ models.providers.forEach(p => {
380
+ const opt = document.createElement('option');
381
+ opt.value = p.name;
382
+ opt.textContent = p.label;
383
+ prov.appendChild(opt);
384
+ });
385
+ updateModels();
386
+ prov.onchange = updateModels;
387
+ } catch (e) {
388
+ document.getElementById('config-area').innerHTML = '<div class="status fail">Failed to load model list: ' + e.message + '</div>';
389
+ }
390
+
391
+ // Run through steps
392
+ runSteps();
393
+ }
394
+
395
+ async function updateModels() {
396
+ const sel = document.getElementById('provider-select');
397
+ const modelSel = document.getElementById('model-select');
398
+ modelSel.innerHTML = '';
399
+ const models = await api('/api/models');
400
+ const prov = models.providers.find(p => p.name === sel.value);
401
+ if (prov) {
402
+ prov.models.forEach(m => {
403
+ const opt = document.createElement('option');
404
+ opt.value = m;
405
+ opt.textContent = m;
406
+ modelSel.appendChild(opt);
407
+ });
408
+ }
409
+ }
410
+
411
+ async function saveConfig() {
412
+ const provider = document.getElementById('provider-select').value;
413
+ const model = document.getElementById('model-select').value;
414
+ const result = document.getElementById('config-result');
415
+ result.innerHTML = '⏳ Saving... <span class="spinner"></span>';
416
+ const r = await api('/api/setup-model', { model, provider });
417
+ result.innerHTML = r.ok ? '✅ Saved!' : '❌ ' + r.msg;
418
+ document.getElementById('config-status').innerHTML = '✅ Configured: ' + provider + ' / ' + model;
419
+ const card = document.getElementById('card-config');
420
+ card.classList.remove('step-active');
421
+ card.classList.add('step-done');
422
+ setTimeout(() => runSteps(), 500);
423
+ }
424
+
425
+ function skipConfig() {
426
+ document.getElementById('config-status').innerHTML = '⏭️ Skipped — configure later with \`hermes model\`';
427
+ document.getElementById('config-area').style.display = 'none';
428
+ const card = document.getElementById('card-config');
429
+ card.classList.remove('step-active');
430
+ card.classList.add('step-done');
431
+ setTimeout(() => runSteps(), 300);
432
+ }
433
+
434
+ function renderTools(tools) {
435
+ const grid = document.getElementById('tools-grid');
436
+ grid.innerHTML = '';
437
+ tools.forEach(t => {
438
+ const div = document.createElement('div');
439
+ div.className = 'tag ' + (t.enabled ? 'enabled' : 'disabled');
440
+ div.style.cursor = 'pointer';
441
+ div.style.justifyContent = 'center';
442
+ div.style.padding = '8px';
443
+ div.dataset.name = t.name;
444
+ div.dataset.enabled = t.enabled;
445
+ div.innerHTML = (t.enabled ? '✅' : '➕') + ' ' + t.name;
446
+ div.onclick = () => {
447
+ div.dataset.enabled = div.dataset.enabled === 'true' ? 'false' : 'true';
448
+ div.className = 'tag ' + (div.dataset.enabled === 'true' ? 'enabled' : 'disabled');
449
+ div.innerHTML = (div.dataset.enabled === 'true' ? '✅' : '➕') + ' ' + t.name;
450
+ };
451
+ grid.appendChild(div);
452
+ });
453
+ }
454
+
455
+ async function saveTools() {
456
+ const enabled = [...document.querySelectorAll('#tools-grid .tag')]
457
+ .filter(el => el.dataset.enabled === 'true')
458
+ .map(el => el.dataset.name);
459
+ const result = document.getElementById('tools-result');
460
+ result.innerHTML = '⏳ Enabling... <span class="spinner"></span>';
461
+ const r = await api('/api/enable-tools', { tools: enabled });
462
+ result.innerHTML = r.ok ? '✅ ' + r.results.filter(x => x.ok).length + ' tools enabled' : '❌ Failed';
463
+ document.getElementById('tools-status').innerHTML = '✅ ' + r.results.filter(x => x.ok).length + ' tools enabled';
464
+ setTimeout(() => runSteps(), 300);
465
+ }
466
+
467
+ async function enableAllTools() {
468
+ const names = [...document.querySelectorAll('#tools-grid .tag')].map(el => el.dataset.name);
469
+ const result = document.getElementById('tools-result');
470
+ result.innerHTML = '⏳ Enabling all... <span class="spinner"></span>';
471
+ const r = await api('/api/enable-tools', { tools: names });
472
+ result.innerHTML = r.ok ? '✅ All enabled' : '❌ Failed';
473
+ document.getElementById('tools-status').innerHTML = '✅ All tools enabled';
474
+ document.getElementById('tools-grid').querySelectorAll('.tag').forEach(el => {
475
+ el.dataset.enabled = 'true';
476
+ el.className = 'tag enabled';
477
+ el.innerHTML = '✅ ' + el.dataset.name;
478
+ });
479
+ setTimeout(() => runSteps(), 300);
480
+ }
481
+
482
+ function skipTools() {
483
+ document.getElementById('tools-status').innerHTML = '⏭️ Skipped — enable later with \`hermes tools\`';
484
+ document.getElementById('tools-area').style.display = 'none';
485
+ const card = document.getElementById('card-tools');
486
+ card.classList.remove('step-active');
487
+ card.classList.add('step-done');
488
+ setTimeout(() => runSteps(), 300);
489
+ }
490
+
491
+ async function runSteps() {
492
+ for (const step of STEPS) {
493
+ const card = document.getElementById('card-' + step.id);
494
+ const st = document.getElementById(step.id + '-status');
495
+
496
+ if (st.textContent.startsWith('✅') || st.textContent.startsWith('⏭️')) continue;
497
+
498
+ card.classList.add('step-active');
499
+ const result = await step.run();
500
+ if (result === true) {
501
+ card.classList.remove('step-active');
502
+ card.classList.add('step-done');
503
+ }
504
+ }
505
+
506
+ // Show footer with next steps
507
+ document.getElementById('footer').innerHTML = 'Hermes Agent is ready. '
508
+ + 'Run <code>hermes</code> in your terminal or open the <a href="http://localhost:9119" class="link" target="_blank">web dashboard</a>. '
509
+ + '<br><span style="font-size:12px">This window can remain open — the setup server will keep running.</span>';
510
+ }
511
+
512
+ document.addEventListener('DOMContentLoaded', renderSteps);
513
+ </script>
514
+ </body>
515
+ </html>`;
516
+ }
517
+
518
+ // ——— server ———
519
+
520
+ export function startServer(port = 5050) {
521
+ const srv = http.createServer(async (req, res) => {
522
+ // CORS preflight
523
+ if (req.method === 'OPTIONS') {
524
+ res.writeHead(204, {
525
+ 'Access-Control-Allow-Origin': '*',
526
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
527
+ 'Access-Control-Allow-Headers': 'Content-Type',
528
+ });
529
+ res.end();
530
+ return;
531
+ }
532
+
533
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
534
+ const path = url.pathname;
535
+
536
+ try {
537
+ // Serve onboarding page
538
+ if (path === '/' || path === '/index.html') {
539
+ html(res, serveOnboarding());
540
+ return;
541
+ }
542
+
543
+ // API endpoints
544
+ switch (path) {
545
+ case '/api/status':
546
+ json(res, 200, await api.status());
547
+ return;
548
+ case '/api/install': {
549
+ const body = await parseBody(req);
550
+ json(res, 200, await api.install());
551
+ return;
552
+ }
553
+ case '/api/setup-model': {
554
+ const body = await parseBody(req);
555
+ json(res, 200, await api.setupModel(body.model, body.provider));
556
+ return;
557
+ }
558
+ case '/api/models':
559
+ json(res, 200, await api.listModels());
560
+ return;
561
+ case '/api/enable-tools': {
562
+ const body = await parseBody(req);
563
+ json(res, 200, await api.enableTools(body.tools));
564
+ return;
565
+ }
566
+ case '/api/start-dashboard':
567
+ json(res, 200, await api.startDashboard());
568
+ return;
569
+ case '/api/gateway-status':
570
+ json(res, 200, await api.gatewayStatus());
571
+ return;
572
+ case '/api/exec': {
573
+ const body = await parseBody(req);
574
+ json(res, 200, await api.runCommand(body.cmd));
575
+ return;
576
+ }
577
+ default:
578
+ json(res, 404, { error: 'not found' });
579
+ }
580
+ } catch (e) {
581
+ json(res, 500, { error: e.message });
582
+ }
583
+ });
584
+
585
+ return new Promise((resolve, reject) => {
586
+ srv.listen(port, '127.0.0.1', () => {
587
+ console.log(` → Setup wizard at http://127.0.0.1:${port}`);
588
+ resolve(srv);
589
+ });
590
+ srv.on('error', reject);
591
+ });
592
+ }
593
+
594
+ // ——— auto-start when run directly ———
595
+ const port = parseInt(process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || 5050);
596
+ startServer(port).catch(e => {
597
+ console.error('Failed to start server:', e);
598
+ process.exit(1);
599
+ });