hermium 0.1.9 → 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 (209) 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-BHkmI3j7.js +0 -1
  28. package/dist/public/assets/IconAlertTriangle-wCJudlVg.js +0 -1
  29. package/dist/public/assets/IconCheck-CFuEh_p7.js +0 -1
  30. package/dist/public/assets/IconLoader2-BIx3OuF9.js +0 -1
  31. package/dist/public/assets/IconRefresh-Dgm93w3T.js +0 -1
  32. package/dist/public/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  33. package/dist/public/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  34. package/dist/public/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  35. package/dist/public/assets/index-Bbz3abmO.js +0 -14
  36. package/dist/public/assets/index-CWUaRwcE.js +0 -1
  37. package/dist/public/assets/index-CinLq3cd.js +0 -1
  38. package/dist/public/assets/index-CrQs9n6q.js +0 -29
  39. package/dist/public/assets/index-CtacpN3I.js +0 -1
  40. package/dist/public/assets/index-DY7aE-9s.js +0 -2
  41. package/dist/public/assets/index-DkYGodJj.js +0 -94
  42. package/dist/public/assets/index-DvDLadUx.js +0 -1
  43. package/dist/public/assets/index-U6RcWedt.js +0 -1
  44. package/dist/public/assets/index-_6iFZ0fh.js +0 -1
  45. package/dist/public/assets/index-enFS26SU.js +0 -1
  46. package/dist/public/assets/input-eNcwlDHp.js +0 -1
  47. package/dist/public/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  48. package/dist/public/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  49. package/dist/public/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  50. package/dist/public/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  51. package/dist/public/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  52. package/dist/public/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  53. package/dist/public/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  54. package/dist/public/assets/queries-iHRgZzw2.js +0 -1
  55. package/dist/public/assets/styles-KcflDlA_.css +0 -1
  56. package/dist/public/assets/switch-B1DcZLwL.js +0 -1
  57. package/dist/public/assets/syntax-highlighter-DWPF-A_h.js +0 -6
  58. package/dist/public/assets/textarea-Di_syYTS.js +0 -1
  59. package/dist/public/favicon.png +0 -0
  60. package/dist/public/nous-logo.png +0 -0
  61. package/dist/server/index.mjs +0 -244
  62. package/dist/web-server/__23tanstack-start-plugin-adapters-Cwee5PKy.mjs +0 -6
  63. package/dist/web-server/_chunks/ssr-renderer.mjs +0 -22
  64. package/dist/web-server/_libs/babel__runtime.mjs +0 -237
  65. package/dist/web-server/_libs/bail.mjs +0 -8
  66. package/dist/web-server/_libs/base-ui__react.mjs +0 -9554
  67. package/dist/web-server/_libs/base-ui__utils.mjs +0 -1101
  68. package/dist/web-server/_libs/ccount.mjs +0 -16
  69. package/dist/web-server/_libs/character-entities-legacy.mjs +0 -111
  70. package/dist/web-server/_libs/character-entities.mjs +0 -2130
  71. package/dist/web-server/_libs/character-reference-invalid.mjs +0 -33
  72. package/dist/web-server/_libs/class-variance-authority.mjs +0 -44
  73. package/dist/web-server/_libs/clsx.mjs +0 -16
  74. package/dist/web-server/_libs/comma-separated-tokens.mjs +0 -31
  75. package/dist/web-server/_libs/cookie-es.mjs +0 -44
  76. package/dist/web-server/_libs/croner.mjs +0 -1
  77. package/dist/web-server/_libs/crossws.mjs +0 -1
  78. package/dist/web-server/_libs/decode-named-character-reference+[...].mjs +0 -8
  79. package/dist/web-server/_libs/devlop.mjs +0 -8
  80. package/dist/web-server/_libs/escape-string-regexp.mjs +0 -9
  81. package/dist/web-server/_libs/estree-util-is-identifier-name.mjs +0 -11
  82. package/dist/web-server/_libs/extend.mjs +0 -97
  83. package/dist/web-server/_libs/fault.mjs +0 -1
  84. package/dist/web-server/_libs/floating-ui__core.mjs +0 -663
  85. package/dist/web-server/_libs/floating-ui__dom.mjs +0 -624
  86. package/dist/web-server/_libs/floating-ui__react-dom.mjs +0 -279
  87. package/dist/web-server/_libs/floating-ui__utils.mjs +0 -322
  88. package/dist/web-server/_libs/format.mjs +0 -1
  89. package/dist/web-server/_libs/h3.mjs +0 -408
  90. package/dist/web-server/_libs/hast-util-parse-selector.mjs +0 -39
  91. package/dist/web-server/_libs/hast-util-to-jsx-runtime.mjs +0 -388
  92. package/dist/web-server/_libs/hast-util-whitespace.mjs +0 -10
  93. package/dist/web-server/_libs/hastscript.mjs +0 -200
  94. package/dist/web-server/_libs/highlight.js.mjs +0 -1
  95. package/dist/web-server/_libs/hookable.mjs +0 -1
  96. package/dist/web-server/_libs/html-url-attributes.mjs +0 -26
  97. package/dist/web-server/_libs/inline-style-parser.mjs +0 -142
  98. package/dist/web-server/_libs/is-alphabetical.mjs +0 -7
  99. package/dist/web-server/_libs/is-alphanumerical.mjs +0 -8
  100. package/dist/web-server/_libs/is-decimal.mjs +0 -7
  101. package/dist/web-server/_libs/is-hexadecimal.mjs +0 -7
  102. package/dist/web-server/_libs/is-plain-obj.mjs +0 -10
  103. package/dist/web-server/_libs/isbot.mjs +0 -21
  104. package/dist/web-server/_libs/longest-streak.mjs +0 -25
  105. package/dist/web-server/_libs/lowlight.mjs +0 -1
  106. package/dist/web-server/_libs/markdown-table.mjs +0 -142
  107. package/dist/web-server/_libs/mdast-util-find-and-replace.mjs +0 -109
  108. package/dist/web-server/_libs/mdast-util-from-markdown.mjs +0 -717
  109. package/dist/web-server/_libs/mdast-util-gfm-autolink-literal+[...].mjs +0 -156
  110. package/dist/web-server/_libs/mdast-util-gfm-footnote.mjs +0 -117
  111. package/dist/web-server/_libs/mdast-util-gfm-strikethrough.mjs +0 -54
  112. package/dist/web-server/_libs/mdast-util-gfm-table.mjs +0 -157
  113. package/dist/web-server/_libs/mdast-util-gfm-task-list-item.mjs +0 -77
  114. package/dist/web-server/_libs/mdast-util-gfm.mjs +0 -29
  115. package/dist/web-server/_libs/mdast-util-phrasing.mjs +0 -30
  116. package/dist/web-server/_libs/mdast-util-to-hast.mjs +0 -710
  117. package/dist/web-server/_libs/mdast-util-to-markdown.mjs +0 -798
  118. package/dist/web-server/_libs/mdast-util-to-string.mjs +0 -38
  119. package/dist/web-server/_libs/micromark-core-commonmark.mjs +0 -2259
  120. package/dist/web-server/_libs/micromark-extension-gfm-autolink-literal+[...].mjs +0 -344
  121. package/dist/web-server/_libs/micromark-extension-gfm-footnote+[...].mjs +0 -279
  122. package/dist/web-server/_libs/micromark-extension-gfm-strikethrough+[...].mjs +0 -98
  123. package/dist/web-server/_libs/micromark-extension-gfm-table.mjs +0 -491
  124. package/dist/web-server/_libs/micromark-extension-gfm-tagfilter+[...].mjs +0 -1
  125. package/dist/web-server/_libs/micromark-extension-gfm-task-list-item+[...].mjs +0 -77
  126. package/dist/web-server/_libs/micromark-extension-gfm.mjs +0 -18
  127. package/dist/web-server/_libs/micromark-factory-destination.mjs +0 -94
  128. package/dist/web-server/_libs/micromark-factory-label.mjs +0 -63
  129. package/dist/web-server/_libs/micromark-factory-space.mjs +0 -24
  130. package/dist/web-server/_libs/micromark-factory-title.mjs +0 -65
  131. package/dist/web-server/_libs/micromark-factory-whitespace.mjs +0 -22
  132. package/dist/web-server/_libs/micromark-util-character.mjs +0 -44
  133. package/dist/web-server/_libs/micromark-util-chunked.mjs +0 -36
  134. package/dist/web-server/_libs/micromark-util-classify-character+[...].mjs +0 -12
  135. package/dist/web-server/_libs/micromark-util-combine-extensions+[...].mjs +0 -41
  136. package/dist/web-server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +0 -19
  137. package/dist/web-server/_libs/micromark-util-decode-string.mjs +0 -21
  138. package/dist/web-server/_libs/micromark-util-encode.mjs +0 -1
  139. package/dist/web-server/_libs/micromark-util-html-tag-name.mjs +0 -69
  140. package/dist/web-server/_libs/micromark-util-normalize-identifier+[...].mjs +0 -6
  141. package/dist/web-server/_libs/micromark-util-resolve-all.mjs +0 -15
  142. package/dist/web-server/_libs/micromark-util-sanitize-uri.mjs +0 -41
  143. package/dist/web-server/_libs/micromark-util-subtokenize.mjs +0 -346
  144. package/dist/web-server/_libs/micromark.mjs +0 -906
  145. package/dist/web-server/_libs/ocache.mjs +0 -1
  146. package/dist/web-server/_libs/ohash.mjs +0 -1
  147. package/dist/web-server/_libs/parse-entities.mjs +0 -245
  148. package/dist/web-server/_libs/property-information.mjs +0 -1210
  149. package/dist/web-server/_libs/react-dom.mjs +0 -10779
  150. package/dist/web-server/_libs/react-markdown.mjs +0 -147
  151. package/dist/web-server/_libs/react-syntax-highlighter.mjs +0 -941
  152. package/dist/web-server/_libs/react.mjs +0 -513
  153. package/dist/web-server/_libs/refractor.mjs +0 -2425
  154. package/dist/web-server/_libs/remark-gfm.mjs +0 -20
  155. package/dist/web-server/_libs/remark-parse.mjs +0 -19
  156. package/dist/web-server/_libs/remark-rehype.mjs +0 -21
  157. package/dist/web-server/_libs/reselect.mjs +0 -1
  158. package/dist/web-server/_libs/rou3.mjs +0 -8
  159. package/dist/web-server/_libs/seroval-plugins.mjs +0 -58
  160. package/dist/web-server/_libs/seroval.mjs +0 -1775
  161. package/dist/web-server/_libs/space-separated-tokens.mjs +0 -11
  162. package/dist/web-server/_libs/srvx.mjs +0 -781
  163. package/dist/web-server/_libs/style-to-js.mjs +0 -72
  164. package/dist/web-server/_libs/style-to-object.mjs +0 -38
  165. package/dist/web-server/_libs/tabler__icons-react.mjs +0 -230
  166. package/dist/web-server/_libs/tanstack__history.mjs +0 -204
  167. package/dist/web-server/_libs/tanstack__query-core.mjs +0 -2552
  168. package/dist/web-server/_libs/tanstack__react-query.mjs +0 -190
  169. package/dist/web-server/_libs/tanstack__react-router.mjs +0 -1120
  170. package/dist/web-server/_libs/tanstack__react-store.mjs +0 -2
  171. package/dist/web-server/_libs/tanstack__router-core.mjs +0 -4288
  172. package/dist/web-server/_libs/tanstack__store.mjs +0 -1
  173. package/dist/web-server/_libs/trim-lines.mjs +0 -41
  174. package/dist/web-server/_libs/trough.mjs +0 -85
  175. package/dist/web-server/_libs/ufo.mjs +0 -54
  176. package/dist/web-server/_libs/unctx.mjs +0 -1
  177. package/dist/web-server/_libs/ungap__structured-clone.mjs +0 -224
  178. package/dist/web-server/_libs/unified.mjs +0 -661
  179. package/dist/web-server/_libs/unist-util-is.mjs +0 -100
  180. package/dist/web-server/_libs/unist-util-position.mjs +0 -27
  181. package/dist/web-server/_libs/unist-util-stringify-position.mjs +0 -27
  182. package/dist/web-server/_libs/unist-util-visit-parents.mjs +0 -83
  183. package/dist/web-server/_libs/unist-util-visit.mjs +0 -24
  184. package/dist/web-server/_libs/unstorage.mjs +0 -1
  185. package/dist/web-server/_libs/use-sync-external-store.mjs +0 -139
  186. package/dist/web-server/_libs/vfile-message.mjs +0 -138
  187. package/dist/web-server/_libs/vfile.mjs +0 -467
  188. package/dist/web-server/_libs/zod.mjs +0 -3915
  189. package/dist/web-server/_libs/zustand.mjs +0 -343
  190. package/dist/web-server/_libs/zwitch.mjs +0 -1
  191. package/dist/web-server/_ssr/index-0n2Z3BPQ.mjs +0 -369
  192. package/dist/web-server/_ssr/index-6itDALOw.mjs +0 -339
  193. package/dist/web-server/_ssr/index-BIRTrOmp.mjs +0 -449
  194. package/dist/web-server/_ssr/index-BPzfADac.mjs +0 -66
  195. package/dist/web-server/_ssr/index-BQE3bF14.mjs +0 -1870
  196. package/dist/web-server/_ssr/index-C5HpvlUP.mjs +0 -190
  197. package/dist/web-server/_ssr/index-C_ZxnypN.mjs +0 -213
  198. package/dist/web-server/_ssr/index-Ca8JFH8f.mjs +0 -612
  199. package/dist/web-server/_ssr/index-DNVESZiA.mjs +0 -513
  200. package/dist/web-server/_ssr/index.mjs +0 -1558
  201. package/dist/web-server/_ssr/input-CqXjTRQg.mjs +0 -20
  202. package/dist/web-server/_ssr/queries-3H_19mUt.mjs +0 -16
  203. package/dist/web-server/_ssr/router-sbsNus0Y.mjs +0 -2093
  204. package/dist/web-server/_ssr/start-HYkvq4Ni.mjs +0 -4
  205. package/dist/web-server/_ssr/switch-usf2F1UM.mjs +0 -33
  206. package/dist/web-server/_ssr/syntax-highlighter-5vezNTce.mjs +0 -62
  207. package/dist/web-server/_ssr/textarea-DfRheWY0.mjs +0 -18
  208. package/dist/web-server/_tanstack-start-manifest_v-DqW-pKEH.mjs +0 -4
  209. package/dist/web-server/index.mjs +0 -597
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':