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.
- package/bin/hermium.mjs +290 -0
- package/dist/server/index.mjs +239 -0
- package/dist/web/assets/IconAlertCircle-B7TgWpbP.js +1 -0
- package/dist/web/assets/IconAlertTriangle-B7IYa46Q.js +1 -0
- package/dist/web/assets/IconCheck-B-_pWgtP.js +1 -0
- package/dist/web/assets/IconCode-AzDguzgN.js +1 -0
- package/dist/web/assets/IconLoader2-ClSf33Yd.js +1 -0
- package/dist/web/assets/IconPlus-BcEAWT5L.js +1 -0
- package/dist/web/assets/IconRefresh-D4MZeezY.js +1 -0
- package/dist/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/dist/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/dist/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/dist/web/assets/index-B-EALIKE.js +1 -0
- package/dist/web/assets/index-BP5217kc.js +1 -0
- package/dist/web/assets/index-BiFHsDt6.js +1 -0
- package/dist/web/assets/index-BuznEPrX.js +1 -0
- package/dist/web/assets/index-CsJLc3b2.js +2 -0
- package/dist/web/assets/index-D04H-m2p.js +29 -0
- package/dist/web/assets/index-DEVyhUPW.js +14 -0
- package/dist/web/assets/index-DKYPj90W.js +90 -0
- package/dist/web/assets/index-D_8SzN4W.js +1 -0
- package/dist/web/assets/index-DmUrSr_K.js +1 -0
- package/dist/web/assets/index-xH6PhxmS.js +1 -0
- package/dist/web/assets/input-of658hyw.js +1 -0
- package/dist/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/web/assets/styles-C9Ypt703.css +1 -0
- package/dist/web/assets/switch-CgWk9YD3.js +1 -0
- package/dist/web/assets/syntax-highlighter-CR5pD3DZ.js +6 -0
- package/dist/web/assets/textarea-xdRt6_lX.js +1 -0
- package/dist/web/assets/useQuery-BtGwEPDz.js +1 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/manifest.json +25 -0
- package/dist/web/nous-logo.png +0 -0
- package/dist/web/robots.txt +3 -0
- package/package.json +35 -0
package/bin/hermium.mjs
ADDED
|
@@ -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
|
+
}
|