hermium 0.1.10 → 0.2.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 (212) hide show
  1. package/README.md +56 -0
  2. package/bin/hermium.mjs +185 -164
  3. package/dist/api.mjs +3513 -0
  4. package/dist/public/assets/css/index-Dfs9RUU9.css +1 -0
  5. package/dist/public/assets/css/styles-B8p6jk5Z.css +1 -0
  6. package/dist/public/assets/js/ChatInputBlock-Bw7AL70H.js +1 -0
  7. package/dist/public/assets/js/MarkdownMessage-8d7Y6VL-.js +1 -0
  8. package/dist/public/assets/js/base-ui-BvQbAt_1.js +1 -0
  9. package/dist/public/assets/js/chat._sessionId-BG6lVraH.js +1 -0
  10. package/dist/public/assets/js/chat.index-D2zdMPTT.js +1 -0
  11. package/dist/public/assets/js/index-C0AK45FU.js +60 -0
  12. package/dist/public/assets/js/index-Cx5En4FK.js +1 -0
  13. package/dist/public/assets/js/memory-CeSRdTkW.js +3 -0
  14. package/dist/public/assets/js/router-8uDKazL-.js +1 -0
  15. package/dist/public/assets/js/settings-Bc3Y5zXO.js +1 -0
  16. package/dist/public/assets/js/skills-DZv7sA_5.js +1 -0
  17. package/dist/public/assets/js/theme-CPkdkpaj.js +1 -0
  18. package/dist/public/assets/js/usage-DXQsT9_b.js +1 -0
  19. package/dist/public/assets/woff2/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  20. package/dist/public/assets/woff2/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  21. package/dist/public/assets/woff2/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  22. package/dist/public/assets/woff2/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  23. package/dist/public/assets/woff2/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  24. package/dist/public/favicon.ico +0 -0
  25. package/dist/public/logo.png +0 -0
  26. package/package.json +1 -1
  27. package/dist/public/assets/IconAlertCircle-BW147gsG.js +0 -1
  28. package/dist/public/assets/IconAlertTriangle-DCoTLVSd.js +0 -1
  29. package/dist/public/assets/IconCheck-DAO7Fpl9.js +0 -1
  30. package/dist/public/assets/IconCode-BqfTl5wU.js +0 -1
  31. package/dist/public/assets/IconLoader2-B_pehSXN.js +0 -1
  32. package/dist/public/assets/IconRefresh-BbMGoMV8.js +0 -1
  33. package/dist/public/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  34. package/dist/public/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  35. package/dist/public/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  36. package/dist/public/assets/index-BL9a2Xg9.js +0 -1
  37. package/dist/public/assets/index-BSwXjgjr.js +0 -1
  38. package/dist/public/assets/index-BWl8tn18.js +0 -1
  39. package/dist/public/assets/index-CQh8SXb2.js +0 -94
  40. package/dist/public/assets/index-Cxd-kSUY.js +0 -1
  41. package/dist/public/assets/index-DA5SH--7.js +0 -2
  42. package/dist/public/assets/index-DCHbvtBS.js +0 -1
  43. package/dist/public/assets/index-DCYXJZEe.js +0 -1
  44. package/dist/public/assets/index-DFDfp0ca.js +0 -1
  45. package/dist/public/assets/index-GuAAqSCJ.js +0 -14
  46. package/dist/public/assets/index-WIDirTHx.js +0 -29
  47. package/dist/public/assets/index-X3XZcAzy.js +0 -1
  48. package/dist/public/assets/input-7TQEEJq6.js +0 -1
  49. package/dist/public/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  50. package/dist/public/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  51. package/dist/public/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  52. package/dist/public/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  53. package/dist/public/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  54. package/dist/public/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  55. package/dist/public/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  56. package/dist/public/assets/models-BlUb1eaf.js +0 -1
  57. package/dist/public/assets/styles-PHtUFHUr.css +0 -1
  58. package/dist/public/assets/switch-B_iYUUM3.js +0 -1
  59. package/dist/public/assets/syntax-highlighter-DcBYfnEK.js +0 -6
  60. package/dist/public/assets/textarea-Dm4aaRqS.js +0 -1
  61. package/dist/public/favicon.png +0 -0
  62. package/dist/public/nous-logo.png +0 -0
  63. package/dist/server/index.mjs +0 -244
  64. package/dist/web-server/__23tanstack-start-plugin-adapters-Cwee5PKy.mjs +0 -6
  65. package/dist/web-server/_chunks/ssr-renderer.mjs +0 -22
  66. package/dist/web-server/_libs/babel__runtime.mjs +0 -237
  67. package/dist/web-server/_libs/bail.mjs +0 -8
  68. package/dist/web-server/_libs/base-ui__react.mjs +0 -9554
  69. package/dist/web-server/_libs/base-ui__utils.mjs +0 -1101
  70. package/dist/web-server/_libs/ccount.mjs +0 -16
  71. package/dist/web-server/_libs/character-entities-legacy.mjs +0 -111
  72. package/dist/web-server/_libs/character-entities.mjs +0 -2130
  73. package/dist/web-server/_libs/character-reference-invalid.mjs +0 -33
  74. package/dist/web-server/_libs/class-variance-authority.mjs +0 -44
  75. package/dist/web-server/_libs/clsx.mjs +0 -16
  76. package/dist/web-server/_libs/comma-separated-tokens.mjs +0 -31
  77. package/dist/web-server/_libs/cookie-es.mjs +0 -44
  78. package/dist/web-server/_libs/croner.mjs +0 -1
  79. package/dist/web-server/_libs/crossws.mjs +0 -1
  80. package/dist/web-server/_libs/decode-named-character-reference+[...].mjs +0 -8
  81. package/dist/web-server/_libs/devlop.mjs +0 -8
  82. package/dist/web-server/_libs/escape-string-regexp.mjs +0 -9
  83. package/dist/web-server/_libs/estree-util-is-identifier-name.mjs +0 -11
  84. package/dist/web-server/_libs/extend.mjs +0 -97
  85. package/dist/web-server/_libs/fault.mjs +0 -1
  86. package/dist/web-server/_libs/floating-ui__core.mjs +0 -663
  87. package/dist/web-server/_libs/floating-ui__dom.mjs +0 -624
  88. package/dist/web-server/_libs/floating-ui__react-dom.mjs +0 -279
  89. package/dist/web-server/_libs/floating-ui__utils.mjs +0 -322
  90. package/dist/web-server/_libs/format.mjs +0 -1
  91. package/dist/web-server/_libs/h3.mjs +0 -408
  92. package/dist/web-server/_libs/hast-util-parse-selector.mjs +0 -39
  93. package/dist/web-server/_libs/hast-util-to-jsx-runtime.mjs +0 -388
  94. package/dist/web-server/_libs/hast-util-whitespace.mjs +0 -10
  95. package/dist/web-server/_libs/hastscript.mjs +0 -200
  96. package/dist/web-server/_libs/highlight.js.mjs +0 -1
  97. package/dist/web-server/_libs/hookable.mjs +0 -1
  98. package/dist/web-server/_libs/html-url-attributes.mjs +0 -26
  99. package/dist/web-server/_libs/inline-style-parser.mjs +0 -142
  100. package/dist/web-server/_libs/is-alphabetical.mjs +0 -7
  101. package/dist/web-server/_libs/is-alphanumerical.mjs +0 -8
  102. package/dist/web-server/_libs/is-decimal.mjs +0 -7
  103. package/dist/web-server/_libs/is-hexadecimal.mjs +0 -7
  104. package/dist/web-server/_libs/is-plain-obj.mjs +0 -10
  105. package/dist/web-server/_libs/isbot.mjs +0 -21
  106. package/dist/web-server/_libs/longest-streak.mjs +0 -25
  107. package/dist/web-server/_libs/lowlight.mjs +0 -1
  108. package/dist/web-server/_libs/markdown-table.mjs +0 -142
  109. package/dist/web-server/_libs/mdast-util-find-and-replace.mjs +0 -109
  110. package/dist/web-server/_libs/mdast-util-from-markdown.mjs +0 -717
  111. package/dist/web-server/_libs/mdast-util-gfm-autolink-literal+[...].mjs +0 -156
  112. package/dist/web-server/_libs/mdast-util-gfm-footnote.mjs +0 -117
  113. package/dist/web-server/_libs/mdast-util-gfm-strikethrough.mjs +0 -54
  114. package/dist/web-server/_libs/mdast-util-gfm-table.mjs +0 -157
  115. package/dist/web-server/_libs/mdast-util-gfm-task-list-item.mjs +0 -77
  116. package/dist/web-server/_libs/mdast-util-gfm.mjs +0 -29
  117. package/dist/web-server/_libs/mdast-util-phrasing.mjs +0 -30
  118. package/dist/web-server/_libs/mdast-util-to-hast.mjs +0 -710
  119. package/dist/web-server/_libs/mdast-util-to-markdown.mjs +0 -798
  120. package/dist/web-server/_libs/mdast-util-to-string.mjs +0 -38
  121. package/dist/web-server/_libs/micromark-core-commonmark.mjs +0 -2259
  122. package/dist/web-server/_libs/micromark-extension-gfm-autolink-literal+[...].mjs +0 -344
  123. package/dist/web-server/_libs/micromark-extension-gfm-footnote+[...].mjs +0 -279
  124. package/dist/web-server/_libs/micromark-extension-gfm-strikethrough+[...].mjs +0 -98
  125. package/dist/web-server/_libs/micromark-extension-gfm-table.mjs +0 -491
  126. package/dist/web-server/_libs/micromark-extension-gfm-tagfilter+[...].mjs +0 -1
  127. package/dist/web-server/_libs/micromark-extension-gfm-task-list-item+[...].mjs +0 -77
  128. package/dist/web-server/_libs/micromark-extension-gfm.mjs +0 -18
  129. package/dist/web-server/_libs/micromark-factory-destination.mjs +0 -94
  130. package/dist/web-server/_libs/micromark-factory-label.mjs +0 -63
  131. package/dist/web-server/_libs/micromark-factory-space.mjs +0 -24
  132. package/dist/web-server/_libs/micromark-factory-title.mjs +0 -65
  133. package/dist/web-server/_libs/micromark-factory-whitespace.mjs +0 -22
  134. package/dist/web-server/_libs/micromark-util-character.mjs +0 -44
  135. package/dist/web-server/_libs/micromark-util-chunked.mjs +0 -36
  136. package/dist/web-server/_libs/micromark-util-classify-character+[...].mjs +0 -12
  137. package/dist/web-server/_libs/micromark-util-combine-extensions+[...].mjs +0 -41
  138. package/dist/web-server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +0 -19
  139. package/dist/web-server/_libs/micromark-util-decode-string.mjs +0 -21
  140. package/dist/web-server/_libs/micromark-util-encode.mjs +0 -1
  141. package/dist/web-server/_libs/micromark-util-html-tag-name.mjs +0 -69
  142. package/dist/web-server/_libs/micromark-util-normalize-identifier+[...].mjs +0 -6
  143. package/dist/web-server/_libs/micromark-util-resolve-all.mjs +0 -15
  144. package/dist/web-server/_libs/micromark-util-sanitize-uri.mjs +0 -41
  145. package/dist/web-server/_libs/micromark-util-subtokenize.mjs +0 -346
  146. package/dist/web-server/_libs/micromark.mjs +0 -906
  147. package/dist/web-server/_libs/ocache.mjs +0 -1
  148. package/dist/web-server/_libs/ohash.mjs +0 -1
  149. package/dist/web-server/_libs/parse-entities.mjs +0 -245
  150. package/dist/web-server/_libs/property-information.mjs +0 -1210
  151. package/dist/web-server/_libs/react-dom.mjs +0 -10779
  152. package/dist/web-server/_libs/react-markdown.mjs +0 -147
  153. package/dist/web-server/_libs/react-syntax-highlighter.mjs +0 -941
  154. package/dist/web-server/_libs/react.mjs +0 -513
  155. package/dist/web-server/_libs/refractor.mjs +0 -2425
  156. package/dist/web-server/_libs/remark-gfm.mjs +0 -20
  157. package/dist/web-server/_libs/remark-parse.mjs +0 -19
  158. package/dist/web-server/_libs/remark-rehype.mjs +0 -21
  159. package/dist/web-server/_libs/reselect.mjs +0 -1
  160. package/dist/web-server/_libs/rou3.mjs +0 -8
  161. package/dist/web-server/_libs/seroval-plugins.mjs +0 -58
  162. package/dist/web-server/_libs/seroval.mjs +0 -1775
  163. package/dist/web-server/_libs/space-separated-tokens.mjs +0 -11
  164. package/dist/web-server/_libs/srvx.mjs +0 -781
  165. package/dist/web-server/_libs/style-to-js.mjs +0 -72
  166. package/dist/web-server/_libs/style-to-object.mjs +0 -38
  167. package/dist/web-server/_libs/tabler__icons-react.mjs +0 -233
  168. package/dist/web-server/_libs/tanstack__history.mjs +0 -204
  169. package/dist/web-server/_libs/tanstack__query-core.mjs +0 -2552
  170. package/dist/web-server/_libs/tanstack__react-query.mjs +0 -190
  171. package/dist/web-server/_libs/tanstack__react-router.mjs +0 -1120
  172. package/dist/web-server/_libs/tanstack__react-store.mjs +0 -2
  173. package/dist/web-server/_libs/tanstack__router-core.mjs +0 -4288
  174. package/dist/web-server/_libs/tanstack__store.mjs +0 -1
  175. package/dist/web-server/_libs/trim-lines.mjs +0 -41
  176. package/dist/web-server/_libs/trough.mjs +0 -85
  177. package/dist/web-server/_libs/ufo.mjs +0 -54
  178. package/dist/web-server/_libs/unctx.mjs +0 -1
  179. package/dist/web-server/_libs/ungap__structured-clone.mjs +0 -224
  180. package/dist/web-server/_libs/unified.mjs +0 -661
  181. package/dist/web-server/_libs/unist-util-is.mjs +0 -100
  182. package/dist/web-server/_libs/unist-util-position.mjs +0 -27
  183. package/dist/web-server/_libs/unist-util-stringify-position.mjs +0 -27
  184. package/dist/web-server/_libs/unist-util-visit-parents.mjs +0 -83
  185. package/dist/web-server/_libs/unist-util-visit.mjs +0 -24
  186. package/dist/web-server/_libs/unstorage.mjs +0 -1
  187. package/dist/web-server/_libs/use-sync-external-store.mjs +0 -139
  188. package/dist/web-server/_libs/vfile-message.mjs +0 -138
  189. package/dist/web-server/_libs/vfile.mjs +0 -467
  190. package/dist/web-server/_libs/zod.mjs +0 -3915
  191. package/dist/web-server/_libs/zustand.mjs +0 -343
  192. package/dist/web-server/_libs/zwitch.mjs +0 -1
  193. package/dist/web-server/_ssr/index--Bo2_ipW.mjs +0 -277
  194. package/dist/web-server/_ssr/index-BEvygh5x.mjs +0 -612
  195. package/dist/web-server/_ssr/index-BFaKxYfN.mjs +0 -40
  196. package/dist/web-server/_ssr/index-BdrMzRTd.mjs +0 -513
  197. package/dist/web-server/_ssr/index-CoDfv1vI.mjs +0 -66
  198. package/dist/web-server/_ssr/index-Cwp8Gyl1.mjs +0 -1812
  199. package/dist/web-server/_ssr/index-DQsfKYjV.mjs +0 -449
  200. package/dist/web-server/_ssr/index-Dbs9k8Ea.mjs +0 -251
  201. package/dist/web-server/_ssr/index-Dk93oyZD.mjs +0 -213
  202. package/dist/web-server/_ssr/index-KNJ7huw_.mjs +0 -369
  203. package/dist/web-server/_ssr/index.mjs +0 -1558
  204. package/dist/web-server/_ssr/input-BV1DMASc.mjs +0 -20
  205. package/dist/web-server/_ssr/models-MzrvbL2i.mjs +0 -43
  206. package/dist/web-server/_ssr/router-CS6Zq3md.mjs +0 -2134
  207. package/dist/web-server/_ssr/start-HYkvq4Ni.mjs +0 -4
  208. package/dist/web-server/_ssr/switch-Bd-Sg0HG.mjs +0 -33
  209. package/dist/web-server/_ssr/syntax-highlighter-5vezNTce.mjs +0 -62
  210. package/dist/web-server/_ssr/textarea-CB4kQp9w.mjs +0 -18
  211. package/dist/web-server/_tanstack-start-manifest_v-m5lY48LR.mjs +0 -4
  212. package/dist/web-server/index.mjs +0 -611
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Hermium CLI
2
+
3
+ Self-hosted AI chat dashboard for Hermes Agent.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g hermium
9
+ ```
10
+
11
+ Or with Bun:
12
+
13
+ ```bash
14
+ bun install -g hermium
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ - [Bun](https://bun.sh) >= 1.2
20
+
21
+ ## Commands
22
+
23
+ ```bash
24
+ hermium start # Start the server (daemon)
25
+ hermium stop # Stop the server
26
+ hermium restart # Restart the server
27
+ hermium status # Show running status
28
+ hermium build # Build from source (requires repo)
29
+ hermium dev # Run in dev mode (requires repo)
30
+ hermium help # Show help
31
+ hermium version # Show version
32
+ ```
33
+
34
+ ## Options
35
+
36
+ ```bash
37
+ hermium start --port 47474 # Custom port (default: 47474)
38
+ ```
39
+
40
+ ## Development
41
+
42
+ To run from source:
43
+
44
+ ```bash
45
+ git clone https://github.com/abboskhonov/hermium.git
46
+ cd hermium
47
+ bun install
48
+ bun run dev
49
+ ```
50
+
51
+ To build the CLI package for publishing:
52
+
53
+ ```bash
54
+ cd packages/cli
55
+ bun run build
56
+ ```
package/bin/hermium.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { spawn, execSync } from 'child_process'
3
3
  import { resolve, dirname, join } from 'path'
4
4
  import { fileURLToPath } from 'url'
5
- import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, existsSync, statSync } from 'fs'
5
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, existsSync, statSync, openSync } from 'fs'
6
6
  import { homedir } from 'os'
7
7
  import pc from 'picocolors'
8
8
 
@@ -10,17 +10,10 @@ const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
10
10
  const pkg = JSON.parse(readFileSync(resolve(pkgDir, 'package.json'), 'utf-8'))
11
11
  const CURRENT_VERSION = pkg.version
12
12
 
13
- const __dirname = dirname(fileURLToPath(import.meta.url))
14
- const apiEntry = resolve(pkgDir, 'dist', 'server', 'index.mjs')
15
- const webEntry = resolve(pkgDir, 'dist', 'web-server', 'index.mjs')
16
- const webServerDir = resolve(pkgDir, 'dist', 'web-server')
17
- const WEB_UI_HOME = resolve(homedir(), '.hermium')
18
- const API_PID_FILE = join(WEB_UI_HOME, 'api.pid')
19
- const WEB_PID_FILE = join(WEB_UI_HOME, 'web.pid')
20
- const API_LOG_FILE = join(WEB_UI_HOME, 'api.log')
21
- const WEB_LOG_FILE = join(WEB_UI_HOME, 'web.log')
22
- const DEFAULT_API_PORT = 47474
23
- const DEFAULT_WEB_PORT = 42424
13
+ const HERMIUM_HOME = resolve(homedir(), '.hermium')
14
+ const PID_FILE = join(HERMIUM_HOME, 'hermium.pid')
15
+ const LOG_FILE = join(HERMIUM_HOME, 'hermium.log')
16
+ const DEFAULT_PORT = 47474
24
17
 
25
18
  // ─── Runtime detection ─────────────────────────────────────────────────────
26
19
 
@@ -44,7 +37,7 @@ function getRuntimeCmd() {
44
37
  // ─── Version check ─────────────────────────────────────────────────────────
45
38
 
46
39
  async function checkLatestVersion() {
47
- const cacheFile = join(WEB_UI_HOME, '.version-check')
40
+ const cacheFile = join(HERMIUM_HOME, '.version-check')
48
41
  const now = Date.now()
49
42
  try {
50
43
  const cache = JSON.parse(readFileSync(cacheFile, 'utf-8'))
@@ -74,9 +67,9 @@ async function checkLatestVersion() {
74
67
 
75
68
  // ─── Helpers ───────────────────────────────────────────────────────────────
76
69
 
77
- function readPid(file) {
70
+ function readPid() {
78
71
  try {
79
- const pid = parseInt(readFileSync(file, 'utf-8').trim())
72
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim())
80
73
  return Number.isFinite(pid) ? pid : null
81
74
  } catch {
82
75
  return null
@@ -92,216 +85,200 @@ function isRunning(pid) {
92
85
  }
93
86
  }
94
87
 
95
- function writePid(file, pid) {
96
- mkdirSync(WEB_UI_HOME, { recursive: true })
97
- writeFileSync(file, String(pid))
88
+ function getPid() {
89
+ const pid = readPid()
90
+ if (pid && isRunning(pid)) return pid
91
+ removePid()
92
+ return null
98
93
  }
99
94
 
100
- function removePid(file) {
101
- try { unlinkSync(file) } catch {}
95
+ function writePid(pid) {
96
+ mkdirSync(HERMIUM_HOME, { recursive: true })
97
+ writeFileSync(PID_FILE, String(pid))
102
98
  }
103
99
 
104
- function getPid(file) {
105
- const pid = readPid(file)
106
- if (pid && isRunning(pid)) return pid
107
- removePid(file)
108
- return null
100
+ function removePid() {
101
+ try { unlinkSync(PID_FILE) } catch {}
109
102
  }
110
103
 
111
- function getApiPid() { return getPid(API_PID_FILE) }
112
- function getWebPid() { return getPid(WEB_PID_FILE) }
113
-
114
- function getPort(argName, defaultPort) {
115
- const idx = process.argv.indexOf(argName)
104
+ function getPort() {
105
+ const idx = process.argv.indexOf('--port')
116
106
  if (idx !== -1 && process.argv[idx + 1]) {
117
107
  const p = parseInt(process.argv[idx + 1])
118
108
  if (!isNaN(p)) return p
119
109
  }
120
- return defaultPort
110
+ return DEFAULT_PORT
121
111
  }
122
112
 
123
- function getRunningPort(pid) {
124
- if (!pid) return null
113
+ function rotateLog() {
125
114
  try {
126
- if (process.platform === 'win32') {
127
- const out = execSync(`netstat -aon -p tcp | findstr LISTENING | findstr " ${pid}$"`, { encoding: 'utf-8' }).trim()
128
- const line = out.split('\n').find(Boolean)
129
- const address = line?.trim().split(/\s+/)[1]
130
- const port = address?.split(':').pop()
131
- return port ? parseInt(port, 10) : null
132
- }
133
- const out = execSync(`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`, { encoding: 'utf-8' }).trim()
134
- const lines = out.split('\n').slice(1)
135
- for (const line of lines) {
136
- const match = line.match(/:(\d+)\s+\(LISTEN\)$/)
137
- if (match) return parseInt(match[1], 10)
115
+ const st = statSync(LOG_FILE)
116
+ if (st.size > 3 * 1024 * 1024) {
117
+ const content = readFileSync(LOG_FILE, 'utf-8')
118
+ const kept = content.split('\n').slice(-2000)
119
+ writeFileSync(LOG_FILE, kept.join('\n'))
120
+ console.log(pc.cyan(` ↻ Rotated ${LOG_FILE.replace(homedir(), '~')}`))
138
121
  }
139
122
  } catch {}
140
- return null
141
123
  }
142
124
 
143
- function rotateLog(file) {
125
+ function stopPid(pid) {
126
+ if (!pid) return false
144
127
  try {
145
- const st = statSync(file)
146
- if (st.size > 3 * 1024 * 1024) {
147
- const content = readFileSync(file, 'utf-8')
148
- const kept = content.split('\n').slice(-2000)
149
- writeFileSync(file, kept.join('\n'))
150
- console.log(pc.cyan(` ↻ Rotated ${file.replace(homedir(), '~')}`))
128
+ process.kill(pid, 'SIGTERM')
129
+ const start = Date.now()
130
+ while (isRunning(pid) && Date.now() - start < 5000) {
131
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
151
132
  }
152
- } catch {}
133
+ if (isRunning(pid)) process.kill(pid, 'SIGKILL')
134
+ return true
135
+ } catch {
136
+ return true
137
+ }
153
138
  }
154
139
 
155
- function spawnServer({ name, entry, logFile, pidFile, port, env, runtime, cwd }) {
140
+ function spawnServer({ name, entry, logFile, pidFile, port, env, runtime }) {
156
141
  if (!existsSync(entry)) {
157
142
  console.log(pc.red(` ✗ ${name} not found: ${entry}`))
158
143
  console.log(` Run "hermium build" first (or check your installation)`)
159
144
  process.exit(1)
160
145
  }
161
146
 
162
- rotateLog(logFile)
147
+ rotateLog()
163
148
 
164
149
  const logFd = openSync(logFile, 'a')
165
150
  const child = spawn(runtime, [entry], {
166
151
  detached: true,
167
152
  stdio: ['ignore', logFd, logFd],
168
153
  env: { ...process.env, ...env, NODE_ENV: 'production' },
169
- cwd: cwd || dirname(entry),
154
+ cwd: dirname(entry),
170
155
  })
171
156
 
172
157
  child.on('error', (err) => {
173
158
  console.error(pc.red(` ✗ Failed to start ${name}: ${err.message}`))
174
- removePid(pidFile)
159
+ removePid()
175
160
  process.exit(1)
176
161
  })
177
162
 
178
163
  child.unref()
179
- writePid(pidFile, child.pid)
164
+ writePid(child.pid)
180
165
  return child.pid
181
166
  }
182
167
 
183
- function stopPid(pid, name) {
184
- if (!pid) return false
185
- try {
186
- process.kill(pid, 'SIGTERM')
187
- const start = Date.now()
188
- while (isRunning(pid) && Date.now() - start < 5000) {
189
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
190
- }
191
- if (isRunning(pid)) process.kill(pid, 'SIGKILL')
192
- return true
193
- } catch {
194
- return true
195
- }
168
+ // ─── Path resolution ───────────────────────────────────────────────────────
169
+
170
+ function findApiEntry() {
171
+ // Production: bundled in cli/dist/api.mjs
172
+ const bundled = resolve(pkgDir, 'dist', 'api.mjs')
173
+ if (existsSync(bundled)) return bundled
174
+
175
+ // Dev: from monorepo packages/api
176
+ const monorepo = resolve(pkgDir, '..', '..', 'packages', 'api', 'dist', 'index.js')
177
+ if (existsSync(monorepo)) return monorepo
178
+
179
+ return null
180
+ }
181
+
182
+ function findWebDist() {
183
+ // Production: bundled in cli/dist/public/
184
+ const bundled = resolve(pkgDir, 'dist', 'public')
185
+ if (existsSync(bundled)) return bundled
186
+
187
+ // Dev: from monorepo packages/web/dist
188
+ const monorepo = resolve(pkgDir, '..', '..', 'packages', 'web', 'dist')
189
+ if (existsSync(monorepo)) return monorepo
190
+
191
+ return null
196
192
  }
197
193
 
198
194
  // ─── Commands ──────────────────────────────────────────────────────────────
199
195
 
200
196
  function cmdStatus() {
201
- const apiPid = getApiPid()
202
- const webPid = getWebPid()
203
- const apiPort = apiPid ? getRunningPort(apiPid) : null
204
- const webPort = webPid ? getRunningPort(webPid) : null
205
-
206
- if (apiPid || webPid) {
207
- console.log(pc.green(` ✓ Hermium is running`))
208
- if (apiPid) console.log(` API : PID ${apiPid}, port ${apiPort || 'unknown'}`)
209
- if (webPid) console.log(` Web : PID ${webPid}, port ${webPort || 'unknown'}`)
210
- console.log(` Logs : ${WEB_UI_HOME}`)
197
+ const pid = getPid()
198
+ if (pid) {
199
+ console.log(pc.green(` Hermium is running (PID: ${pid})`))
200
+ console.log(` URL : http://localhost:${getPort()}`)
201
+ console.log(` Log : ${LOG_FILE.replace(homedir(), '~')}`)
211
202
  } else {
212
203
  console.log(pc.yellow(` ⊘ Hermium is not running`))
213
204
  }
214
205
  }
215
206
 
216
207
  async function cmdStart() {
217
- const existingApi = getApiPid()
218
- const existingWeb = getWebPid()
219
- if (existingApi || existingWeb) {
220
- console.log(pc.yellow(` ✗ Hermium is already running`))
221
- if (existingApi) console.log(` API PID: ${existingApi}`)
222
- if (existingWeb) console.log(` Web PID: ${existingWeb}`)
208
+ const existing = getPid()
209
+ if (existing) {
210
+ console.log(pc.yellow(` Hermium is already running (PID: ${existing})`))
223
211
  console.log(` Use "hermium stop" first`)
224
212
  process.exit(1)
225
213
  }
226
214
 
227
- const apiPort = getPort('--port', DEFAULT_API_PORT)
228
- const webPort = getPort('--web-port', DEFAULT_WEB_PORT)
215
+ const port = getPort()
229
216
  const runtime = getRuntimeCmd()
217
+ const apiEntry = findApiEntry()
218
+ const webDist = findWebDist()
219
+
220
+ if (!apiEntry) {
221
+ console.log(pc.red(` ✗ API server not found. Run "hermium build" first.`))
222
+ process.exit(1)
223
+ }
230
224
 
231
- mkdirSync(WEB_UI_HOME, { recursive: true })
225
+ mkdirSync(HERMIUM_HOME, { recursive: true })
232
226
 
233
- console.log(pc.cyan(` ⏳ Starting Hermium (API:${apiPort}, Web:${webPort})...`))
227
+ console.log(pc.cyan(` ⏳ Starting Hermium on port ${port}...`))
234
228
 
235
- // 1. Start API server
236
- const apiPid = spawnServer({
237
- name: 'API server',
229
+ const pid = spawnServer({
230
+ name: 'Hermium server',
238
231
  entry: apiEntry,
239
- logFile: API_LOG_FILE,
240
- pidFile: API_PID_FILE,
241
- port: apiPort,
232
+ logFile: LOG_FILE,
233
+ pidFile: PID_FILE,
234
+ port,
242
235
  runtime,
243
236
  env: {
244
- HERMIUM_PORT: String(apiPort),
245
- HERMIUM_WEB_PORT: String(webPort),
246
- HERMIUM_VERSION: CURRENT_VERSION,
247
- WEB_STATIC_DIR: resolve(pkgDir, 'dist', 'public'),
237
+ PORT: String(port),
238
+ APP_VERSION: CURRENT_VERSION,
239
+ WEB_STATIC_DIR: webDist || '',
240
+ AUTH_DISABLED: '1',
248
241
  },
249
242
  })
250
243
 
251
- // 2. Start web SSR server
252
- const webPid = spawnServer({
253
- name: 'Web server',
254
- entry: webEntry,
255
- logFile: WEB_LOG_FILE,
256
- pidFile: WEB_PID_FILE,
257
- port: webPort,
258
- runtime,
259
- cwd: webServerDir,
260
- env: { PORT: String(webPort) },
261
- })
244
+ console.log(` PID: ${pid}`)
262
245
 
263
- console.log(` API PID: ${apiPid}`)
264
- console.log(` Web PID: ${webPid}`)
265
-
266
- // Poll web server health
246
+ // Poll health
267
247
  const maxWait = 30000
268
248
  const interval = 500
269
249
  let waited = 0
270
- const webUrl = `http://localhost:${webPort}`
250
+ const url = `http://localhost:${port}`
271
251
 
272
252
  function poll() {
273
253
  waited += interval
274
254
 
275
- if (!isRunning(apiPid)) {
276
- console.log(pc.red(` ✗ API server crashed`))
277
- console.log(` Check log: ${API_LOG_FILE}`)
278
- stopPid(webPid, 'web')
279
- removePid(API_PID_FILE)
280
- removePid(WEB_PID_FILE)
281
- process.exit(1)
282
- }
283
- if (!isRunning(webPid)) {
284
- console.log(pc.red(` ✗ Web server crashed`))
285
- console.log(` Check log: ${WEB_LOG_FILE}`)
286
- stopPid(apiPid, 'api')
287
- removePid(API_PID_FILE)
288
- removePid(WEB_PID_FILE)
255
+ if (!isRunning(pid)) {
256
+ console.log(pc.red(` ✗ Server crashed`))
257
+ console.log(` Check log: ${LOG_FILE}`)
258
+ removePid()
289
259
  process.exit(1)
290
260
  }
291
261
 
292
- fetch(`${webUrl}/api/health`).catch(() => null).then((res) => {
293
- // Web server doesn't have /api/health, this will 404 but we just want to check it's listening
294
- if (waited < maxWait) {
295
- setTimeout(poll, interval)
262
+ fetch(`${url}/api/health`).catch(() => null).then((res) => {
263
+ if (!res || !res.ok) {
264
+ if (waited < maxWait) {
265
+ setTimeout(poll, interval)
266
+ } else {
267
+ console.log(pc.green(` ✓ Hermium started`))
268
+ console.log(` ${url}`)
269
+ console.log(` Logs: ${LOG_FILE.replace(homedir(), '~')}`)
270
+ checkLatestVersion()
271
+ }
296
272
  } else {
297
273
  console.log(pc.green(` ✓ Hermium started`))
298
- console.log(` ${webUrl}`)
299
- console.log(` Logs: ${WEB_UI_HOME}`)
300
- checkLatestVersion() // non-blocking
274
+ console.log(` ${url}`)
275
+ console.log(` Logs: ${LOG_FILE.replace(homedir(), '~')}`)
276
+ checkLatestVersion()
277
+ // Open browser
301
278
  const openCmd =
302
- process.platform === 'win32' ? `start ${webUrl}` :
303
- process.platform === 'darwin' ? `open ${webUrl}` :
304
- `xdg-open ${webUrl}`
279
+ process.platform === 'win32' ? `start ${url}` :
280
+ process.platform === 'darwin' ? `open ${url}` :
281
+ `xdg-open ${url}`
305
282
  try { execSync(openCmd, { stdio: 'ignore' }) } catch {}
306
283
  }
307
284
  })
@@ -311,24 +288,15 @@ async function cmdStart() {
311
288
  }
312
289
 
313
290
  function cmdStop() {
314
- const apiPid = getApiPid()
315
- const webPid = getWebPid()
316
-
317
- if (!apiPid && !webPid) {
291
+ const pid = getPid()
292
+ if (!pid) {
318
293
  console.log(pc.yellow(` ⊘ Hermium is not running`))
319
294
  return
320
295
  }
321
296
 
322
- if (webPid) {
323
- stopPid(webPid, 'web')
324
- removePid(WEB_PID_FILE)
325
- console.log(pc.green(` ✓ Web server stopped (PID: ${webPid})`))
326
- }
327
- if (apiPid) {
328
- stopPid(apiPid, 'api')
329
- removePid(API_PID_FILE)
330
- console.log(pc.green(` ✓ API server stopped (PID: ${apiPid})`))
331
- }
297
+ stopPid(pid)
298
+ removePid()
299
+ console.log(pc.green(` ✓ Hermium stopped (PID: ${pid})`))
332
300
  }
333
301
 
334
302
  function cmdRestart() {
@@ -337,6 +305,52 @@ function cmdRestart() {
337
305
  cmdStart()
338
306
  }
339
307
 
308
+ function cmdBuild() {
309
+ console.log(pc.cyan(` 🔨 Building Hermium...`))
310
+ const runtime = getRuntimeCmd()
311
+
312
+ // Detect if we're in a monorepo
313
+ const rootDir = resolve(pkgDir, '..', '..')
314
+ const isMonorepo = existsSync(resolve(rootDir, 'packages', 'api')) && existsSync(resolve(rootDir, 'packages', 'web'))
315
+
316
+ if (!isMonorepo) {
317
+ console.log(pc.yellow(` ⚠ Not in a monorepo. Build must be done from the Hermium source repository.`))
318
+ process.exit(1)
319
+ }
320
+
321
+ try {
322
+ console.log(` → Building shared...`)
323
+ execSync(`${runtime} run build:shared`, { cwd: rootDir, stdio: 'inherit' })
324
+ console.log(` → Building API...`)
325
+ execSync(`${runtime} run build:api`, { cwd: rootDir, stdio: 'inherit' })
326
+ console.log(` → Building Web...`)
327
+ execSync(`${runtime} run build:web`, { cwd: rootDir, stdio: 'inherit' })
328
+ console.log(pc.green(` ✓ Build complete`))
329
+ } catch (err) {
330
+ console.log(pc.red(` ✗ Build failed`))
331
+ process.exit(1)
332
+ }
333
+ }
334
+
335
+ function cmdDev() {
336
+ console.log(pc.cyan(` 🔥 Starting Hermium in dev mode...`))
337
+ const runtime = getRuntimeCmd()
338
+
339
+ const rootDir = resolve(pkgDir, '..', '..')
340
+ const isMonorepo = existsSync(resolve(rootDir, 'packages', 'api')) && existsSync(resolve(rootDir, 'packages', 'web'))
341
+
342
+ if (!isMonorepo) {
343
+ console.log(pc.yellow(` ⚠ Not in a monorepo. Dev mode must be run from the Hermium source repository.`))
344
+ process.exit(1)
345
+ }
346
+
347
+ try {
348
+ execSync(`${runtime} run dev`, { cwd: rootDir, stdio: 'inherit' })
349
+ } catch {
350
+ process.exit(1)
351
+ }
352
+ }
353
+
340
354
  function cmdHelp() {
341
355
  console.log(`
342
356
  ${pc.bold('Hermium CLI')} — Self-hosted AI chat for Hermes Agent
@@ -345,27 +359,28 @@ ${pc.bold('Usage:')}
345
359
  hermium <command> [options]
346
360
 
347
361
  ${pc.bold('Commands:')}
348
- start Start Hermium servers (daemon)
349
- stop Stop Hermium servers
350
- restart Restart Hermium servers
362
+ start Start Hermium server (daemon)
363
+ stop Stop Hermium server
364
+ restart Restart Hermium server
351
365
  status Show running status
366
+ build Build from source (requires repo)
367
+ dev Run in dev mode (requires repo)
352
368
  help Show this help message
369
+ version Show version
353
370
 
354
371
  ${pc.bold('Options:')}
355
- --port <n> API port (default: 47474)
356
- --web-port <n> Web port (default: 42424)
372
+ --port <n> Server port (default: 47474)
357
373
 
358
374
  ${pc.bold('Examples:')}
359
375
  hermium start
360
- hermium start --port 47474 --web-port 42424
376
+ hermium start --port 47474
361
377
  hermium stop
362
378
  hermium status
363
379
  `)
364
380
  }
365
381
 
366
382
  function cmdVersion() {
367
- const pkg = JSON.parse(readFileSync(resolve(pkgDir, 'package.json'), 'utf-8'))
368
- console.log(pkg.version)
383
+ console.log(CURRENT_VERSION)
369
384
  }
370
385
 
371
386
  // ─── Entrypoint ─────────────────────────────────────────────────────────────
@@ -385,6 +400,12 @@ switch (command) {
385
400
  case 'status':
386
401
  cmdStatus()
387
402
  break
403
+ case 'build':
404
+ cmdBuild()
405
+ break
406
+ case 'dev':
407
+ cmdDev()
408
+ break
388
409
  case 'version':
389
410
  case '--version':
390
411
  case '-v':