hermium 0.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.
Files changed (42) hide show
  1. package/bin/hermium.mjs +290 -0
  2. package/dist/server/index.mjs +239 -0
  3. package/dist/web/assets/IconAlertCircle-B7TgWpbP.js +1 -0
  4. package/dist/web/assets/IconAlertTriangle-B7IYa46Q.js +1 -0
  5. package/dist/web/assets/IconCheck-B-_pWgtP.js +1 -0
  6. package/dist/web/assets/IconCode-AzDguzgN.js +1 -0
  7. package/dist/web/assets/IconLoader2-ClSf33Yd.js +1 -0
  8. package/dist/web/assets/IconPlus-BcEAWT5L.js +1 -0
  9. package/dist/web/assets/IconRefresh-D4MZeezY.js +1 -0
  10. package/dist/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  11. package/dist/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  12. package/dist/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  13. package/dist/web/assets/index-B-EALIKE.js +1 -0
  14. package/dist/web/assets/index-BP5217kc.js +1 -0
  15. package/dist/web/assets/index-BiFHsDt6.js +1 -0
  16. package/dist/web/assets/index-BuznEPrX.js +1 -0
  17. package/dist/web/assets/index-CsJLc3b2.js +2 -0
  18. package/dist/web/assets/index-D04H-m2p.js +29 -0
  19. package/dist/web/assets/index-DEVyhUPW.js +14 -0
  20. package/dist/web/assets/index-DKYPj90W.js +90 -0
  21. package/dist/web/assets/index-D_8SzN4W.js +1 -0
  22. package/dist/web/assets/index-DmUrSr_K.js +1 -0
  23. package/dist/web/assets/index-xH6PhxmS.js +1 -0
  24. package/dist/web/assets/input-of658hyw.js +1 -0
  25. package/dist/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  26. package/dist/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  27. package/dist/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  28. package/dist/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  29. package/dist/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  30. package/dist/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  31. package/dist/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  32. package/dist/web/assets/styles-C9Ypt703.css +1 -0
  33. package/dist/web/assets/switch-CgWk9YD3.js +1 -0
  34. package/dist/web/assets/syntax-highlighter-CR5pD3DZ.js +6 -0
  35. package/dist/web/assets/textarea-xdRt6_lX.js +1 -0
  36. package/dist/web/assets/useQuery-BtGwEPDz.js +1 -0
  37. package/dist/web/favicon.ico +0 -0
  38. package/dist/web/favicon.png +0 -0
  39. package/dist/web/manifest.json +25 -0
  40. package/dist/web/nous-logo.png +0 -0
  41. package/dist/web/robots.txt +3 -0
  42. package/package.json +35 -0
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, execSync } from 'child_process'
3
+ import { resolve, dirname, join } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, existsSync, statSync } from 'fs'
6
+ import { homedir } from 'os'
7
+ import pc from 'picocolors'
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url))
10
+ const pkgDir = resolve(__dirname, '..')
11
+ const serverEntry = resolve(pkgDir, 'dist', 'server', 'index.mjs')
12
+ const WEB_UI_HOME = resolve(homedir(), '.hermium')
13
+ const PID_FILE = join(WEB_UI_HOME, 'server.pid')
14
+ const LOG_FILE = join(WEB_UI_HOME, 'server.log')
15
+ const DEFAULT_PORT = 8788
16
+
17
+ // ─── Helpers ───────────────────────────────────────────────────────────────
18
+
19
+ function readPid() {
20
+ try {
21
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim())
22
+ return Number.isFinite(pid) ? pid : null
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ function isRunning(pid) {
29
+ try {
30
+ process.kill(pid, 0)
31
+ return true
32
+ } catch (err) {
33
+ return err?.code === 'EPERM'
34
+ }
35
+ }
36
+
37
+ function writePid(pid) {
38
+ mkdirSync(WEB_UI_HOME, { recursive: true })
39
+ writeFileSync(PID_FILE, String(pid))
40
+ }
41
+
42
+ function removePid() {
43
+ try { unlinkSync(PID_FILE) } catch {}
44
+ }
45
+
46
+ function getPid() {
47
+ const pid = readPid()
48
+ if (pid && isRunning(pid)) return pid
49
+ removePid()
50
+ return null
51
+ }
52
+
53
+ function getPort() {
54
+ const idx = process.argv.indexOf('--port')
55
+ if (idx !== -1 && process.argv[idx + 1]) {
56
+ const p = parseInt(process.argv[idx + 1])
57
+ if (!isNaN(p)) return p
58
+ }
59
+ if (process.env.HERMIUM_PORT && !isNaN(process.env.HERMIUM_PORT)) {
60
+ return parseInt(process.env.HERMIUM_PORT)
61
+ }
62
+ return DEFAULT_PORT
63
+ }
64
+
65
+ function getRunningPort(pid) {
66
+ if (!pid) return null
67
+
68
+ try {
69
+ if (process.platform === 'win32') {
70
+ const out = execSync(`netstat -aon -p tcp | findstr LISTENING | findstr " ${pid}$"`, { encoding: 'utf-8' }).trim()
71
+ const line = out.split('\n').find(Boolean)
72
+ const address = line?.trim().split(/\s+/)[1]
73
+ const port = address?.split(':').pop()
74
+ return port ? parseInt(port, 10) : null
75
+ }
76
+
77
+ const out = execSync(`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`, { encoding: 'utf-8' }).trim()
78
+ const lines = out.split('\n').slice(1)
79
+ for (const line of lines) {
80
+ const match = line.match(/:(\d+)\s+\(LISTEN\)$/)
81
+ if (match) return parseInt(match[1], 10)
82
+ }
83
+ } catch {}
84
+ return null
85
+ }
86
+
87
+ // ─── Commands ──────────────────────────────────────────────────────────────
88
+
89
+ function cmdStatus() {
90
+ const pid = getPid()
91
+ if (pid) {
92
+ const port = getRunningPort(pid)
93
+ console.log(pc.green(` ✓ Hermium is running`))
94
+ console.log(` PID : ${pid}`)
95
+ console.log(` Port : ${port || 'unknown'}`)
96
+ console.log(` Log : ${LOG_FILE}`)
97
+ } else {
98
+ console.log(pc.yellow(` ⊘ Hermium is not running`))
99
+ }
100
+ }
101
+
102
+ function cmdStart() {
103
+ const existing = getPid()
104
+ if (existing) {
105
+ console.log(pc.yellow(` ✗ Hermium is already running (PID: ${existing})`))
106
+ console.log(` Use "hermium stop" to stop it first`)
107
+ process.exit(1)
108
+ }
109
+
110
+ if (!existsSync(serverEntry)) {
111
+ console.log(pc.red(` ✗ Server not found: ${serverEntry}`))
112
+ console.log(` Run "hermium build" first (or check your installation)`)
113
+ process.exit(1)
114
+ }
115
+
116
+ const port = getPort()
117
+ mkdirSync(WEB_UI_HOME, { recursive: true })
118
+
119
+ // Rotate log if over 3 MB
120
+ try {
121
+ const st = statSync(LOG_FILE)
122
+ if (st.size > 3 * 1024 * 1024) {
123
+ const content = readFileSync(LOG_FILE, 'utf-8')
124
+ const kept = content.split('\n').slice(-2000)
125
+ writeFileSync(LOG_FILE, kept.join('\n'))
126
+ console.log(pc.cyan(` ↻ Log rotated`))
127
+ }
128
+ } catch {}
129
+
130
+ const logFd = openSync(LOG_FILE, 'a')
131
+ const child = spawn(process.execPath, [serverEntry], {
132
+ detached: true,
133
+ stdio: ['ignore', logFd, logFd],
134
+ env: {
135
+ ...process.env,
136
+ NODE_ENV: 'production',
137
+ WEB_STATIC_DIR: resolve(pkgDir, 'dist', 'web'),
138
+ HERMIUM_PORT: String(port),
139
+ },
140
+ })
141
+
142
+ child.on('error', (err) => {
143
+ console.error(pc.red(` ✗ Failed to start: ${err.message}`))
144
+ removePid()
145
+ process.exit(1)
146
+ })
147
+
148
+ child.unref()
149
+ writePid(child.pid)
150
+
151
+ // Poll health endpoint
152
+ const healthUrl = `http://127.0.0.1:${port}/api/health`
153
+ const maxWait = 30000
154
+ const interval = 500
155
+ let waited = 0
156
+
157
+ console.log(pc.cyan(` ⏳ Starting Hermium (PID: ${child.pid}, port: ${port})...`))
158
+
159
+ function poll() {
160
+ waited += interval
161
+ if (!isRunning(child.pid)) {
162
+ console.log(pc.red(` ✗ Failed to start`))
163
+ console.log(` Check log: ${LOG_FILE}`)
164
+ removePid()
165
+ process.exit(1)
166
+ }
167
+
168
+ fetch(healthUrl)
169
+ .then((res) => {
170
+ if (res.ok) {
171
+ const url = `http://localhost:${port}`
172
+ console.log(pc.green(` ✓ Hermium started`))
173
+ console.log(` ${url}`)
174
+ console.log(` Log: ${LOG_FILE}`)
175
+ // Auto-open browser
176
+ const openCmd =
177
+ process.platform === 'win32'
178
+ ? `start ${url}`
179
+ : process.platform === 'darwin'
180
+ ? `open ${url}`
181
+ : `xdg-open ${url}`
182
+ try {
183
+ execSync(openCmd, { stdio: 'ignore' })
184
+ } catch {}
185
+ } else if (waited < maxWait) {
186
+ setTimeout(poll, interval)
187
+ } else {
188
+ console.log(pc.red(` ✗ Timed out waiting for server`))
189
+ console.log(` Check log: ${LOG_FILE}`)
190
+ removePid()
191
+ process.exit(1)
192
+ }
193
+ })
194
+ .catch(() => {
195
+ if (waited < maxWait) {
196
+ setTimeout(poll, interval)
197
+ } else {
198
+ console.log(pc.red(` ✗ Timed out waiting for server`))
199
+ console.log(` Check log: ${LOG_FILE}`)
200
+ removePid()
201
+ process.exit(1)
202
+ }
203
+ })
204
+ }
205
+
206
+ setTimeout(poll, interval)
207
+ }
208
+
209
+ function cmdStop() {
210
+ const pid = getPid()
211
+ if (!pid) {
212
+ console.log(pc.yellow(` ⊘ Hermium is not running`))
213
+ return
214
+ }
215
+
216
+ try {
217
+ process.kill(pid, 'SIGTERM')
218
+ const start = Date.now()
219
+ while (isRunning(pid) && Date.now() - start < 5000) {
220
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
221
+ }
222
+ if (isRunning(pid)) {
223
+ process.kill(pid, 'SIGKILL')
224
+ }
225
+ removePid()
226
+ console.log(pc.green(` ✓ Hermium stopped (PID: ${pid})`))
227
+ } catch {
228
+ removePid()
229
+ console.log(pc.green(` ✓ Hermium stopped (PID: ${pid})`))
230
+ }
231
+ }
232
+
233
+ function cmdRestart() {
234
+ cmdStop()
235
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500)
236
+ cmdStart()
237
+ }
238
+
239
+ function cmdHelp() {
240
+ console.log(`
241
+ ${pc.bold('Hermium CLI')} — Self-hosted AI chat for Hermes Agent
242
+
243
+ ${pc.bold('Usage:')}
244
+ hermium <command> [options]
245
+
246
+ ${pc.bold('Commands:')}
247
+ start Start Hermium server (daemon)
248
+ stop Stop Hermium server
249
+ restart Restart Hermium server
250
+ status Show running status
251
+ help Show this help message
252
+
253
+ ${pc.bold('Options:')}
254
+ --port <n> Port to run on (default: 8788)
255
+
256
+ ${pc.bold('Examples:')}
257
+ hermium start
258
+ hermium start --port 3000
259
+ hermium stop
260
+ hermium status
261
+ `)
262
+ }
263
+
264
+ // ─── Entrypoint ─────────────────────────────────────────────────────────────
265
+
266
+ const command = process.argv[2] || 'help'
267
+
268
+ switch (command) {
269
+ case 'start':
270
+ cmdStart()
271
+ break
272
+ case 'stop':
273
+ cmdStop()
274
+ break
275
+ case 'restart':
276
+ cmdRestart()
277
+ break
278
+ case 'status':
279
+ cmdStatus()
280
+ break
281
+ case 'help':
282
+ case '--help':
283
+ case '-h':
284
+ cmdHelp()
285
+ break
286
+ default:
287
+ console.log(pc.red(` ✗ Unknown command: ${command}`))
288
+ console.log(` Run "hermium help" for usage`)
289
+ process.exit(1)
290
+ }