hermes-web-ui 0.2.8 → 0.3.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 (119) hide show
  1. package/README.md +26 -0
  2. package/dist/client/assets/Add-HK51AOcE.js +1 -0
  3. package/dist/client/assets/{Button-Cxh6x3Um.js → Button-sy1PzWPU.js} +14 -14
  4. package/dist/client/assets/ChannelsView-B3Iv1qGF.js +1 -0
  5. package/dist/client/assets/ChannelsView-Do43S3tP.css +1 -0
  6. package/dist/client/assets/ChatView-BZljWOsk.js +127 -0
  7. package/dist/client/assets/ChatView-DWJ1E8rg.css +1 -0
  8. package/dist/client/assets/{Close-hb0eNgAb.js → Close-h76fWaJi.js} +11 -11
  9. package/dist/client/assets/FormItem-DAkggFjx.js +110 -0
  10. package/dist/client/assets/Input-BgeYIOdH.js +234 -0
  11. package/dist/client/assets/{InputNumber-qFTlSmmT.js → InputNumber-CP0Q1JN_.js} +4 -4
  12. package/dist/client/assets/{JobsView-rFyONi-b.js → JobsView-C3MddyvL.js} +2 -2
  13. package/dist/client/assets/JobsView-CvuV9mZY.css +1 -0
  14. package/dist/client/assets/LoginView-CA9w3oDH.css +1 -0
  15. package/dist/client/assets/{LoginView-C6C3nX0Y.js → LoginView-DR64ehSS.js} +1 -1
  16. package/dist/client/assets/LogsView-BG7Bro4j.css +1 -0
  17. package/dist/client/assets/{LogsView-CO8aslfu.js → LogsView-De8hT0ry.js} +1 -1
  18. package/dist/client/assets/{MarkdownRenderer-Cy_O_tQS.js → MarkdownRenderer-CgO4HMET.js} +1 -1
  19. package/dist/client/assets/MarkdownRenderer-Ct6cRT2D.css +1 -0
  20. package/dist/client/assets/MemoryView-DEyIgPS8.css +1 -0
  21. package/dist/client/assets/{MemoryView-DZjJTdI6.js → MemoryView-vo6uIeGe.js} +1 -1
  22. package/dist/client/assets/Modal-D0bHwidv.js +232 -0
  23. package/dist/client/assets/ModelsView-BhNEZwC0.css +1 -0
  24. package/dist/client/assets/{ModelsView-CvHz1aiJ.js → ModelsView-Czh0QZMT.js} +1 -1
  25. package/dist/client/assets/Popconfirm-C4L6j7Ed.js +16 -0
  26. package/dist/client/assets/Popover-CSVNytD9.js +117 -0
  27. package/dist/client/assets/ProfilesView-5N8GD-OY.js +440 -0
  28. package/dist/client/assets/ProfilesView-Chffv0-D.css +1 -0
  29. package/dist/client/assets/{Scrollbar-BHJ682ve.js → Scrollbar-CP3UrDPW.js} +17 -17
  30. package/dist/client/assets/Select-BX8K2ITf.js +340 -0
  31. package/dist/client/assets/SettingRow-CwidKv2Q.css +1 -0
  32. package/dist/client/assets/{SettingRow-pvADoOth.js → SettingRow-Dj1WGKUP.js} +1 -1
  33. package/dist/client/assets/SettingsView-8WKddMDg.js +352 -0
  34. package/dist/client/assets/SettingsView-C78xbLXK.css +1 -0
  35. package/dist/client/assets/SkillsView-D5bivF7Z.css +1 -0
  36. package/dist/client/assets/{SkillsView-Bl8tqv9V.js → SkillsView-huWlVVSd.js} +1 -1
  37. package/dist/client/assets/{Spin-BbaC-FmW.js → Spin-7qVct-fX.js} +10 -10
  38. package/dist/client/assets/{Suffix-yRSbGVfA.js → Suffix-CTB2TVbZ.js} +7 -7
  39. package/dist/client/assets/{Switch-Svs0Rnhi.js → Switch-DznAKpSq.js} +15 -15
  40. package/dist/client/assets/{TerminalView-XDDNp69h.js → TerminalView-D8h6hUON.js} +2 -2
  41. package/dist/client/assets/TerminalView-DPdn9YA7.css +1 -0
  42. package/dist/client/assets/Tooltip-hZdU3QDl.js +1 -0
  43. package/dist/client/assets/UsageView-CnADqqzf.css +1 -0
  44. package/dist/client/assets/{UsageView-xqsO8W9W.js → UsageView-c67PH6UD.js} +1 -1
  45. package/dist/client/assets/{Warning-Srffo82k.js → Warning-ajVqmXLg.js} +1 -1
  46. package/dist/client/assets/_common-DgdkN_d5.js +1 -0
  47. package/dist/client/assets/{_plugin-vue_export-helper-TyZ5GTTn.js → _plugin-vue_export-helper-Cla2Sei2.js} +1 -1
  48. package/dist/client/assets/{app-DTyari-k.js → app-Dn_xFNgc.js} +1 -1
  49. package/dist/client/assets/app-DuW413U3.js +1 -0
  50. package/dist/client/assets/browser-Bj_pylgC.js +47 -0
  51. package/dist/client/assets/chat-oRYxV4zZ.js +6 -0
  52. package/dist/client/assets/composables-NK3hMwZN.js +1 -0
  53. package/dist/client/assets/dance-dark-BBm98kyn.mp4 +0 -0
  54. package/dist/client/assets/dance-light-424rxYpN.mp4 +0 -0
  55. package/dist/client/assets/{fade-in-scale-up.cssr-CiZdBCR3.js → fade-in-scale-up.cssr-A5vydVRa.js} +1 -1
  56. package/dist/client/assets/index-8Iu2oDQ9.css +1 -0
  57. package/dist/client/assets/index-CNVL9pSf.js +284 -0
  58. package/dist/client/assets/{jobs-DxRPm5Sf.js → jobs-D6sYt3p2.js} +1 -1
  59. package/dist/client/assets/light-2H7dwk18.js +1 -0
  60. package/dist/client/assets/light-BRnwbni5.js +71 -0
  61. package/dist/client/assets/light-BlRycys6.js +1 -0
  62. package/dist/client/assets/light-DTze2Byq.js +1 -0
  63. package/dist/client/assets/light-DyxaUrhN.js +1 -0
  64. package/dist/client/assets/light-s39H4IqC.js +1 -0
  65. package/dist/client/assets/{pinia-BTE-WqCz.js → pinia-0T1SpgZT.js} +1 -1
  66. package/dist/client/assets/profiles-CXj2WAET.js +23 -0
  67. package/dist/client/assets/{router-mPoLLYeq.js → router-k7Mt11r-.js} +2 -2
  68. package/dist/client/assets/{sessions-Bv5JsX3U.js → sessions-C04iLbID.js} +1 -1
  69. package/dist/client/assets/{skills-CXFhBwRj.js → skills-CrJMmxLv.js} +1 -1
  70. package/dist/client/assets/thinking-dark-yYkYRZLs.mp4 +0 -0
  71. package/dist/client/assets/thinking-light-BjeGd2gJ.mp4 +0 -0
  72. package/dist/client/assets/use-compitable-CBi_h52X.js +1 -0
  73. package/dist/client/assets/{use-message-B3svE98N.js → use-message-kLSX34Jy.js} +1 -1
  74. package/dist/client/assets/useTheme-OKrDn5ib.js +1 -0
  75. package/dist/client/index.html +30 -24
  76. package/dist/server/index.js +17 -3
  77. package/dist/server/routes/hermes/profiles.js +52 -5
  78. package/dist/server/routes/hermes/proxy-handler.js +45 -17
  79. package/dist/server/routes/hermes/terminal.js +32 -4
  80. package/dist/server/services/hermes-cli.js +41 -20
  81. package/package.json +9 -2
  82. package/dist/client/assets/ChannelsView-CQJ1fX8O.js +0 -1
  83. package/dist/client/assets/ChannelsView-D168bwSq.css +0 -1
  84. package/dist/client/assets/ChatView-Ds8Q3Yrv.css +0 -1
  85. package/dist/client/assets/ChatView-nxyDDB73.js +0 -127
  86. package/dist/client/assets/FormItem-dHtHjgtl.js +0 -110
  87. package/dist/client/assets/Input-CZZ9TfBw.js +0 -234
  88. package/dist/client/assets/JobsView-Bbg7Yvse.css +0 -1
  89. package/dist/client/assets/LoginView-Bt3yT3yN.css +0 -1
  90. package/dist/client/assets/LogsView-D9Es8tZb.css +0 -1
  91. package/dist/client/assets/MarkdownRenderer-Bd-nS4jK.css +0 -1
  92. package/dist/client/assets/MemoryView-hIfyUkwz.css +0 -1
  93. package/dist/client/assets/Modal-BLinjj9c.js +0 -232
  94. package/dist/client/assets/ModelsView-HlT7QxLF.css +0 -1
  95. package/dist/client/assets/Popconfirm-JTALkPda.js +0 -16
  96. package/dist/client/assets/Popover-0__1CUa3.js +0 -117
  97. package/dist/client/assets/ProfilesView-Dv1-Qa9-.js +0 -1
  98. package/dist/client/assets/ProfilesView-oIk8yBVG.css +0 -1
  99. package/dist/client/assets/Select-DqPBeTT3.js +0 -340
  100. package/dist/client/assets/SettingRow-DsjrPlmy.css +0 -1
  101. package/dist/client/assets/SettingsView-Y522req-.js +0 -352
  102. package/dist/client/assets/SettingsView-vVdZjtPc.css +0 -1
  103. package/dist/client/assets/SkillsView-l8q6J0Ov.css +0 -1
  104. package/dist/client/assets/Tag-DdwPhG9T.js +0 -71
  105. package/dist/client/assets/TerminalView-CuqATvpq.css +0 -1
  106. package/dist/client/assets/Tooltip-CHbB9ntH.js +0 -1
  107. package/dist/client/assets/UsageView-BgU-h2NT.css +0 -1
  108. package/dist/client/assets/app-CpUVwd9n.js +0 -1
  109. package/dist/client/assets/browser-DbKCW2k6.js +0 -47
  110. package/dist/client/assets/chat-BmXwfHmM.js +0 -6
  111. package/dist/client/assets/composables-BoU_Wud6.js +0 -1
  112. package/dist/client/assets/dance-gGRu7JHG.mp4 +0 -0
  113. package/dist/client/assets/get-DUDt1SRf.js +0 -1
  114. package/dist/client/assets/index-BT19Bivl.css +0 -1
  115. package/dist/client/assets/index-WIPEA9ik.js +0 -306
  116. package/dist/client/assets/profiles-C9UmdPLd.js +0 -1
  117. package/dist/client/assets/thinking-Cwsva50h.mp4 +0 -0
  118. package/dist/client/assets/use-compitable-aofta5ri.js +0 -1
  119. /package/dist/client/assets/{omit-DKSz8IbY.js → omit-1BRB6K75.js} +0 -0
@@ -1 +1 @@
1
- import{o as e}from"./router-mPoLLYeq.js";async function t(t,n){let r=new URLSearchParams;t&&r.set(`source`,t),n&&r.set(`limit`,String(n));let i=r.toString();return(await e(`/api/hermes/sessions${i?`?${i}`:``}`)).sessions}async function n(t){try{return(await e(`/api/hermes/sessions/${t}`)).session}catch{return null}}async function r(t){try{return await e(`/api/hermes/sessions/${t}`,{method:`DELETE`}),!0}catch{return!1}}async function i(t,n){try{return await e(`/api/hermes/sessions/${t}/rename`,{method:`POST`,body:JSON.stringify({title:n})}),!0}catch{return!1}}export{i,n,t as r,r as t};
1
+ import{o as e}from"./router-k7Mt11r-.js";async function t(t,n){let r=new URLSearchParams;t&&r.set(`source`,t),n&&r.set(`limit`,String(n));let i=r.toString();return(await e(`/api/hermes/sessions${i?`?${i}`:``}`)).sessions}async function n(t){try{return(await e(`/api/hermes/sessions/${t}`)).session}catch{return null}}async function r(t){try{return await e(`/api/hermes/sessions/${t}`,{method:`DELETE`}),!0}catch{return!1}}async function i(t,n){try{return await e(`/api/hermes/sessions/${t}/rename`,{method:`POST`,body:JSON.stringify({title:n})}),!0}catch{return!1}}export{i,n,t as r,r as t};
@@ -1 +1 @@
1
- import{o as e}from"./router-mPoLLYeq.js";async function t(){return(await e(`/api/hermes/skills`)).categories}async function n(t){return(await e(`/api/hermes/skills/${t}`)).content}async function r(t,n){return(await e(`/api/hermes/skills/${t}/${n}/files`)).files}async function i(){return e(`/api/hermes/memory`)}async function a(t,n){await e(`/api/hermes/memory`,{method:`POST`,body:JSON.stringify({section:t,content:n})})}async function o(t,n){await e(`/api/hermes/skills/toggle`,{method:`PUT`,body:JSON.stringify({name:t,enabled:n})})}export{a,t as i,n,o,r,i as t};
1
+ import{o as e}from"./router-k7Mt11r-.js";async function t(){return(await e(`/api/hermes/skills`)).categories}async function n(t){return(await e(`/api/hermes/skills/${t}`)).content}async function r(t,n){return(await e(`/api/hermes/skills/${t}/${n}/files`)).files}async function i(){return e(`/api/hermes/memory`)}async function a(t,n){await e(`/api/hermes/memory`,{method:`POST`,body:JSON.stringify({section:t,content:n})})}async function o(t,n){await e(`/api/hermes/skills/toggle`,{method:`PUT`,body:JSON.stringify({name:t,enabled:n})})}export{a,t as i,n,o,r,i as t};
@@ -0,0 +1 @@
1
+ import{C as e}from"./router-k7Mt11r-.js";function t(t,n){return e(()=>{for(let e of n)if(t[e]!==void 0)return t[e];return t[n[n.length-1]]})}export{t};
@@ -1 +1 @@
1
- import{F as e}from"./router-mPoLLYeq.js";import{J as t,Q as n}from"./browser-DbKCW2k6.js";var r=n(`n-message-api`),i=n(`n-message-provider`);function a(){let n=e(r,null);return n===null&&t(`use-message`,"No outer <n-message-provider /> founded. See prerequisite in https://www.naiveui.com/en-US/os-theme/components/message for more details. If you want to use `useMessage` outside setup, please check https://www.naiveui.com/zh-CN/os-theme/components/message#Q-&-A."),n}export{r as n,i as r,a as t};
1
+ import{F as e}from"./router-k7Mt11r-.js";import{$ as t,Y as n}from"./browser-Bj_pylgC.js";var r=t(`n-message-api`),i=t(`n-message-provider`);function a(){let t=e(r,null);return t===null&&n(`use-message`,"No outer <n-message-provider /> founded. See prerequisite in https://www.naiveui.com/en-US/os-theme/components/message for more details. If you want to use `useMessage` outside setup, please check https://www.naiveui.com/zh-CN/os-theme/components/message#Q-&-A."),t}export{r as n,i as r,a as t};
@@ -0,0 +1 @@
1
+ import{X as e,ct as t}from"./router-k7Mt11r-.js";var n=`hermes_theme`,r=t(localStorage.getItem(n)||`system`),i=t(!1);function a(e){i.value=e,document.documentElement.classList.toggle(`dark`,e)}function o(e){return e===`system`?window.matchMedia(`(prefers-color-scheme: dark)`).matches:e===`dark`}a(o(r.value)),window.matchMedia(`(prefers-color-scheme: dark)`).addEventListener(`change`,()=>{r.value===`system`&&a(o(`system`))}),e(r,e=>{localStorage.setItem(n,e),a(o(e))});function s(){function e(e){r.value=e}function t(){r.value=i.value?`light`:`dark`}return{mode:r,isDark:i,setMode:e,toggleTheme:t}}export{s as t};
@@ -6,32 +6,38 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>Hermes</title>
9
- <script type="module" crossorigin src="/assets/index-WIPEA9ik.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/router-mPoLLYeq.js">
11
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-TyZ5GTTn.js">
12
- <link rel="modulepreload" crossorigin href="/assets/browser-DbKCW2k6.js">
13
- <link rel="modulepreload" crossorigin href="/assets/Scrollbar-BHJ682ve.js">
14
- <link rel="modulepreload" crossorigin href="/assets/get-DUDt1SRf.js">
15
- <link rel="modulepreload" crossorigin href="/assets/Close-hb0eNgAb.js">
16
- <link rel="modulepreload" crossorigin href="/assets/Button-Cxh6x3Um.js">
17
- <link rel="modulepreload" crossorigin href="/assets/use-compitable-aofta5ri.js">
18
- <link rel="modulepreload" crossorigin href="/assets/Popover-0__1CUa3.js">
19
- <link rel="modulepreload" crossorigin href="/assets/Suffix-yRSbGVfA.js">
20
- <link rel="modulepreload" crossorigin href="/assets/fade-in-scale-up.cssr-CiZdBCR3.js">
21
- <link rel="modulepreload" crossorigin href="/assets/Tag-DdwPhG9T.js">
9
+ <script type="module" crossorigin src="/assets/index-CNVL9pSf.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/router-k7Mt11r-.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-Cla2Sei2.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/browser-Bj_pylgC.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/Scrollbar-CP3UrDPW.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/use-compitable-CBi_h52X.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/Popover-CSVNytD9.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/Close-h76fWaJi.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/Button-sy1PzWPU.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/Suffix-CTB2TVbZ.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/fade-in-scale-up.cssr-A5vydVRa.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/light-BRnwbni5.js">
22
21
  <link rel="modulepreload" crossorigin href="/assets/create-5zWq3BEB.js">
23
- <link rel="modulepreload" crossorigin href="/assets/Select-DqPBeTT3.js">
24
- <link rel="modulepreload" crossorigin href="/assets/Warning-Srffo82k.js">
25
- <link rel="modulepreload" crossorigin href="/assets/Modal-BLinjj9c.js">
26
- <link rel="modulepreload" crossorigin href="/assets/omit-DKSz8IbY.js">
27
- <link rel="modulepreload" crossorigin href="/assets/use-message-B3svE98N.js">
28
- <link rel="modulepreload" crossorigin href="/assets/pinia-BTE-WqCz.js">
29
- <link rel="modulepreload" crossorigin href="/assets/sessions-Bv5JsX3U.js">
30
- <link rel="modulepreload" crossorigin href="/assets/app-DTyari-k.js">
31
- <link rel="modulepreload" crossorigin href="/assets/chat-BmXwfHmM.js">
32
- <link rel="modulepreload" crossorigin href="/assets/profiles-C9UmdPLd.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/Select-BX8K2ITf.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/Warning-ajVqmXLg.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/Modal-D0bHwidv.js">
25
+ <link rel="modulepreload" crossorigin href="/assets/pinia-0T1SpgZT.js">
26
+ <link rel="modulepreload" crossorigin href="/assets/profiles-CXj2WAET.js">
27
+ <link rel="modulepreload" crossorigin href="/assets/omit-1BRB6K75.js">
28
+ <link rel="modulepreload" crossorigin href="/assets/sessions-C04iLbID.js">
29
+ <link rel="modulepreload" crossorigin href="/assets/app-Dn_xFNgc.js">
30
+ <link rel="modulepreload" crossorigin href="/assets/chat-oRYxV4zZ.js">
31
+ <link rel="modulepreload" crossorigin href="/assets/light-DyxaUrhN.js">
32
+ <link rel="modulepreload" crossorigin href="/assets/light-s39H4IqC.js">
33
+ <link rel="modulepreload" crossorigin href="/assets/use-message-kLSX34Jy.js">
34
+ <link rel="modulepreload" crossorigin href="/assets/light-2H7dwk18.js">
35
+ <link rel="modulepreload" crossorigin href="/assets/_common-DgdkN_d5.js">
36
+ <link rel="modulepreload" crossorigin href="/assets/light-DTze2Byq.js">
37
+ <link rel="modulepreload" crossorigin href="/assets/light-BlRycys6.js">
38
+ <link rel="modulepreload" crossorigin href="/assets/useTheme-OKrDn5ib.js">
33
39
  <link rel="modulepreload" crossorigin href="/assets/logo-Cd-t_oGE.js">
34
- <link rel="stylesheet" crossorigin href="/assets/index-BT19Bivl.css">
40
+ <link rel="stylesheet" crossorigin href="/assets/index-8Iu2oDQ9.css">
35
41
  </head>
36
42
 
37
43
  <body>
@@ -272,6 +272,19 @@ async function ensureApiServerConfig() {
272
272
  }
273
273
  async function ensureGatewayRunning() {
274
274
  const upstream = config_1.config.upstream.replace(/\/$/, '');
275
+ const waitForGatewayReady = async (timeoutMs = 15000) => {
276
+ const deadline = Date.now() + timeoutMs;
277
+ while (Date.now() < deadline) {
278
+ try {
279
+ const res = await fetch(`${upstream}/health`, { signal: AbortSignal.timeout(2000) });
280
+ if (res.ok)
281
+ return true;
282
+ }
283
+ catch { }
284
+ await new Promise(r => setTimeout(r, 300));
285
+ }
286
+ return false;
287
+ };
275
288
  try {
276
289
  const res = await fetch(`${upstream}/health`, { signal: AbortSignal.timeout(5000) });
277
290
  if (res.ok)
@@ -282,11 +295,12 @@ async function ensureGatewayRunning() {
282
295
  try {
283
296
  // 👉 关键:保存 PID
284
297
  gatewayPid = await startGatewayBackground();
285
- await new Promise(r => setTimeout(r, 3000));
286
- const res = await fetch(`${upstream}/health`, { signal: AbortSignal.timeout(5000) });
287
- if (res.ok) {
298
+ if (await waitForGatewayReady()) {
288
299
  console.log(`✓ Gateway started (PID: ${gatewayPid})`);
289
300
  }
301
+ else {
302
+ console.error('gateway start failed: timed out waiting for health');
303
+ }
290
304
  }
291
305
  catch (err) {
292
306
  console.error('gateway start failed:', err.message);
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.profileRoutes = void 0;
40
40
  const router_1 = __importDefault(require("@koa/router"));
41
41
  const fs_1 = require("fs");
42
+ const promises_1 = require("fs/promises");
42
43
  const path_1 = require("path");
43
44
  const os_1 = require("os");
44
45
  const js_yaml_1 = __importDefault(require("js-yaml"));
@@ -278,19 +279,65 @@ exports.profileRoutes.post('/api/hermes/profiles/:name/export', async (ctx) => {
278
279
  ctx.body = { error: err.message };
279
280
  }
280
281
  });
281
- // POST /api/profiles/import - Import profile from archive
282
+ // POST /api/profiles/import - Import profile from uploaded archive
282
283
  exports.profileRoutes.post('/api/hermes/profiles/import', async (ctx) => {
283
- const { archive, name } = ctx.request.body;
284
- if (!archive) {
284
+ const contentType = ctx.get('content-type') || '';
285
+ if (!contentType.startsWith('multipart/form-data')) {
285
286
  ctx.status = 400;
286
- ctx.body = { error: 'Missing archive path' };
287
+ ctx.body = { error: 'Expected multipart/form-data' };
288
+ return;
289
+ }
290
+ const boundary = '--' + contentType.split('boundary=')[1];
291
+ if (!boundary || boundary === '--undefined') {
292
+ ctx.status = 400;
293
+ ctx.body = { error: 'Missing boundary' };
294
+ return;
295
+ }
296
+ const tmpDir = (0, path_1.join)((0, os_1.tmpdir)(), 'hermes-import');
297
+ await (0, promises_1.mkdir)(tmpDir, { recursive: true });
298
+ // Read raw body and parse multipart
299
+ const chunks = [];
300
+ for await (const chunk of ctx.req)
301
+ chunks.push(chunk);
302
+ const body = Buffer.concat(chunks).toString('latin1');
303
+ const parts = body.split(boundary).slice(1, -1);
304
+ let archivePath = '';
305
+ for (const part of parts) {
306
+ const headerEnd = part.indexOf('\r\n\r\n');
307
+ if (headerEnd === -1)
308
+ continue;
309
+ const header = part.substring(0, headerEnd);
310
+ const data = part.substring(headerEnd + 4, part.length - 2);
311
+ const filenameMatch = header.match(/filename="([^"]+)"/);
312
+ if (!filenameMatch)
313
+ continue;
314
+ const filename = filenameMatch[1];
315
+ const ext = filename.includes('.') ? '.' + filename.split('.').pop() : '';
316
+ if (!['.gz', '.tar.gz', '.zip', '.tgz'].includes(ext))
317
+ continue;
318
+ archivePath = (0, path_1.join)(tmpDir, filename);
319
+ await (0, promises_1.writeFile)(archivePath, Buffer.from(data, 'binary'));
320
+ break;
321
+ }
322
+ if (!archivePath) {
323
+ ctx.status = 400;
324
+ ctx.body = { error: 'No archive file found (.gz, .zip, .tgz)' };
287
325
  return;
288
326
  }
289
327
  try {
290
- const result = await hermesCli.importProfile(archive, name);
328
+ const result = await hermesCli.importProfile(archivePath);
329
+ // Clean up temp file
330
+ try {
331
+ (0, fs_1.unlinkSync)(archivePath);
332
+ }
333
+ catch { }
291
334
  ctx.body = { success: true, message: result.trim() };
292
335
  }
293
336
  catch (err) {
337
+ try {
338
+ (0, fs_1.unlinkSync)(archivePath);
339
+ }
340
+ catch { }
294
341
  ctx.status = 500;
295
342
  ctx.body = { error: err.message };
296
343
  }
@@ -2,6 +2,30 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.proxy = proxy;
4
4
  const config_1 = require("../../config");
5
+ function isTransientGatewayError(err) {
6
+ const msg = String(err?.message || '');
7
+ const causeCode = String(err?.cause?.code || '');
8
+ return (causeCode === 'ECONNREFUSED' ||
9
+ causeCode === 'ECONNRESET' ||
10
+ /ECONNREFUSED|ECONNRESET|fetch failed|socket hang up/i.test(msg));
11
+ }
12
+ async function waitForGatewayReady(upstream, timeoutMs = 5000) {
13
+ const deadline = Date.now() + timeoutMs;
14
+ const healthUrl = `${upstream}/health`;
15
+ while (Date.now() < deadline) {
16
+ try {
17
+ const res = await fetch(healthUrl, {
18
+ method: 'GET',
19
+ signal: AbortSignal.timeout(1200),
20
+ });
21
+ if (res.ok)
22
+ return true;
23
+ }
24
+ catch { }
25
+ await new Promise(resolve => setTimeout(resolve, 250));
26
+ }
27
+ return false;
28
+ }
5
29
  async function proxy(ctx) {
6
30
  const upstream = config_1.config.upstream.replace(/\/$/, '');
7
31
  // Rewrite path for upstream gateway:
@@ -9,8 +33,7 @@ async function proxy(ctx) {
9
33
  // /api/hermes/* -> /api/* (upstream uses /api/ prefix)
10
34
  const upstreamPath = ctx.path.replace(/^\/api\/hermes\/v1/, '/v1').replace(/^\/api\/hermes/, '/api');
11
35
  const url = `${upstream}${upstreamPath}${ctx.search || ''}`;
12
- console.log(`[PROXY] ${ctx.method} ${ctx.path} -> ${url}`);
13
- // Build headers — forward most, strip browser-specific ones
36
+ // Build headers forward most, strip browser/web-ui specific ones
14
37
  const headers = {};
15
38
  for (const [key, value] of Object.entries(ctx.headers)) {
16
39
  if (value == null)
@@ -19,42 +42,47 @@ async function proxy(ctx) {
19
42
  if (lower === 'host') {
20
43
  headers['host'] = new URL(upstream).host;
21
44
  }
22
- else if (lower !== 'origin' && lower !== 'referer' && lower !== 'connection') {
45
+ else if (lower === 'authorization' || lower === 'origin' || lower === 'referer' || lower === 'connection') {
46
+ continue;
47
+ }
48
+ else {
23
49
  const v = Array.isArray(value) ? value[0] : value;
24
50
  if (v)
25
51
  headers[key] = v;
26
52
  }
27
53
  }
28
- // Add SSE-friendly headers
29
- if (ctx.path.match(/\/events$/)) {
30
- headers['x-accel-buffering'] = 'no';
31
- headers['cache-control'] = 'no-cache';
32
- }
33
54
  try {
34
55
  // Build request body from raw body
35
56
  let body;
36
57
  if (ctx.req.method !== 'GET' && ctx.req.method !== 'HEAD') {
37
58
  body = ctx.request.rawBody;
38
59
  }
39
- const res = await fetch(url, {
60
+ const requestInit = {
40
61
  method: ctx.req.method,
41
62
  headers,
42
63
  body,
43
- });
64
+ };
65
+ let res;
66
+ try {
67
+ res = await fetch(url, requestInit);
68
+ }
69
+ catch (err) {
70
+ // Gateway may be restarting; wait briefly and retry once.
71
+ if (isTransientGatewayError(err) && await waitForGatewayReady(upstream)) {
72
+ res = await fetch(url, requestInit);
73
+ }
74
+ else {
75
+ throw err;
76
+ }
77
+ }
44
78
  // Set response headers
45
- const resHeaders = {};
46
79
  res.headers.forEach((value, key) => {
47
80
  const lower = key.toLowerCase();
48
81
  if (lower !== 'transfer-encoding' && lower !== 'connection') {
49
- resHeaders[key] = value;
82
+ ctx.set(key, value);
50
83
  }
51
84
  });
52
- if (ctx.path.match(/\/events$/)) {
53
- resHeaders['x-accel-buffering'] = 'no';
54
- resHeaders['cache-control'] = 'no-cache';
55
- }
56
85
  ctx.status = res.status;
57
- ctx.set(resHeaders);
58
86
  // Stream response body
59
87
  if (res.body) {
60
88
  const reader = res.body.getReader();
@@ -3,14 +3,42 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setupTerminalWebSocket = setupTerminalWebSocket;
4
4
  const ws_1 = require("ws");
5
5
  const fs_1 = require("fs");
6
+ const path_1 = require("path");
6
7
  const auth_1 = require("../../services/auth");
7
8
  let pty = null;
9
+ function ensureNodePtySpawnHelperExecutable() {
10
+ if (process.platform !== 'darwin')
11
+ return;
12
+ try {
13
+ const nodePtyRoot = (0, path_1.dirname)(require.resolve('node-pty/package.json'));
14
+ const helperCandidates = [
15
+ (0, path_1.join)(nodePtyRoot, 'build', 'Release', 'spawn-helper'),
16
+ (0, path_1.join)(nodePtyRoot, 'build', 'Debug', 'spawn-helper'),
17
+ (0, path_1.join)(nodePtyRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper'),
18
+ ];
19
+ for (const helperPath of helperCandidates) {
20
+ if (!(0, fs_1.existsSync)(helperPath))
21
+ continue;
22
+ try {
23
+ (0, fs_1.accessSync)(helperPath, fs_1.constants.X_OK);
24
+ }
25
+ catch {
26
+ (0, fs_1.chmodSync)(helperPath, 0o755);
27
+ console.log(`[Terminal] Restored execute bit for node-pty helper: ${helperPath}`);
28
+ }
29
+ }
30
+ }
31
+ catch (err) {
32
+ console.warn(`[Terminal] Could not normalize node-pty helper permissions: ${err?.message || err}`);
33
+ }
34
+ }
8
35
  try {
36
+ ensureNodePtySpawnHelperExecutable();
9
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports
10
38
  pty = require('node-pty');
11
39
  }
12
- catch {
13
- console.warn('[Terminal] node-pty failed to load, terminal feature disabled');
40
+ catch (err) {
41
+ console.warn(`[Terminal] node-pty failed to load, terminal feature disabled (${err?.message || 'unknown error'})`);
14
42
  }
15
43
  // ─── Shell detection ────────────────────────────────────────────
16
44
  function findShell() {
@@ -46,7 +74,7 @@ function createSession(shell) {
46
74
  });
47
75
  }
48
76
  catch (err) {
49
- throw new Error(`Failed to spawn shell "${shell}": ${err.message}. Run "npm rebuild node-pty" to fix.`);
77
+ throw new Error(`Failed to spawn shell "${shell}": ${err.message}`);
50
78
  }
51
79
  const session = {
52
80
  id,
@@ -262,5 +290,5 @@ function setupTerminalWebSocket(httpServer) {
262
290
  }));
263
291
  console.log(`[Terminal] First session created: ${firstSession.id} (${shellName(defaultShell)}, pid ${firstSession.pid})`);
264
292
  });
265
- console.log(`[Terminal] WebSocket ready at /terminal (shell: ${defaultShell})`);
293
+ console.log(`[Terminal] WebSocket ready at /terminal (shell: ${defaultShell}, transport: node-pty)`);
266
294
  }
@@ -21,9 +21,18 @@ exports.exportProfile = exportProfile;
21
21
  exports.setupReset = setupReset;
22
22
  exports.importProfile = importProfile;
23
23
  const child_process_1 = require("child_process");
24
+ const fs_1 = require("fs");
24
25
  const util_1 = require("util");
25
26
  const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
26
27
  const execOpts = { windowsHide: true };
28
+ const isDocker = (0, fs_1.existsSync)('/.dockerenv');
29
+ function resolveHermesBin() {
30
+ const envBin = process.env.HERMES_BIN?.trim();
31
+ if (envBin)
32
+ return envBin;
33
+ return 'hermes';
34
+ }
35
+ const HERMES_BIN = resolveHermesBin();
27
36
  /**
28
37
  * List sessions from Hermes CLI (without messages)
29
38
  */
@@ -32,7 +41,7 @@ async function listSessions(source, limit) {
32
41
  if (source)
33
42
  args.push('--source', source);
34
43
  try {
35
- const { stdout } = await execFileAsync('hermes', args, {
44
+ const { stdout } = await execFileAsync(HERMES_BIN, args, {
36
45
  maxBuffer: 50 * 1024 * 1024, // 50MB
37
46
  timeout: 30000,
38
47
  ...execOpts,
@@ -92,7 +101,7 @@ async function listSessions(source, limit) {
92
101
  async function getSession(id) {
93
102
  const args = ['sessions', 'export', '-', '--session-id', id];
94
103
  try {
95
- const { stdout } = await execFileAsync('hermes', args, {
104
+ const { stdout } = await execFileAsync(HERMES_BIN, args, {
96
105
  maxBuffer: 50 * 1024 * 1024,
97
106
  timeout: 30000,
98
107
  ...execOpts,
@@ -138,7 +147,7 @@ async function getSession(id) {
138
147
  */
139
148
  async function deleteSession(id) {
140
149
  try {
141
- await execFileAsync('hermes', ['sessions', 'delete', id, '--yes'], {
150
+ await execFileAsync(HERMES_BIN, ['sessions', 'delete', id, '--yes'], {
142
151
  timeout: 10000,
143
152
  ...execOpts,
144
153
  });
@@ -154,7 +163,7 @@ async function deleteSession(id) {
154
163
  */
155
164
  async function renameSession(id, title) {
156
165
  try {
157
- await execFileAsync('hermes', ['sessions', 'rename', id, title], {
166
+ await execFileAsync(HERMES_BIN, ['sessions', 'rename', id, title], {
158
167
  timeout: 10000,
159
168
  ...execOpts,
160
169
  });
@@ -170,7 +179,7 @@ async function renameSession(id, title) {
170
179
  */
171
180
  async function getVersion() {
172
181
  try {
173
- const { stdout } = await execFileAsync('hermes', ['--version'], { timeout: 5000, ...execOpts });
182
+ const { stdout } = await execFileAsync(HERMES_BIN, ['--version'], { timeout: 5000, ...execOpts });
174
183
  return stdout.trim();
175
184
  }
176
185
  catch {
@@ -181,7 +190,11 @@ async function getVersion() {
181
190
  * Start Hermes gateway (uses launchd/systemd)
182
191
  */
183
192
  async function startGateway() {
184
- const { stdout, stderr } = await execFileAsync('hermes', ['gateway', 'start'], {
193
+ if (isDocker) {
194
+ const pid = await startGatewayBackground();
195
+ return pid ? `Gateway started (PID: ${pid})` : 'Gateway start triggered';
196
+ }
197
+ const { stdout, stderr } = await execFileAsync(HERMES_BIN, ['gateway', 'start'], {
185
198
  timeout: 30000,
186
199
  ...execOpts,
187
200
  });
@@ -193,7 +206,7 @@ async function startGateway() {
193
206
  */
194
207
  async function startGatewayBackground() {
195
208
  const { spawn } = require('child_process');
196
- const child = spawn('hermes', ['gateway', 'run'], {
209
+ const child = spawn(HERMES_BIN, ['gateway', 'run'], {
197
210
  detached: true,
198
211
  stdio: 'ignore',
199
212
  windowsHide: true,
@@ -205,7 +218,15 @@ async function startGatewayBackground() {
205
218
  * Restart Hermes gateway
206
219
  */
207
220
  async function restartGateway() {
208
- const { stdout, stderr } = await execFileAsync('hermes', ['gateway', 'restart'], {
221
+ if (isDocker) {
222
+ try {
223
+ await stopGateway();
224
+ }
225
+ catch { }
226
+ const pid = await startGatewayBackground();
227
+ return pid ? `Gateway restarted (PID: ${pid})` : 'Gateway restart triggered';
228
+ }
229
+ const { stdout, stderr } = await execFileAsync(HERMES_BIN, ['gateway', 'restart'], {
209
230
  timeout: 30000,
210
231
  ...execOpts,
211
232
  });
@@ -215,7 +236,7 @@ async function restartGateway() {
215
236
  * Stop Hermes gateway
216
237
  */
217
238
  async function stopGateway() {
218
- const { stdout, stderr } = await execFileAsync('hermes', ['gateway', 'stop'], {
239
+ const { stdout, stderr } = await execFileAsync(HERMES_BIN, ['gateway', 'stop'], {
219
240
  timeout: 30000,
220
241
  ...execOpts,
221
242
  });
@@ -226,7 +247,7 @@ async function stopGateway() {
226
247
  */
227
248
  async function listLogFiles() {
228
249
  try {
229
- const { stdout } = await execFileAsync('hermes', ['logs', 'list'], {
250
+ const { stdout } = await execFileAsync(HERMES_BIN, ['logs', 'list'], {
230
251
  timeout: 10000,
231
252
  ...execOpts,
232
253
  });
@@ -261,7 +282,7 @@ async function readLogs(logName = 'agent', lines = 100, level, session, since) {
261
282
  if (since)
262
283
  args.push('--since', since);
263
284
  try {
264
- const { stdout } = await execFileAsync('hermes', args, {
285
+ const { stdout } = await execFileAsync(HERMES_BIN, args, {
265
286
  maxBuffer: 10 * 1024 * 1024,
266
287
  timeout: 15000,
267
288
  ...execOpts,
@@ -278,7 +299,7 @@ async function readLogs(logName = 'agent', lines = 100, level, session, since) {
278
299
  */
279
300
  async function listProfiles() {
280
301
  try {
281
- const { stdout } = await execFileAsync('hermes', ['profile', 'list'], {
302
+ const { stdout } = await execFileAsync(HERMES_BIN, ['profile', 'list'], {
282
303
  timeout: 10000,
283
304
  ...execOpts,
284
305
  });
@@ -311,7 +332,7 @@ async function listProfiles() {
311
332
  */
312
333
  async function getProfile(name) {
313
334
  try {
314
- const { stdout } = await execFileAsync('hermes', ['profile', 'show', name], {
335
+ const { stdout } = await execFileAsync(HERMES_BIN, ['profile', 'show', name], {
315
336
  timeout: 10000,
316
337
  ...execOpts,
317
338
  });
@@ -352,7 +373,7 @@ async function createProfile(name, clone) {
352
373
  if (clone)
353
374
  args.push('--clone');
354
375
  try {
355
- const { stdout, stderr } = await execFileAsync('hermes', args, {
376
+ const { stdout, stderr } = await execFileAsync(HERMES_BIN, args, {
356
377
  timeout: 15000,
357
378
  ...execOpts,
358
379
  });
@@ -368,7 +389,7 @@ async function createProfile(name, clone) {
368
389
  */
369
390
  async function deleteProfile(name) {
370
391
  try {
371
- await execFileAsync('hermes', ['profile', 'delete', name, '--yes'], {
392
+ await execFileAsync(HERMES_BIN, ['profile', 'delete', name, '--yes'], {
372
393
  timeout: 10000,
373
394
  ...execOpts,
374
395
  });
@@ -384,7 +405,7 @@ async function deleteProfile(name) {
384
405
  */
385
406
  async function renameProfile(oldName, newName) {
386
407
  try {
387
- await execFileAsync('hermes', ['profile', 'rename', oldName, newName], {
408
+ await execFileAsync(HERMES_BIN, ['profile', 'rename', oldName, newName], {
388
409
  timeout: 10000,
389
410
  ...execOpts,
390
411
  });
@@ -400,7 +421,7 @@ async function renameProfile(oldName, newName) {
400
421
  */
401
422
  async function useProfile(name) {
402
423
  try {
403
- const { stdout, stderr } = await execFileAsync('hermes', ['profile', 'use', name], {
424
+ const { stdout, stderr } = await execFileAsync(HERMES_BIN, ['profile', 'use', name], {
404
425
  timeout: 10000,
405
426
  ...execOpts,
406
427
  });
@@ -419,7 +440,7 @@ async function exportProfile(name, outputPath) {
419
440
  if (outputPath)
420
441
  args.push('--output', outputPath);
421
442
  try {
422
- const { stdout, stderr } = await execFileAsync('hermes', args, {
443
+ const { stdout, stderr } = await execFileAsync(HERMES_BIN, args, {
423
444
  timeout: 60000,
424
445
  ...execOpts,
425
446
  });
@@ -435,7 +456,7 @@ async function exportProfile(name, outputPath) {
435
456
  */
436
457
  async function setupReset() {
437
458
  try {
438
- const { stdout, stderr } = await execFileAsync('hermes', ['setup', '--non-interactive', '--reset'], {
459
+ const { stdout, stderr } = await execFileAsync(HERMES_BIN, ['setup', '--non-interactive', '--reset'], {
439
460
  timeout: 30000,
440
461
  ...execOpts,
441
462
  });
@@ -454,7 +475,7 @@ async function importProfile(archivePath, name) {
454
475
  if (name)
455
476
  args.push('--name', name);
456
477
  try {
457
- const { stdout, stderr } = await execFileAsync('hermes', args, {
478
+ const { stdout, stderr } = await execFileAsync(HERMES_BIN, args, {
458
479
  timeout: 60000,
459
480
  ...execOpts,
460
481
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermes-web-ui",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "Web dashboard for Hermes Agent — multi-platform AI chat, session management, scheduled jobs, usage analytics & channel configuration (Telegram, Discord, Slack, WhatsApp)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,7 +35,10 @@
35
35
  "dev:client": "vite --host",
36
36
  "dev:server": "nodemon --signal SIGTERM --watch packages/server/src -e ts,tsx --exec node -r ts-node/register packages/server/src/index.ts",
37
37
  "build": "vue-tsc -b && vite build && tsc -p packages/server/tsconfig.json",
38
- "preview": "vite preview"
38
+ "preview": "vite preview",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "test:coverage": "vitest run --coverage"
39
42
  },
40
43
  "files": [
41
44
  "bin/",
@@ -65,6 +68,7 @@
65
68
  "ws": "^8.20.0"
66
69
  },
67
70
  "devDependencies": {
71
+ "@pinia/testing": "^1.0.3",
68
72
  "@types/js-yaml": "^4.0.9",
69
73
  "@types/koa": "^2.15.0",
70
74
  "@types/koa__cors": "^5.0.0",
@@ -76,13 +80,16 @@
76
80
  "@types/qrcode": "^1.5.6",
77
81
  "@types/ws": "^8.18.1",
78
82
  "@vitejs/plugin-vue": "^6.0.5",
83
+ "@vue/test-utils": "^2.4.6",
79
84
  "@vue/tsconfig": "^0.9.1",
80
85
  "concurrently": "^9.2.1",
86
+ "jsdom": "^27.0.1",
81
87
  "nodemon": "^3.1.14",
82
88
  "sass": "^1.99.0",
83
89
  "ts-node": "^10.9.2",
84
90
  "typescript": "~6.0.2",
85
91
  "vite": "^8.0.4",
92
+ "vitest": "^3.2.4",
86
93
  "vue-tsc": "^3.2.6"
87
94
  }
88
95
  }