Compare commits

...

11 Commits

Author SHA1 Message Date
新觅源码库 a7c6f47c12 补充 Docker 配置与登录页品牌修改 2026-06-02 06:07:58 +00:00
新觅源码库 f10cd4cd9a 迁移至新觅源码库 — 替换社区链接,保留AI功能 2026-06-02 03:44:33 +00:00
ekko 7aa483f003 Fix desktop runtime cold start handling (#1233)
* fix desktop runtime cold start handling

* fix windows desktop python startup env

* Revert "fix windows desktop python startup env"

This reverts commit 3718ba7586ab1a672c7e599ff1e315dfa76d7cda.

* bump desktop release version to 0.6.8
2026-06-02 10:58:15 +08:00
ekko 1acfb6486b fix runtime workflow checkout ref (#1231) 2026-06-02 09:02:17 +08:00
sir1st 00ea452310 Codex/pr 1217 (#1226)
* bundle node and windows git runtimes

* split desktop runtime into release package

* fix desktop runtime packaging ci

* embed desktop runtime release tag

* show desktop runtime download progress

* fix desktop runtime release handling

* refactor desktop runtime version config

* fix desktop package license

---------

Co-authored-by: xingzhi <chuzihao.czh@alibaba-inc.com>
Co-authored-by: ekko <fqsy1416@gmail.com>
2026-06-02 08:55:17 +08:00
ekko 7440da9d23 fix mac desktop signing file limit (#1222) 2026-06-01 21:48:43 +08:00
ekko 0835732aba remove PR desktop smoke test (#1221) 2026-06-01 21:46:38 +08:00
ekko c27a12f56c [codex] fix Windows desktop browser packaging (#1219)
* fix windows hermes home fallback

* bundle Hermes desktop browser runtime

* bundle desktop channel dependencies

* avoid matrix e2ee build dependency

* fix windows npm shim execution

* fix bundled agent-browser chrome packaging

* fix agent-browser chrome fallback copy

* fix windows agent-browser home lookup

* copy agent-browser chrome after install

* fix browser output decoding on windows

---------

Co-authored-by: xingzhi <chuzihao.czh@alibaba-inc.com>
2026-06-01 21:35:26 +08:00
ekko 90929d0bfb add hermes studio cli shim (#1209) 2026-06-01 16:15:50 +08:00
ekko ed905e419d set package version to 0.6.7 (#1208) 2026-06-01 14:38:36 +08:00
ekko 6972717193 hide desktop python subprocesses on login (#1206) 2026-06-01 13:58:00 +08:00
76 changed files with 2423 additions and 1376 deletions
-101
View File
@@ -1,101 +0,0 @@
name: Bug Report
description: File a bug report to help us improve
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: version
attributes:
label: Hermes Web UI Version
description: What version of Hermes Web UI are you using?
placeholder: e.g., v0.5.8
validations:
required: true
- type: input
id: hermes_version
attributes:
label: Hermes Agent Version
description: What version of Hermes Agent are you using?
placeholder: e.g., v0.12.0
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What you expected to happen
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened
placeholder: |
If applicable, add screenshots to help explain your problem
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs / Error Messages
description: Paste any relevant logs or error messages
render: shell
- type: dropdown
id: environment
attributes:
label: Environment
description: Where are you running Hermes Web UI?
options:
- Docker
- macOS
- Linux
- Windows
- WSL
multiple: true
validations:
required: true
- type: input
id: node_version
attributes:
label: Node Version
description: What version of Node.js are you using?
placeholder: e.g., v24.14.1
- type: textarea
id: additional
attributes:
label: Additional Context
description: Any other context about the problem
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Documentation
url: https://github.com/EKKOLearnAI/hermes-web-ui#readme
about: Please check the documentation first
- name: GitHub Discussions
url: https://github.com/EKKOLearnAI/hermes-web-ui/discussions
about: Use GitHub Discussions for questions that don't fit as issues
@@ -1,76 +0,0 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a new feature! Please fill out the form below.
- type: textarea
id: problem
attributes:
label: Problem Statement
description: What problem does this feature solve? What pain point does it address?
placeholder: |
I'm always frustrated when...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: What would you like to see implemented?
placeholder: |
I think adding X would be great because...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions or workarounds?
- type: dropdown
id: priority
attributes:
label: Priority
description: How important is this feature to you?
options:
- Critical - blocking my usage
- High - really need this
- Medium - nice to have
- Low - would be convenient
validations:
required: true
- type: textarea
id: use_cases
attributes:
label: Use Cases
description: Describe specific use cases where this feature would be helpful
placeholder: |
1. When I do X...
2. When I need to Y...
- type: checkboxes
id: contribution
attributes:
label: Willing to Contribute?
description: Would you be willing to help implement this feature?
options:
- label: Yes, I'd like to submit a PR
required: false
- label: Yes, but I need guidance
required: false
- label: No, I don't have time
required: false
- type: textarea
id: additional
attributes:
label: Additional Context
description: Any other context, mockups, or examples about the feature request
-22
View File
@@ -1,22 +0,0 @@
---
name: General Issue
about: Use this for issues that don't fit into bug reports or feature requests
title: '[Question]: '
labels: ['question']
assignees: ''
---
## Please describe your issue
<!-- Provide a clear description of what you'd like to ask or discuss -->
## Context
<!-- Add any other context or screenshots about the issue -->
## Environment (if applicable)
- Hermes Web UI Version:
- Hermes Agent Version:
- Operating System:
- Node Version:
-113
View File
@@ -1,113 +0,0 @@
name: Build
on:
push:
branches:
- main
pull_request:
branches:
- main
- base
permissions:
contents: read
concurrency:
group: build-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- name: Install dependencies
run: |
npm ci --ignore-scripts
npm rebuild node-pty
- name: Check repository harness
run: npm run harness:check
- name: Test with coverage
run: npm run test:coverage
- name: Build
run: npm run build
# Smoke test only: verify desktop packaging still works on pull requests.
# Full multi-platform release artifacts are built by desktop-release.yml on release.
desktop:
name: Desktop smoke test (${{ matrix.label }})
needs: build
if: github.event_name == 'pull_request'
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- label: Linux x64
runner: ubuntu-22.04
target_os: linux
target_arch: x64
electron_target: "--linux AppImage deb --x64"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: |
package-lock.json
packages/desktop/package-lock.json
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Install web UI dependencies
run: |
npm ci --ignore-scripts
npm rebuild node-pty
- name: Build web UI
run: npm run build
- name: Keep production web UI dependencies only
run: npm prune --omit=dev --no-audit --no-fund
- name: Install desktop dependencies
run: npm ci --prefix packages/desktop --no-audit --no-fund
- name: Prepare bundled Python
env:
TARGET_OS: ${{ matrix.target_os }}
TARGET_ARCH: ${{ matrix.target_arch }}
run: npm --prefix packages/desktop run prepare:python
- name: Build desktop artifact
run: npm --prefix packages/desktop run dist -- ${{ matrix.electron_target }} --publish never
- name: Upload desktop artifacts
uses: actions/upload-artifact@v4
with:
name: desktop-${{ matrix.target_os }}-${{ matrix.target_arch }}
path: |
packages/desktop/release/*.AppImage
packages/desktop/release/*.deb
packages/desktop/release/latest*.yml
if-no-files-found: error
retention-days: 7
-207
View File
@@ -1,207 +0,0 @@
name: Manual Desktop Build
on:
workflow_dispatch:
inputs:
target_os:
description: "Desktop target OS"
required: true
type: choice
default: win32
options:
- win32
- darwin
- linux
target_arch:
description: "Desktop target architecture"
required: true
type: choice
default: x64
options:
- x64
- arm64
release_tag:
description: "Optional release tag to attach artifacts to"
required: false
type: string
permissions:
contents: write
concurrency:
group: desktop-manual-${{ github.event.inputs.target_os }}-${{ github.event.inputs.target_arch }}-${{ github.ref }}
cancel-in-progress: false
jobs:
validate:
runs-on: ubuntu-latest
outputs:
label: ${{ steps.target.outputs.label }}
runner: ${{ steps.target.outputs.runner }}
target_os: ${{ steps.target.outputs.target_os }}
target_arch: ${{ steps.target.outputs.target_arch }}
electron_target: ${{ steps.target.outputs.electron_target }}
artifact_name: ${{ steps.target.outputs.artifact_name }}
artifact_files: ${{ steps.target.outputs.artifact_files }}
steps:
- name: Select requested target
id: target
shell: bash
run: |
write_common_outputs() {
{
echo "label=$1"
echo "runner=$2"
echo "target_os=${{ github.event.inputs.target_os }}"
echo "target_arch=${{ github.event.inputs.target_arch }}"
echo "electron_target=$3"
echo "artifact_name=$4"
echo "artifact_files<<EOF"
shift 4
printf '%s\n' "$@"
echo "EOF"
} >> "$GITHUB_OUTPUT"
}
case "${{ github.event.inputs.target_os }}-${{ github.event.inputs.target_arch }}" in
win32-x64)
write_common_outputs "Windows x64" "windows-latest" "--win nsis --x64" "desktop-win32-x64" \
"packages/desktop/release/*.exe" \
"packages/desktop/release/*.exe.blockmap" \
"packages/desktop/release/latest*.yml"
;;
darwin-arm64)
write_common_outputs "macOS arm64" "macos-14" "--mac dmg zip --arm64" "desktop-darwin-arm64" \
"packages/desktop/release/*.dmg" \
"packages/desktop/release/*.dmg.blockmap" \
"packages/desktop/release/*.zip" \
"packages/desktop/release/*.zip.blockmap" \
"packages/desktop/release/latest*.yml"
;;
darwin-x64)
write_common_outputs "macOS x64" "macos-15-intel" "--mac dmg zip --x64" "desktop-darwin-x64" \
"packages/desktop/release/*.dmg" \
"packages/desktop/release/*.dmg.blockmap" \
"packages/desktop/release/*.zip" \
"packages/desktop/release/*.zip.blockmap" \
"packages/desktop/release/latest*.yml"
;;
linux-x64)
write_common_outputs "Linux x64" "ubuntu-22.04" "--linux AppImage deb --x64" "desktop-linux-x64" \
"packages/desktop/release/*.AppImage" \
"packages/desktop/release/*.deb" \
"packages/desktop/release/latest*.yml"
;;
linux-arm64)
write_common_outputs "Linux arm64" "ubuntu-22.04-arm" "--linux AppImage --arm64" "desktop-linux-arm64" \
"packages/desktop/release/*.AppImage" \
"packages/desktop/release/latest*.yml"
;;
*)
echo "Unsupported desktop target: ${{ github.event.inputs.target_os }} ${{ github.event.inputs.target_arch }}" >&2
exit 1
;;
esac
desktop:
name: Desktop (${{ needs.validate.outputs.label }})
needs: validate
runs-on: ${{ needs.validate.outputs.runner }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: |
package-lock.json
packages/desktop/package-lock.json
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Install web UI dependencies
run: |
npm ci --ignore-scripts
npm rebuild node-pty
- name: Build web UI
run: npm run build
- name: Keep production web UI dependencies only
run: npm prune --omit=dev --no-audit --no-fund
- name: Install desktop dependencies
run: npm ci --prefix packages/desktop --no-audit --no-fund
- name: Prepare bundled Python
env:
TARGET_OS: ${{ needs.validate.outputs.target_os }}
TARGET_ARCH: ${{ needs.validate.outputs.target_arch }}
run: npm --prefix packages/desktop run prepare:python
- name: Configure macOS signing
if: needs.validate.outputs.target_os == 'darwin'
shell: bash
env:
MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
MAC_APPLE_ID: ${{ secrets.APPLE_ID }}
MAC_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
MAC_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
write_env() {
local name="$1"
local value="$2"
if [ -n "$value" ]; then
{
echo "$name<<EOF"
echo "$value"
echo "EOF"
} >> "$GITHUB_ENV"
fi
}
if [ -z "${MAC_CSC_LINK:-}" ]; then
echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV"
echo "MAC_BUILD_EXTRA_ARGS=--config.mac.notarize=false" >> "$GITHUB_ENV"
echo "No macOS signing certificate configured; building unsigned and skipping notarization."
exit 0
fi
write_env "CSC_LINK" "$MAC_CSC_LINK"
write_env "CSC_KEY_PASSWORD" "$MAC_CSC_KEY_PASSWORD"
if [ -n "${MAC_APPLE_ID:-}" ] && [ -n "${MAC_APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${MAC_APPLE_TEAM_ID:-}" ]; then
write_env "APPLE_ID" "$MAC_APPLE_ID"
write_env "APPLE_APP_SPECIFIC_PASSWORD" "$MAC_APPLE_APP_SPECIFIC_PASSWORD"
write_env "APPLE_TEAM_ID" "$MAC_APPLE_TEAM_ID"
echo "macOS signing and notarization are configured."
else
echo "MAC_BUILD_EXTRA_ARGS=--config.mac.notarize=false" >> "$GITHUB_ENV"
echo "macOS signing certificate configured; Apple notarization credentials incomplete, skipping notarization."
fi
- name: Build desktop artifact
shell: bash
run: npm --prefix packages/desktop run dist -- ${{ needs.validate.outputs.electron_target }} ${MAC_BUILD_EXTRA_ARGS:-} --publish never
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: ${{ needs.validate.outputs.artifact_name }}
path: ${{ needs.validate.outputs.artifact_files }}
if-no-files-found: error
retention-days: 7
- name: Upload artifacts to release
if: github.event.inputs.release_tag != ''
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.release_tag }}
fail_on_unmatched_files: true
files: ${{ needs.validate.outputs.artifact_files }}
-204
View File
@@ -1,204 +0,0 @@
name: Publish Desktop Artifacts to Release
on:
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to attach artifacts to (e.g. v0.6.5)"
required: true
release:
types: [published]
permissions:
contents: write
concurrency:
group: desktop-release-${{ github.event.release.tag_name || github.event.inputs.tag || github.ref }}
cancel-in-progress: false
jobs:
desktop:
name: Desktop (${{ matrix.label }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- label: macOS arm64
runner: macos-14
target_os: darwin
target_arch: arm64
electron_target: "--mac dmg zip --arm64"
artifact_files: |
packages/desktop/release/*.dmg
packages/desktop/release/*.dmg.blockmap
packages/desktop/release/*.zip
packages/desktop/release/*.zip.blockmap
- label: macOS x64
runner: macos-15-intel
target_os: darwin
target_arch: x64
electron_target: "--mac dmg zip --x64"
artifact_files: |
packages/desktop/release/*.dmg
packages/desktop/release/*.dmg.blockmap
packages/desktop/release/*.zip
packages/desktop/release/*.zip.blockmap
- label: Windows x64
runner: windows-latest
target_os: win32
target_arch: x64
electron_target: "--win nsis --x64"
artifact_files: |
packages/desktop/release/*.exe
packages/desktop/release/*.exe.blockmap
packages/desktop/release/latest*.yml
- label: Linux x64
runner: ubuntu-22.04
target_os: linux
target_arch: x64
electron_target: "--linux AppImage deb --x64"
artifact_files: |
packages/desktop/release/*.AppImage
packages/desktop/release/*.deb
packages/desktop/release/latest*.yml
- label: Linux arm64
runner: ubuntu-22.04-arm
target_os: linux
target_arch: arm64
electron_target: "--linux AppImage --arm64"
artifact_files: |
packages/desktop/release/*.AppImage
packages/desktop/release/latest*.yml
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.event.inputs.tag }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: |
package-lock.json
packages/desktop/package-lock.json
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Install web UI dependencies
run: |
npm ci --ignore-scripts
npm rebuild node-pty
- name: Build web UI
run: npm run build
- name: Keep production web UI dependencies only
run: npm prune --omit=dev --no-audit --no-fund
- name: Install desktop dependencies
run: npm ci --prefix packages/desktop --no-audit --no-fund
- name: Prepare bundled Python
env:
TARGET_OS: ${{ matrix.target_os }}
TARGET_ARCH: ${{ matrix.target_arch }}
run: npm --prefix packages/desktop run prepare:python
- name: Configure macOS signing
if: matrix.target_os == 'darwin'
shell: bash
env:
MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
MAC_APPLE_ID: ${{ secrets.APPLE_ID }}
MAC_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
MAC_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
write_env() {
local name="$1"
local value="$2"
if [ -n "$value" ]; then
{
echo "$name<<EOF"
echo "$value"
echo "EOF"
} >> "$GITHUB_ENV"
fi
}
if [ -z "${MAC_CSC_LINK:-}" ]; then
echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV"
echo "MAC_BUILD_EXTRA_ARGS=--config.mac.notarize=false" >> "$GITHUB_ENV"
echo "No macOS signing certificate configured; building unsigned and skipping notarization."
exit 0
fi
write_env "CSC_LINK" "$MAC_CSC_LINK"
write_env "CSC_KEY_PASSWORD" "$MAC_CSC_KEY_PASSWORD"
if [ -n "${MAC_APPLE_ID:-}" ] && [ -n "${MAC_APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${MAC_APPLE_TEAM_ID:-}" ]; then
write_env "APPLE_ID" "$MAC_APPLE_ID"
write_env "APPLE_APP_SPECIFIC_PASSWORD" "$MAC_APPLE_APP_SPECIFIC_PASSWORD"
write_env "APPLE_TEAM_ID" "$MAC_APPLE_TEAM_ID"
echo "macOS signing and notarization are configured."
else
echo "MAC_BUILD_EXTRA_ARGS=--config.mac.notarize=false" >> "$GITHUB_ENV"
echo "macOS signing certificate configured; Apple notarization credentials incomplete, skipping notarization."
fi
- name: Build desktop artifact
shell: bash
run: npm --prefix packages/desktop run dist -- ${{ matrix.electron_target }} ${MAC_BUILD_EXTRA_ARGS:-} --publish never
- name: Upload macOS update manifest artifact
if: matrix.target_os == 'darwin'
uses: actions/upload-artifact@v4
with:
name: latest-mac-${{ matrix.target_arch }}
path: packages/desktop/release/latest-mac.yml
if-no-files-found: error
retention-days: 1
- name: Upload artifacts to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.release.tag_name || github.event.inputs.tag }}
fail_on_unmatched_files: true
files: ${{ matrix.artifact_files }}
mac-update-manifest:
name: Merge macOS updater manifest
needs: desktop
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name || github.event.inputs.tag }}
- name: Download macOS update manifests
uses: actions/download-artifact@v4
with:
pattern: latest-mac-*
path: /tmp/hermes-mac-manifests
merge-multiple: false
- name: Merge macOS update manifests
shell: bash
run: |
node packages/desktop/scripts/merge-mac-latest-yml.mjs \
/tmp/hermes-mac-manifests/latest-mac-arm64/latest-mac.yml \
/tmp/hermes-mac-manifests/latest-mac-x64/latest-mac.yml \
> /tmp/latest-mac.yml
- name: Upload merged macOS updater manifest to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.release.tag_name || github.event.inputs.tag }}
fail_on_unmatched_files: true
files: /tmp/latest-mac.yml
-46
View File
@@ -1,46 +0,0 @@
name: Build and Push Docker Image to Docker Hub
on:
workflow_dispatch:
release:
types: [published]
permissions:
contents: read
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: false
jobs:
build-and-push:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/hermes-web-ui:latest
${{ secrets.DOCKERHUB_USERNAME }}/hermes-web-ui:${{ github.sha }}
${{ secrets.DOCKERHUB_USERNAME }}/hermes-web-ui:${{ github.event.release.tag_name || github.ref_name }}
-45
View File
@@ -1,45 +0,0 @@
name: NPM Lockfile Check
on:
push:
branches:
- main
paths:
- package.json
- package-lock.json
- .github/workflows/npm-lockfile-check.yml
pull_request:
branches:
- main
- base
paths:
- package.json
- package-lock.json
- .github/workflows/npm-lockfile-check.yml
permissions:
contents: read
concurrency:
group: npm-lockfile-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
check:
name: npm ci --ignore-scripts
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- name: Verify package-lock.json is in sync
run: npm ci --ignore-scripts
-52
View File
@@ -1,52 +0,0 @@
name: Playwright
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
concurrency:
group: playwright-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- name: Install dependencies
run: |
npm ci --ignore-scripts
npm rebuild node-pty
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
run: npm run test:e2e
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 7
-89
View File
@@ -1,89 +0,0 @@
name: Website
on:
pull_request:
branches:
- main
- base
paths:
- packages/website/**
- packages/client/src/styles/variables.scss
- package.json
- package-lock.json
- tsconfig.website.json
- vite.config.website.ts
- .github/workflows/website-deploy.yml
workflow_run:
workflows:
- Publish Desktop Artifacts to Release
types:
- completed
workflow_dispatch:
permissions:
contents: read
concurrency:
group: website-${{ github.event.pull_request.number || github.event.workflow_run.id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
name: Build website
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
if: github.event_name != 'workflow_run'
uses: actions/checkout@v4
- name: Checkout desktop release ref
if: github.event_name == 'workflow_run'
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_branch || github.event.workflow_run.head_sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Type-check website
run: npx vue-tsc -p tsconfig.website.json --noEmit
- name: Build website
run: npm run build:website
- name: Prepare SSH
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
env:
WEBSITE_SSH_KEY: ${{ secrets.WEBSITE_SSH_KEY }}
WEBSITE_SSH_KNOWN_HOSTS: ${{ secrets.WEBSITE_SSH_KNOWN_HOSTS }}
run: |
test -n "$WEBSITE_SSH_KEY"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$WEBSITE_SSH_KEY" > ~/.ssh/website_deploy_key
chmod 600 ~/.ssh/website_deploy_key
if [ -n "$WEBSITE_SSH_KNOWN_HOSTS" ]; then
printf '%s\n' "$WEBSITE_SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
fi
- name: Deploy website
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
env:
WEBSITE_SSH_USER: ${{ secrets.WEBSITE_SSH_USER }}
WEBSITE_SSH_PORT: ${{ secrets.WEBSITE_SSH_PORT }}
run: |
SSH_USER="${WEBSITE_SSH_USER:-root}"
SSH_PORT="${WEBSITE_SSH_PORT:-22}"
DEPLOY_DIR="/var/www/ekkolearnai.com/current"
SSH_CMD="ssh -i ~/.ssh/website_deploy_key -p ${SSH_PORT} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
$SSH_CMD "$SSH_USER@154.3.33.232" "mkdir -p '$DEPLOY_DIR' && find '$DEPLOY_DIR' -mindepth 1 -maxdepth 1 -exec rm -rf {} +"
tar -C dist/website -czf - . | $SSH_CMD "$SSH_USER@154.3.33.232" "tar -xzf - -C '$DEPLOY_DIR'"
+19
View File
@@ -0,0 +1,19 @@
=== 改造日志 ===
时间: 2026-06-02 03:44:23
源: https://github.com/EKKOLearnAI/hermes-web-ui
目标: http://192.168.6.101:3001/root/Hermes-ui
## 执行操作
- 替换 github.com → www.xinmi.cloud
- 替换 ekkolearnai.com → www.xinmi.cloud
- 替换 EKKOLearnAI → root
- 替换 GitHub/GitLab → 新觅源码库
- 删除 .github 目录
- 更新 package.json repository/homepage
- 更新侧边栏链接
- 保留 LICENSE 原文
- 跳过 AI 页面删除(本项目为 AI 应用)
## 保留内容
- LICENSE: 保留原始版权声明
- 所有 AI 对话功能: 本项目核心功能完整保留
+3 -2
View File
@@ -70,8 +70,9 @@ Frontend rules:
Desktop packaging is intentionally split:
- Pull requests run a Linux desktop smoke test in `.github/workflows/build.yml`.
- Published releases and manual dispatches run `.github/workflows/desktop-release.yml`.
- Pull requests run the web UI build and tests in `.github/workflows/build.yml`.
- Published releases and manual dispatches run desktop artifact packaging in `.github/workflows/desktop-release.yml`
and `.github/workflows/desktop-manual-build.yml`.
- Each release matrix target uploads only the artifact globs for its own platform.
Do not make a Windows job require macOS `.dmg` files or a Linux job require
+1 -1
View File
@@ -1,4 +1,4 @@
ARG BASE_IMAGE=nousresearch/hermes-agent:latest
ARG BASE_IMAGE=xinmi/hermes-agent:latest
FROM ${BASE_IMAGE}
ARG NODE_VERSION=24.15.0
+10 -10
View File
@@ -4,29 +4,29 @@
</p>
<p align="center">
A full-featured desktop app and web dashboard for <a href="https://github.com/NousResearch/hermes-agent">Hermes Agent</a>.<br/>
A full-featured desktop app and web dashboard for <a href="https://www.xinmi.cloud/NousResearch/hermes-agent">Hermes Agent</a>.<br/>
Manage AI chat sessions, monitor usage & costs, configure platform channels,<br/>
schedule cron jobs, browse skills — all from a clean, responsive web interface.
</p>
<p align="center">
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest">Download Hermes Studio Desktop</a>
<a href="https://www.xinmi.cloud/root/Hermes-ui/releases/latest">Download Hermes Studio Desktop</a>
·
<code>npm install -g hermes-web-ui && hermes-web-ui start</code>
</p>
<p align="center">
<img src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/image1.png" alt="Hermes Web UI Demo" width="680"/>
<img src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/image1.png" alt="Hermes Web UI Demo" width="680"/>
</p>
<p align="center">
<img src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/image2.png" alt="Hermes Web UI Demo" width="680"/>
<img src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/image2.png" alt="Hermes Web UI Demo" width="680"/>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/hermes-web-ui"><img src="https://img.shields.io/npm/v/hermes-web-ui?style=flat-square&color=blue" alt="npm version"/></a>
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hermes-web-ui?style=flat-square" alt="license"/></a>
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="stars"/></a>
<a href="https://www.xinmi.cloud/root/Hermes-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hermes-web-ui?style=flat-square" alt="license"/></a>
<a href="https://www.xinmi.cloud/root/Hermes-ui/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="stars"/></a>
</p>
---
@@ -176,7 +176,7 @@ hermes-web-ui reset-default-login
### Desktop App (Recommended)
Download the latest **Hermes Studio** desktop installer from
[GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest).
[新觅源码库 Releases](https://www.xinmi.cloud/root/Hermes-ui/releases/latest).
Desktop builds are published for macOS, Windows, and Linux, with separate
architecture assets where applicable. The desktop app bundles the Web UI
@@ -268,13 +268,13 @@ These variables configure Hermes Web UI, its local Hermes runtime integration, a
| `HERMES_BRIDGE_TOOLSETS` | profile/default | Toolset override for bridge runs. |
| `HERMES_BRIDGE_MAX_TURNS` | profile/default | Maximum turn override for bridge runs. |
| `HERMES_BRIDGE_SUPPRESS_PLATFORM_HINT` | `cli` | Controls bridge platform hint suppression passed to Hermes Agent. |
| `HERMES_OPENROUTER_APP_REFERER` | `https://ekkolearnai.com` | OpenRouter attribution referer sent by bridge runs. |
| `HERMES_OPENROUTER_APP_REFERER` | `https://www.xinmi.cloud` | OpenRouter attribution referer sent by bridge runs. |
| `HERMES_OPENROUTER_APP_TITLE` | `Hermes Web UI` | OpenRouter attribution title sent by bridge runs. |
| `HERMES_OPENROUTER_APP_CATEGORIES` | `cli-agent,personal-agent` | OpenRouter attribution categories sent by bridge runs. |
| `HERMES_WEB_UI_MANAGED_GATEWAY` | platform/runtime dependent | Force managed legacy gateway process handling. Set `1`, `true`, `yes`, or `on` to enable. |
| `HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN` | enabled in production | Controls whether Web UI shutdown also stops managed gateway processes. Set `0` or `false` to detach them. |
| `GATEWAY_HOST` | `127.0.0.1` | Default gateway host written into profile config for legacy gateway compatibility. |
| `HERMES_WEB_UI_PREVIEW_REPO` | package repository | GitHub repository used by Version Preview. |
| `HERMES_WEB_UI_PREVIEW_REPO` | package repository | 新觅源码库 repository used by Version Preview. |
| `HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_TRANSPORT` | platform default | Version Preview broker transport. Set `tcp` to use loopback TCP for Preview on macOS/Linux; when unset, Preview follows `HERMES_AGENT_BRIDGE_WORKER_TRANSPORT=tcp`. |
| `HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_ENDPOINT` | isolated preview endpoint | Directly overrides the Version Preview broker endpoint. |
| `HERMES_WEB_UI_BACKEND_PORT` | `8648` | Backend port used by the Vite dev proxy. |
@@ -309,7 +309,7 @@ On startup the BFF server automatically:
## Development
```bash
git clone https://github.com/EKKOLearnAI/hermes-web-ui.git
git clone https://www.xinmi.cloud/root/Hermes-ui.git
cd hermes-web-ui
npm install
npm run dev
+11 -11
View File
@@ -4,23 +4,23 @@
</p>
<p align="center">
<a href="https://github.com/NousResearch/hermes-agent">Hermes Agent</a> 的全功能桌面应用和 Web 管理面板。<br/>
<a href="https://www.xinmi.cloud/NousResearch/hermes-agent">Hermes Agent</a> 的全功能桌面应用和 Web 管理面板。<br/>
管理 AI 聊天会话、监控用量与成本、配置平台渠道、<br/>
管理定时任务、浏览技能 —— 全部在一个简洁响应式的 Web 界面中完成。
</p>
<p align="center">
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest">下载 Hermes Studio 桌面版</a>
<a href="https://www.xinmi.cloud/root/Hermes-ui/releases/latest">下载 Hermes Studio 桌面版</a>
·
<code>npm install -g hermes-web-ui && hermes-web-ui start</code>
</p>
<p align="center">
<img src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/image1.png" alt="Hermes Web UI 演示" width="680"/>
<img src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/image1.png" alt="Hermes Web UI 演示" width="680"/>
</p>
<p align="center">
<img src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/image2.png" alt="Hermes Web UI 演示" width="680"/>
<img src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/image2.png" alt="Hermes Web UI 演示" width="680"/>
</p>
<p align="center">
@@ -28,13 +28,13 @@
</p>
<p align="center">
<video src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/video.mp4?raw=true" width="360" controls></video>
<video src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/video.mp4?raw=true" width="360" controls></video>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/hermes-web-ui"><img src="https://img.shields.io/npm/v/hermes-web-ui?style=flat-square&color=blue" alt="npm 版本"/></a>
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hermes-web-ui?style=flat-square" alt="许可证"/></a>
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="Star"/></a>
<a href="https://www.xinmi.cloud/root/Hermes-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hermes-web-ui?style=flat-square" alt="许可证"/></a>
<a href="https://www.xinmi.cloud/root/Hermes-ui/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="Star"/></a>
</p>
---
@@ -183,7 +183,7 @@ hermes-web-ui reset-default-login
### 桌面应用(推荐)
从 [GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest)
从 [新觅源码库 Releases](https://www.xinmi.cloud/root/Hermes-ui/releases/latest)
下载最新的 **Hermes Studio** 桌面安装包。
桌面版会发布 macOS、Windows 和 Linux 构建;适用时会区分不同 CPU 架构。
@@ -274,13 +274,13 @@ Web UI 启动后端聊天能力时,会优先使用包含 `run_agent.py` 的源
| `HERMES_BRIDGE_TOOLSETS` | profile/默认值 | bridge 运行时的 toolset 覆盖。 |
| `HERMES_BRIDGE_MAX_TURNS` | profile/默认值 | bridge 运行时的最大轮数覆盖。 |
| `HERMES_BRIDGE_SUPPRESS_PLATFORM_HINT` | `cli` | 控制传给 Hermes Agent 的 bridge platform hint suppression。 |
| `HERMES_OPENROUTER_APP_REFERER` | `https://ekkolearnai.com` | bridge 运行发送给 OpenRouter 的 attribution referer。 |
| `HERMES_OPENROUTER_APP_REFERER` | `https://www.xinmi.cloud` | bridge 运行发送给 OpenRouter 的 attribution referer。 |
| `HERMES_OPENROUTER_APP_TITLE` | `Hermes Web UI` | bridge 运行发送给 OpenRouter 的 attribution title。 |
| `HERMES_OPENROUTER_APP_CATEGORIES` | `cli-agent,personal-agent` | bridge 运行发送给 OpenRouter 的 attribution categories。 |
| `HERMES_WEB_UI_MANAGED_GATEWAY` | 由平台/运行环境决定 | 强制启用旧 gateway 进程托管;设为 `1``true``yes``on` 开启。 |
| `HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN` | 生产环境默认开启 | Web UI 关闭时是否同时停止托管的 gateway 进程;设为 `0``false` 可让 gateway 分离运行。 |
| `GATEWAY_HOST` | `127.0.0.1` | 旧 gateway 兼容配置中写入 profile 的默认 gateway host。 |
| `HERMES_WEB_UI_PREVIEW_REPO` | package repository | Version Preview 使用的 GitHub 仓库。 |
| `HERMES_WEB_UI_PREVIEW_REPO` | package repository | Version Preview 使用的 新觅源码库 仓库。 |
| `HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_TRANSPORT` | 平台默认值 | Version Preview broker transport。设为 `tcp` 可让预览环境在 macOS/Linux 上也使用 loopback TCP;未设置时会跟随 `HERMES_AGENT_BRIDGE_WORKER_TRANSPORT=tcp`。 |
| `HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_ENDPOINT` | 隔离的预览 endpoint | 直接覆盖 Version Preview 的 broker endpoint。 |
| `HERMES_WEB_UI_BACKEND_PORT` | `8648` | Vite dev proxy 使用的后端端口。 |
@@ -315,7 +315,7 @@ Web UI 启动后端聊天能力时,会优先使用包含 `run_agent.py` 的源
## 开发
```bash
git clone https://github.com/EKKOLearnAI/hermes-web-ui.git
git clone https://www.xinmi.cloud/root/Hermes-ui.git
cd hermes-web-ui
npm install
npm run dev
+3 -3
View File
@@ -1,10 +1,10 @@
services:
hermes-webui:
xinmi-hermes-ui:
build:
context: .
dockerfile: Dockerfile
image: ${WEBUI_IMAGE:-hermes-web-ui-local:latest}
container_name: ${WEBUI_CONTAINER_NAME:-hermes-webui}
image: ${WEBUI_IMAGE:-xinmi-hermes-ui:latest}
container_name: ${WEBUI_CONTAINER_NAME:-xinmi-hermes-ui}
ports:
- "${PORT:-6060}:${PORT:-6060}"
- "${PREVIEW_FRONTEND_PORT:-8651}:8651"
+5 -5
View File
@@ -28,17 +28,17 @@ npm run build
| Auth, profile, or credential behavior | focused server tests plus relevant e2e auth tests |
| Chat, Socket.IO, group chat | focused server tests plus relevant e2e chat tests |
| Desktop packaging | `npm run harness:check`, `npm run build`, and a platform-specific desktop build when practical |
| GitHub workflow | `npm run harness:check` and `actionlint` when available |
| 新觅源码库 workflow | `npm run harness:check` and `actionlint` when available |
| Package manifests | `npm ci --ignore-scripts` and lockfile workflow expectations |
## CI Mapping
- Build workflow: installs dependencies, runs coverage, builds production assets,
then runs a Linux desktop smoke test on pull requests.
- Build workflow: installs dependencies, runs coverage, and builds production
assets on pushes and pull requests.
- Playwright workflow: runs browser e2e tests.
- NPM lockfile workflow: verifies `package-lock.json` is synchronized.
- Desktop release workflow: builds and uploads platform-specific desktop artifacts
for release tags.
- Desktop release and manual desktop build workflows build and upload
platform-specific desktop artifacts.
- Docker workflow: builds and publishes release images.
## Release Workflow Guardrail
+102 -102
View File
@@ -116,7 +116,7 @@
"tinyexec": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
"url": "https://www.xinmi.cloud/sponsors/antfu"
}
},
"node_modules/@asamuzakjp/css-color": {
@@ -280,7 +280,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
"url": "https://www.xinmi.cloud/sponsors/csstools"
},
{
"type": "opencollective",
@@ -300,7 +300,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
"url": "https://www.xinmi.cloud/sponsors/csstools"
},
{
"type": "opencollective",
@@ -324,7 +324,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
"url": "https://www.xinmi.cloud/sponsors/csstools"
},
{
"type": "opencollective",
@@ -352,7 +352,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
"url": "https://www.xinmi.cloud/sponsors/csstools"
},
{
"type": "opencollective",
@@ -375,7 +375,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
"url": "https://www.xinmi.cloud/sponsors/csstools"
},
{
"type": "opencollective",
@@ -400,7 +400,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
"url": "https://www.xinmi.cloud/sponsors/csstools"
},
{
"type": "opencollective",
@@ -1299,7 +1299,7 @@
"node": ">= 22"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
"url": "https://www.xinmi.cloud/sponsors/kazupon"
}
},
"node_modules/@intlify/devtools-types": {
@@ -1316,7 +1316,7 @@
"node": ">= 22"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
"url": "https://www.xinmi.cloud/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
@@ -1333,7 +1333,7 @@
"node": ">= 22"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
"url": "https://www.xinmi.cloud/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
@@ -1346,7 +1346,7 @@
"node": ">= 22"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
"url": "https://www.xinmi.cloud/sponsors/kazupon"
}
},
"node_modules/@isaacs/cliui": {
@@ -1507,7 +1507,7 @@
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
"url": "https://www.xinmi.cloud/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
@@ -1528,7 +1528,7 @@
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
"url": "https://www.xinmi.cloud/sponsors/Boshen"
}
},
"node_modules/@parcel/watcher": {
@@ -1852,7 +1852,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
}
},
"node_modules/@pinia/testing": {
@@ -1862,7 +1862,7 @@
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/posva"
"url": "https://www.xinmi.cloud/sponsors/posva"
},
"peerDependencies": {
"pinia": ">=3.0.4"
@@ -3474,7 +3474,7 @@
"integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
"https://www.xinmi.cloud/sponsors/katex"
],
"license": "MIT",
"dependencies": {
@@ -3600,7 +3600,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
}
},
"node_modules/@vue/reactivity": {
@@ -3801,7 +3801,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
@@ -3816,7 +3816,7 @@
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
@@ -3937,7 +3937,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
"url": "https://www.xinmi.cloud/sponsors/feross"
},
{
"type": "patreon",
@@ -3979,7 +3979,7 @@
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/birpc": {
@@ -3989,7 +3989,7 @@
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
"url": "https://www.xinmi.cloud/sponsors/antfu"
}
},
"node_modules/body-parser": {
@@ -4139,7 +4139,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/camelcase": {
@@ -4183,7 +4183,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/supports-color": {
@@ -4303,7 +4303,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/co": {
@@ -4411,7 +4411,7 @@
"node": ">=18"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
"url": "https://www.xinmi.cloud/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/config-chain": {
@@ -4491,7 +4491,7 @@
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
"url": "https://www.xinmi.cloud/sponsors/mesqueeb"
}
},
"node_modules/cors": {
@@ -5199,7 +5199,7 @@
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
"url": "https://www.xinmi.cloud/sponsors/kossnocorp"
}
},
"node_modules/date-fns-tz": {
@@ -5545,7 +5545,7 @@
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
"url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
@@ -5904,7 +5904,7 @@
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
"url": "https://www.xinmi.cloud/sponsors/RubenVerborgh"
}
],
"license": "MIT",
@@ -5931,7 +5931,7 @@
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/form-data": {
@@ -6015,7 +6015,7 @@
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/generator-function": {
@@ -6059,7 +6059,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/get-proto": {
@@ -6095,7 +6095,7 @@
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
@@ -6121,7 +6121,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
@@ -6180,7 +6180,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
@@ -6196,7 +6196,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/hasown": {
@@ -6401,7 +6401,7 @@
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
"url": "https://www.xinmi.cloud/sponsors/wooorm"
}
},
"node_modules/inflation": {
@@ -6486,7 +6486,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
@@ -6525,7 +6525,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/is-glob": {
@@ -6574,7 +6574,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/is-what": {
@@ -6587,7 +6587,7 @@
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
"url": "https://www.xinmi.cloud/sponsors/mesqueeb"
}
},
"node_modules/isexe": {
@@ -6674,7 +6674,7 @@
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
@@ -6834,7 +6834,7 @@
"integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
"https://www.xinmi.cloud/sponsors/katex"
],
"license": "MIT",
"dependencies": {
@@ -7317,11 +7317,11 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
"url": "https://www.xinmi.cloud/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
"url": "https://www.xinmi.cloud/sponsors/markdown-it"
}
],
"license": "MIT",
@@ -7415,7 +7415,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/make-error": {
@@ -7433,11 +7433,11 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
"url": "https://www.xinmi.cloud/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
"url": "https://www.xinmi.cloud/sponsors/markdown-it"
}
],
"license": "MIT",
@@ -7463,7 +7463,7 @@
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
"url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
}
},
"node_modules/marked": {
@@ -7526,7 +7526,7 @@
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
"url": "https://www.xinmi.cloud/sponsors/mesqueeb"
}
},
"node_modules/merge-anything/node_modules/is-what": {
@@ -7539,7 +7539,7 @@
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
"url": "https://www.xinmi.cloud/sponsors/mesqueeb"
}
},
"node_modules/merge-descriptors": {
@@ -7549,7 +7549,7 @@
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/mermaid": {
@@ -7599,7 +7599,7 @@
"dev": true,
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
"https://www.xinmi.cloud/sponsors/katex"
],
"license": "MIT",
"dependencies": {
@@ -7666,7 +7666,7 @@
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/minimist": {
@@ -7676,7 +7676,7 @@
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/minipass": {
@@ -7797,7 +7797,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
"url": "https://www.xinmi.cloud/sponsors/ai"
}
],
"license": "MIT",
@@ -7951,7 +7951,7 @@
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/nodemon/node_modules/supports-color": {
@@ -8012,7 +8012,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/on-exit-leak-free": {
@@ -8067,7 +8067,7 @@
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
@@ -8117,7 +8117,7 @@
"entities": "^8.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
"url": "https://www.xinmi.cloud/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
@@ -8130,7 +8130,7 @@
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
"url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
}
},
"node_modules/parseurl": {
@@ -8208,7 +8208,7 @@
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
@@ -8270,7 +8270,7 @@
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
@@ -8283,7 +8283,7 @@
"@vue/devtools-api": "^7.7.7"
},
"funding": {
"url": "https://github.com/sponsors/posva"
"url": "https://www.xinmi.cloud/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.5.0",
@@ -8451,7 +8451,7 @@
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
"url": "https://www.xinmi.cloud/sponsors/ai"
}
],
"license": "MIT",
@@ -8472,7 +8472,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
"url": "https://www.xinmi.cloud/sponsors/fastify"
},
{
"type": "opencollective",
@@ -8697,7 +8697,7 @@
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/quick-format-unescaped": {
@@ -8821,7 +8821,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/resolve-path": {
@@ -8939,7 +8939,7 @@
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
@@ -9086,7 +9086,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
"url": "https://www.xinmi.cloud/sponsors/feross"
},
{
"type": "patreon",
@@ -9114,7 +9114,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/safe-stable-stringify": {
@@ -9206,7 +9206,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
"url": "https://www.xinmi.cloud/sponsors/fastify"
},
{
"type": "opencollective",
@@ -9360,7 +9360,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/side-channel": {
@@ -9380,7 +9380,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
@@ -9397,7 +9397,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
@@ -9416,7 +9416,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
@@ -9436,7 +9436,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/siginfo": {
@@ -9456,7 +9456,7 @@
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/simple-update-notifier": {
@@ -9649,7 +9649,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
@@ -9711,7 +9711,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
@@ -9758,7 +9758,7 @@
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/strip-literal": {
@@ -9771,7 +9771,7 @@
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
"url": "https://www.xinmi.cloud/sponsors/antfu"
}
},
"node_modules/strip-literal/node_modules/js-tokens": {
@@ -9814,7 +9814,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/supports-color?sponsor=1"
}
},
"node_modules/supports-preserve-symlinks-flag": {
@@ -9827,7 +9827,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/symbol-tree": {
@@ -9888,7 +9888,7 @@
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/thread-stream": {
@@ -9942,7 +9942,7 @@
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
"url": "https://www.xinmi.cloud/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
@@ -9973,7 +9973,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
}
},
"node_modules/tinypool": {
@@ -10357,8 +10357,8 @@
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
"https://www.xinmi.cloud/sponsors/broofa",
"https://www.xinmi.cloud/sponsors/ctavan"
],
"license": "MIT",
"bin": {
@@ -10424,7 +10424,7 @@
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
"url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
@@ -10533,7 +10533,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
}
},
"node_modules/vite-node/node_modules/vite": {
@@ -10557,7 +10557,7 @@
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
"url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
@@ -10621,7 +10621,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
}
},
"node_modules/vitest": {
@@ -10762,7 +10762,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
}
},
"node_modules/vitest/node_modules/tinyexec": {
@@ -10793,7 +10793,7 @@
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
"url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
@@ -10912,7 +10912,7 @@
"node": ">= 22"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
"url": "https://www.xinmi.cloud/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
@@ -10935,7 +10935,7 @@
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
"url": "https://www.xinmi.cloud/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.5.0"
@@ -11103,7 +11103,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
@@ -11122,7 +11122,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
@@ -11180,7 +11180,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrappy": {
@@ -11268,7 +11268,7 @@
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
"url": "https://www.xinmi.cloud/sponsors/eemeli"
}
},
"node_modules/yargs": {
+9 -8
View File
@@ -1,12 +1,12 @@
{
"name": "hermes-web-ui",
"name": "xinmi-hermes-ui",
"version": "0.6.8",
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
"repository": {
"type": "git",
"url": "https://github.com/EKKOLearnAI/hermes-web-ui.git"
"url": "https://www.xinmi.cloud/root/Hermes-ui.git"
},
"homepage": "https://ekkolearnai.com",
"homepage": "https://www.xinmi.cloud",
"license": "BSL-1.1",
"engines": {
"node": ">=23.0.0"
@@ -30,7 +30,7 @@
"typescript"
],
"bin": {
"hermes-web-ui": "./bin/hermes-web-ui.mjs"
"xinmi-hermes-ui": "./bin/hermes-web-ui.mjs"
},
"scripts": {
"start": "vite --host --port 8648",
@@ -50,11 +50,12 @@
"build:website": "vite build --config vite.config.website.ts",
"preview:website": "vite preview --config vite.config.website.ts",
"desktop:install": "npm ci --prefix packages/desktop --no-audit --no-fund",
"desktop:prepare-runtime": "npm --prefix packages/desktop run prepare:runtime",
"desktop:prepare-python": "npm --prefix packages/desktop run prepare:python",
"build:desktop": "npm run build && npm run desktop:install && npm run desktop:prepare-python && npm --prefix packages/desktop run dist -- --publish never",
"build:desktop:mac": "npm run build && npm run desktop:install && npm run desktop:prepare-python && npm --prefix packages/desktop run dist -- --mac --publish never",
"build:desktop:win": "npm run build && npm run desktop:install && npm run desktop:prepare-python && npm --prefix packages/desktop run dist -- --win --publish never",
"build:desktop:linux": "npm run build && npm run desktop:install && npm run desktop:prepare-python && npm --prefix packages/desktop run dist -- --linux --publish never",
"build:desktop": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --publish never",
"build:desktop:mac": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --mac --publish never",
"build:desktop:win": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --win --publish never",
"build:desktop:linux": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --linux --publish never",
"openapi:generate": "node scripts/generate-openapi.mjs",
"claude": "claude --dangerously-skip-permissions"
},
@@ -4,7 +4,7 @@ This page collects useful community skill repositories that can extend Hermes, C
Community skills are third-party code and instructions. Review them before installing, especially when a skill can read API keys, cookies, browser sessions, local files, repositories, shell scripts, package managers, or social media accounts.
Useful skill recommendations are welcome. If you find a high-quality skill that should be listed here, please submit a pull request on GitHub with the repository link, usage scenario, and any security notes.
Useful skill recommendations are welcome. If you find a high-quality skill that should be listed here, please submit a pull request on 新觅源码库 with the repository link, usage scenario, and any security notes.
## Maintenance Guidelines
@@ -28,7 +28,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Anthropic Official Skills
- Repository: [anthropics/skills](https://github.com/anthropics/skills/tree/main/skills)
- Repository: [anthropics/skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills)
- Focus: official reference skills for Claude-style agents.
- Good for: learning the expected skill structure, adapting stable examples, and bootstrapping common workflows.
- Representative skills: `docx`, `pdf`, `pptx`, `xlsx`, `frontend-design`, `webapp-testing`, `skill-creator`, `mcp-builder`, `theme-factory`, `web-artifacts-builder`.
@@ -36,7 +36,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Matt Pocock Skills
- Repository: [mattpocock/skills](https://github.com/mattpocock/skills)
- Repository: [mattpocock/skills](https://www.xinmi.cloud/mattpocock/skills)
- Focus: engineering and productivity skills from a real development workflow.
- Good for: TypeScript engineering, test-driven work, triage, diagnosis, reviews, prototyping, and product handoff workflows.
- Representative skills: `tdd`, `triage`, `diagnose`, `prototype`, `review`, `to-prd`, `to-issues`, `handoff`, `write-a-skill`.
@@ -46,56 +46,56 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Frontend Slides
- Repository: [zarazhangrui/frontend-slides](https://github.com/zarazhangrui/frontend-slides)
- Repository: [zarazhangrui/frontend-slides](https://www.xinmi.cloud/zarazhangrui/frontend-slides)
- Focus: creating web-native slide decks with frontend techniques.
- Good for: HTML/CSS slide decks, visual storytelling, and browser-rendered presentations.
- Notes: useful when a deck should be designed as a rich web artifact rather than a traditional office file.
### Huashu Design
- Repository: [alchaincyf/huashu-design](https://github.com/alchaincyf/huashu-design)
- Repository: [alchaincyf/huashu-design](https://www.xinmi.cloud/alchaincyf/huashu-design)
- Focus: HTML-native design work for Claude Code and agent workflows.
- Good for: high-fidelity prototypes, slides, animation concepts, visual review, and export-oriented design flows.
- Notes: includes design philosophy, review heuristics, and presentation-oriented workflows.
### Guizang PPT Skill
- Repository: [op7418/guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill)
- Repository: [op7418/guizang-ppt-skill](https://www.xinmi.cloud/op7418/guizang-ppt-skill)
- Focus: polished HTML slide decks with editorial, magazine, and Swiss-style layouts.
- Good for: presentation decks, social covers, image prompts, and visual narrative work.
- Notes: includes a presentation runtime and style-oriented slide generation patterns.
### HTML PPT Skill
- Repository: [lewislulu/html-ppt-skill](https://github.com/lewislulu/html-ppt-skill)
- Repository: [lewislulu/html-ppt-skill](https://www.xinmi.cloud/lewislulu/html-ppt-skill)
- Focus: HTML PPT Studio for professional HTML presentations.
- Good for: themed slide decks, layout-rich presentations, and animated browser presentations.
- Representative capabilities: multiple themes, layout templates, animation patterns, and HTML presentation scaffolding.
### PPT Image First
- Repository: [NyxTides/ppt-image-first](https://github.com/NyxTides/ppt-image-first)
- Repository: [NyxTides/ppt-image-first](https://www.xinmi.cloud/NyxTides/ppt-image-first)
- Focus: image-first presentation generation.
- Good for: decks where the visual direction should lead the content structure.
- Notes: designed for Codex, Claude Code, and OpenCode-style CLI agents.
### GPT Image To PPT
- Repository: [JuneYaooo/gpt-image2-ppt-skills](https://github.com/JuneYaooo/gpt-image2-ppt-skills)
- Repository: [JuneYaooo/gpt-image2-ppt-skills](https://www.xinmi.cloud/JuneYaooo/gpt-image2-ppt-skills)
- Focus: cloning or adapting PowerPoint visual layouts using image generation.
- Good for: recreating a deck style from an existing `.pptx` template while replacing the actual content.
- Notes: useful for template-driven presentations, but review external image generation/API behavior before use.
### Fireworks Tech Graph
- Repository: [yizhiyanhua-ai/fireworks-tech-graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph)
- Repository: [yizhiyanhua-ai/fireworks-tech-graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph)
- Focus: technical diagram generation.
- Good for: architecture diagrams, workflow charts, UML-style visuals, AI agent workflow diagrams, and production-ready SVG/PNG outputs.
- Notes: a practical choice when you need diagrams rather than full slide decks.
### Diagram Skill
- Repository: [312362115/claude diagram skill](https://github.com/312362115/claude/blob/main/skills/diagram/SKILL.md)
- Repository: [312362115/claude diagram skill](https://www.xinmi.cloud/312362115/claude/blob/main/skills/diagram/SKILL.md)
- Focus: diagram generation inside a broader Claude skill collection.
- Good for: generating structured diagrams, templates, and visual explanations.
- Notes: this is a direct skill file link, so review the surrounding `references`, `scripts`, and `templates` folders before installing.
@@ -104,7 +104,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Huashu Markdown To HTML
- Repository: [alchaincyf/huashu-md-html](https://github.com/alchaincyf/huashu-md-html)
- Repository: [alchaincyf/huashu-md-html](https://www.xinmi.cloud/alchaincyf/huashu-md-html)
- Focus: Markdown and HTML conversion pipelines.
- Good for: converting files or URLs to Markdown, turning Markdown into polished HTML, and converting HTML back to Markdown.
- Representative tools: MarkItDown, Pandoc, html-to-markdown, and trafilatura-based workflows.
@@ -112,14 +112,14 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Chinese Web Novel Skill
- Repository: [Tomsawyerhu/Chinese-WebNovel-Skill](https://github.com/Tomsawyerhu/Chinese-WebNovel-Skill)
- Repository: [Tomsawyerhu/Chinese-WebNovel-Skill](https://www.xinmi.cloud/Tomsawyerhu/Chinese-WebNovel-Skill)
- Focus: Chinese web novel writing workflows.
- Good for: long-form fiction planning, chapter writing, style continuity, and web-novel oriented drafting.
- Representative skill: `webnovel-writing`.
### Software Copyright Skill
- Repository: [Fokkyp/SoftwareCopyright-Skill](https://github.com/Fokkyp/SoftwareCopyright-Skill)
- Repository: [Fokkyp/SoftwareCopyright-Skill](https://www.xinmi.cloud/Fokkyp/SoftwareCopyright-Skill)
- Focus: preparing Chinese software copyright application materials.
- Good for: generating `.docx` application documents from a local software project.
- Representative skills: `software-copyright-materials`, `docx-toolkit`.
@@ -127,7 +127,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Patent Disclosure Skill
- Repository: [handsomestWei/patent-disclosure-skill](https://github.com/handsomestWei/patent-disclosure-skill)
- Repository: [handsomestWei/patent-disclosure-skill](https://www.xinmi.cloud/handsomestWei/patent-disclosure-skill)
- Focus: patent disclosure drafting.
- Good for: extracting patentable points from project documents, novelty checks, desensitized drafting, and self-review loops.
- Notes: may involve web research and sensitive technical documents. Review data handling carefully.
@@ -136,7 +136,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Baoyu Skills
- Repository: [JimLiu/baoyu-skills](https://github.com/JimLiu/baoyu-skills)
- Repository: [JimLiu/baoyu-skills](https://www.xinmi.cloud/JimLiu/baoyu-skills)
- Focus: image generation, content transformation, publishing, and media workflows.
- Good for: image cards, article illustrations, slide decks, URL-to-Markdown conversion, YouTube transcripts, Markdown-to-HTML, and social posting workflows.
- Representative skills: `baoyu-image-gen`, `baoyu-imagine`, `baoyu-slide-deck`, `baoyu-markdown-to-html`, `baoyu-post-to-x`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-url-to-markdown`, `baoyu-youtube-transcript`, `baoyu-translate`, `baoyu-diagram`, `baoyu-comic`.
@@ -144,7 +144,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Virtual Couple Travel Vlog
- Repository: [vibeshotclub/vsc-skills / virtual-couple-travel-vlog](https://github.com/vibeshotclub/vsc-skills/tree/main/virtual-couple-travel-vlog)
- Repository: [vibeshotclub/vsc-skills / virtual-couple-travel-vlog](https://www.xinmi.cloud/vibeshotclub/vsc-skills/tree/main/virtual-couple-travel-vlog)
- Focus: travel-vlog style media generation.
- Good for: short-form visual storytelling, character-based travel content, and repeatable media production prompts.
- Notes: this is a subdirectory skill inside a larger skill collection.
@@ -153,14 +153,14 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Web Access
- Repository: [eze-is/web-access](https://github.com/eze-is/web-access)
- Repository: [eze-is/web-access](https://www.xinmi.cloud/eze-is/web-access)
- Focus: giving an agent structured web access through layered routing and browser/CDP workflows.
- Good for: web research, browser-assisted tasks, parallel information gathering, and pages that require interaction.
- Security note: browser access can expose logged-in sessions and local browser state. Audit before enabling.
### OpenCLI
- Repository: [jackwener/opencli](https://github.com/jackwener/opencli)
- Repository: [jackwener/opencli](https://www.xinmi.cloud/jackwener/opencli)
- Focus: converting websites, browser sessions, Electron apps, and local tools into CLI-accessible automation surfaces for humans and AI agents.
- Good for: letting agents operate logged-in Chrome pages, building reusable website adapters, wrapping local binaries, and turning browser workflows into deterministic commands.
- Representative skills: `opencli-browser`, `opencli-adapter-author`, `opencli-autofix`, `opencli-usage`.
@@ -168,7 +168,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Follow Builders
- Repository: [zarazhangrui/follow-builders](https://github.com/zarazhangrui/follow-builders)
- Repository: [zarazhangrui/follow-builders](https://www.xinmi.cloud/zarazhangrui/follow-builders)
- Focus: monitoring AI builders across X, blogs, and YouTube podcasts.
- Good for: tracking builders rather than influencers, summarizing feeds, and creating digest-style updates.
- Representative data/config files: X feeds, blog feeds, podcast feeds, prompts, and state files.
@@ -176,7 +176,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### SlowMist Agent Security
- Repository: [slowmist/slowmist-agent-security](https://github.com/slowmist/slowmist-agent-security)
- Repository: [slowmist/slowmist-agent-security](https://www.xinmi.cloud/slowmist/slowmist-agent-security)
- Focus: security review for AI agents operating with untrusted inputs.
- Good for: checking skills, MCP servers, repositories, URLs, prompts, and crypto/on-chain addresses for security risks.
- Core idea: external input should be considered untrusted until verified.
@@ -186,7 +186,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Huashu Nuwa Skill
- Repository: [alchaincyf/nuwa-skill](https://github.com/alchaincyf/nuwa-skill)
- Repository: [alchaincyf/nuwa-skill](https://www.xinmi.cloud/alchaincyf/nuwa-skill)
- Focus: distilling a person or viewpoint into a reusable agent skill.
- Good for: advisory-board style thinking, mental models, decision heuristics, and writing in a specific perspective.
- Representative perspectives: Huashu Nuwa, Feynman, Steve Jobs, Elon Musk, Naval Ravikant, Paul Graham, Nassim Taleb.
@@ -194,7 +194,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### PUA / Anti-PUA Skills
- Repository: [tanweai/pua](https://github.com/tanweai/pua)
- Repository: [tanweai/pua](https://www.xinmi.cloud/tanweai/pua)
- Focus: high-agency, confrontational, coaching, or anti-PUA style agent behavior.
- Good for: motivation, critique, resistance to manipulation, and intentionally sharp agent feedback.
- Representative skills: `pua`, `pua-en`, `pua-ja`, `pua-loop`, `mama`, `p7`, `p9`, `p10`, `pro`, `shot`, `yes`.
@@ -202,7 +202,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Ex Skill
- Repository: [therealXiaomanChu/ex-skill](https://github.com/therealXiaomanChu/ex-skill)
- Repository: [therealXiaomanChu/ex-skill](https://www.xinmi.cloud/therealXiaomanChu/ex-skill)
- Focus: distilling an ex-partner/persona into an AI skill that speaks in that style.
- Good for: persona experiments, emotional roleplay, and style simulation.
- Representative skill: `create-ex`.
@@ -212,17 +212,17 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
If you only want a practical starter set:
- [Anthropic Official Skills](https://github.com/anthropics/skills/tree/main/skills) for reference implementations.
- [Matt Pocock Skills](https://github.com/mattpocock/skills) for engineering workflows.
- [Baoyu Skills](https://github.com/JimLiu/baoyu-skills) for image, media, and publishing workflows.
- [Huashu Design](https://github.com/alchaincyf/huashu-design) for high-fidelity HTML-native design.
- [Guizang PPT Skill](https://github.com/op7418/guizang-ppt-skill) or [HTML PPT Skill](https://github.com/lewislulu/html-ppt-skill) for browser-based presentations.
- [Huashu Markdown To HTML](https://github.com/alchaincyf/huashu-md-html) for Markdown/HTML document conversion.
- [Web Access](https://github.com/eze-is/web-access) for web research workflows.
- [OpenCLI](https://github.com/jackwener/opencli) for logged-in browser automation and reusable website CLI adapters.
- [Fireworks Tech Graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph) for technical diagrams.
- [SlowMist Agent Security](https://github.com/slowmist/slowmist-agent-security) for reviewing risky community skills.
- [Anthropic Official Skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills) for reference implementations.
- [Matt Pocock Skills](https://www.xinmi.cloud/mattpocock/skills) for engineering workflows.
- [Baoyu Skills](https://www.xinmi.cloud/JimLiu/baoyu-skills) for image, media, and publishing workflows.
- [Huashu Design](https://www.xinmi.cloud/alchaincyf/huashu-design) for high-fidelity HTML-native design.
- [Guizang PPT Skill](https://www.xinmi.cloud/op7418/guizang-ppt-skill) or [HTML PPT Skill](https://www.xinmi.cloud/lewislulu/html-ppt-skill) for browser-based presentations.
- [Huashu Markdown To HTML](https://www.xinmi.cloud/alchaincyf/huashu-md-html) for Markdown/HTML document conversion.
- [Web Access](https://www.xinmi.cloud/eze-is/web-access) for web research workflows.
- [OpenCLI](https://www.xinmi.cloud/jackwener/opencli) for logged-in browser automation and reusable website CLI adapters.
- [Fireworks Tech Graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph) for technical diagrams.
- [SlowMist Agent Security](https://www.xinmi.cloud/slowmist/slowmist-agent-security) for reviewing risky community skills.
## Original Source List
This document was compiled from a curated Hermes / Claude skill sharing list and expanded with public GitHub repository metadata.
This document was compiled from a curated Hermes / Claude skill sharing list and expanded with public 新觅源码库 repository metadata.
@@ -4,7 +4,7 @@
社区 Skill 本质上是第三方指令和代码。安装前请先审计,尤其是会读取 API Key、Cookie、浏览器登录态、本地文件、仓库内容,或者会执行 shell、安装依赖、自动发帖、访问外部 API 的 Skill。
欢迎大家推荐各种好用的 Skill。如果你发现值得收录的高质量 Skill,可以到 GitHub 提交 PR,并附上仓库链接、适用场景和必要的安全说明。
欢迎大家推荐各种好用的 Skill。如果你发现值得收录的高质量 Skill,可以到 新觅源码库 提交 PR,并附上仓库链接、适用场景和必要的安全说明。
## 维护规范
@@ -28,7 +28,7 @@
### Anthropic 官方 Skills
- 仓库:[anthropics/skills](https://github.com/anthropics/skills/tree/main/skills)
- 仓库:[anthropics/skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills)
- 方向:Claude 官方参考 Skill。
- 适合:学习标准 Skill 结构、参考稳定实现、搭建通用工作流。
- 代表 Skills`docx``pdf``pptx``xlsx``frontend-design``webapp-testing``skill-creator``mcp-builder``theme-factory``web-artifacts-builder`
@@ -36,7 +36,7 @@
### Matt Pocock Skills
- 仓库:[mattpocock/skills](https://github.com/mattpocock/skills)
- 仓库:[mattpocock/skills](https://www.xinmi.cloud/mattpocock/skills)
- 方向:工程与生产力工作流。
- 适合:TypeScript 工程、TDD、问题诊断、代码评审、原型开发、PRD/Issue/Handoff 等开发流程。
- 代表 Skills`tdd``triage``diagnose``prototype``review``to-prd``to-issues``handoff``write-a-skill`
@@ -46,56 +46,56 @@
### Frontend Slides
- 仓库:[zarazhangrui/frontend-slides](https://github.com/zarazhangrui/frontend-slides)
- 仓库:[zarazhangrui/frontend-slides](https://www.xinmi.cloud/zarazhangrui/frontend-slides)
- 方向:用前端技术生成网页幻灯片。
- 适合:HTML/CSS 幻灯片、视觉叙事、浏览器渲染的演示稿。
- 备注:适合把演示稿当成 Web Artifact 来做,而不是传统 Office 文件。
### 华叔 Design
- 仓库:[alchaincyf/huashu-design](https://github.com/alchaincyf/huashu-design)
- 仓库:[alchaincyf/huashu-design](https://www.xinmi.cloud/alchaincyf/huashu-design)
- 方向:Claude Code 中的 HTML 原生设计 Skill。
- 适合:高保真原型、幻灯片、动画概念、视觉评审和导出型设计流程。
- 备注:包含设计哲学、评审维度和演示型工作流。
### 归藏 PPT Skill
- 仓库:[op7418/guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill)
- 仓库:[op7418/guizang-ppt-skill](https://www.xinmi.cloud/op7418/guizang-ppt-skill)
- 方向:生成高质量 HTML 幻灯片。
- 适合:杂志风、编辑风、瑞士风等视觉风格的演示稿、社交封面、图片提示词和叙事型页面。
- 备注:包含演示运行时和风格化生成模式。
### HTML PPT Skill
- 仓库:[lewislulu/html-ppt-skill](https://github.com/lewislulu/html-ppt-skill)
- 仓库:[lewislulu/html-ppt-skill](https://www.xinmi.cloud/lewislulu/html-ppt-skill)
- 方向:HTML PPT Studio。
- 适合:主题化幻灯片、复杂布局演示稿和带动画的浏览器演示。
- 代表能力:多主题、多布局、动画模式和 HTML 演示脚手架。
### PPT Image First
- 仓库:[NyxTides/ppt-image-first](https://github.com/NyxTides/ppt-image-first)
- 仓库:[NyxTides/ppt-image-first](https://www.xinmi.cloud/NyxTides/ppt-image-first)
- 方向:图片优先的 PPT 生成。
- 适合:视觉方向先行的演示稿创作。
- 备注:面向 Codex、Claude Code、OpenCode CLI 等 Agent 工作流。
### GPT Image To PPT
- 仓库:[JuneYaooo/gpt-image2-ppt-skills](https://github.com/JuneYaooo/gpt-image2-ppt-skills)
- 仓库:[JuneYaooo/gpt-image2-ppt-skills](https://www.xinmi.cloud/JuneYaooo/gpt-image2-ppt-skills)
- 方向:用图像生成能力复刻或改造 PPT 视觉版式。
- 适合:从已有 `.pptx` 模板中学习版式,再替换成自己的内容。
- 备注:涉及图像生成和外部 API 时请先检查配置与数据发送逻辑。
### Fireworks Tech Graph
- 仓库:[yizhiyanhua-ai/fireworks-tech-graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph)
- 仓库:[yizhiyanhua-ai/fireworks-tech-graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph)
- 方向:技术图表生成。
- 适合:架构图、流程图、UML 风格图、AI Agent 工作流图,以及 SVG/PNG 输出。
- 备注:需要图表而不是整套演示稿时很实用。
### Diagram Skill
- 仓库:[312362115/claude diagram skill](https://github.com/312362115/claude/blob/main/skills/diagram/SKILL.md)
- 仓库:[312362115/claude diagram skill](https://www.xinmi.cloud/312362115/claude/blob/main/skills/diagram/SKILL.md)
- 方向:结构化图表生成。
- 适合:生成图表、模板化视觉解释和技术说明。
- 备注:这是一个直接指向 `SKILL.md` 的链接,安装前也要检查同目录下的 `references``scripts``templates`
@@ -104,7 +104,7 @@
### 华叔 Markdown To HTML
- 仓库:[alchaincyf/huashu-md-html](https://github.com/alchaincyf/huashu-md-html)
- 仓库:[alchaincyf/huashu-md-html](https://www.xinmi.cloud/alchaincyf/huashu-md-html)
- 方向:Markdown 与 HTML 双向转换流水线。
- 适合:把文件或网页转 Markdown,把 Markdown 转精美 HTML,把 HTML 再转回 Markdown。
- 代表工具:MarkItDown、Pandoc、html-to-markdown、trafilatura。
@@ -112,14 +112,14 @@
### 中文网文写作 Skill
- 仓库:[Tomsawyerhu/Chinese-WebNovel-Skill](https://github.com/Tomsawyerhu/Chinese-WebNovel-Skill)
- 仓库:[Tomsawyerhu/Chinese-WebNovel-Skill](https://www.xinmi.cloud/Tomsawyerhu/Chinese-WebNovel-Skill)
- 方向:中文网文小说写作。
- 适合:长篇小说规划、章节创作、风格延续和网文式叙事。
- 代表 Skill`webnovel-writing`
### 软件著作权材料 Skill
- 仓库:[Fokkyp/SoftwareCopyright-Skill](https://github.com/Fokkyp/SoftwareCopyright-Skill)
- 仓库:[Fokkyp/SoftwareCopyright-Skill](https://www.xinmi.cloud/Fokkyp/SoftwareCopyright-Skill)
- 方向:中国软件著作权申请材料生成。
- 适合:根据本地项目生成 `.docx` 软著申请材料。
- 代表 Skills`software-copyright-materials``docx-toolkit`
@@ -127,7 +127,7 @@
### 专利交底书 Skill
- 仓库:[handsomestWei/patent-disclosure-skill](https://github.com/handsomestWei/patent-disclosure-skill)
- 仓库:[handsomestWei/patent-disclosure-skill](https://www.xinmi.cloud/handsomestWei/patent-disclosure-skill)
- 方向:专利技术交底书生成。
- 适合:从项目文档挖掘专利点、联网查新、脱敏成文和自检。
- 备注:可能涉及敏感技术资料和联网检索,使用前请关注数据处理方式。
@@ -136,7 +136,7 @@
### 宝玉 Skills
- 仓库:[JimLiu/baoyu-skills](https://github.com/JimLiu/baoyu-skills)
- 仓库:[JimLiu/baoyu-skills](https://www.xinmi.cloud/JimLiu/baoyu-skills)
- 方向:图片生成、内容转换、发布和媒体工作流。
- 适合:图片卡片、文章配图、幻灯片、URL 转 Markdown、YouTube 字幕、Markdown 转 HTML、社交平台发布。
- 代表 Skills`baoyu-image-gen``baoyu-imagine``baoyu-slide-deck``baoyu-markdown-to-html``baoyu-post-to-x``baoyu-post-to-wechat``baoyu-post-to-weibo``baoyu-url-to-markdown``baoyu-youtube-transcript``baoyu-translate``baoyu-diagram``baoyu-comic`
@@ -144,7 +144,7 @@
### Virtual Couple Travel Vlog
- 仓库:[vibeshotclub/vsc-skills / virtual-couple-travel-vlog](https://github.com/vibeshotclub/vsc-skills/tree/main/virtual-couple-travel-vlog)
- 仓库:[vibeshotclub/vsc-skills / virtual-couple-travel-vlog](https://www.xinmi.cloud/vibeshotclub/vsc-skills/tree/main/virtual-couple-travel-vlog)
- 方向:旅行 vlog 风格媒体生成。
- 适合:短视频视觉叙事、角色化旅行内容和可复用媒体提示词。
- 备注:这是一个大仓库里的子目录 Skill。
@@ -153,14 +153,14 @@
### Web Access
- 仓库:[eze-is/web-access](https://github.com/eze-is/web-access)
- 仓库:[eze-is/web-access](https://www.xinmi.cloud/eze-is/web-access)
- 方向:为 Agent 提供结构化联网能力。
- 适合:网页研究、浏览器辅助任务、并行信息收集和需要交互的网站。
- 安全提示:浏览器访问可能暴露已登录状态和本地浏览器数据,启用前要审计。
### OpenCLI
- 仓库:[jackwener/opencli](https://github.com/jackwener/opencli)
- 仓库:[jackwener/opencli](https://www.xinmi.cloud/jackwener/opencli)
- 方向:把网站、浏览器会话、Electron 应用和本地工具转换成 CLI 可调用的自动化入口。
- 适合:让 Agent 操作已登录的 Chrome 页面、编写可复用网站适配器、封装本地命令,以及把浏览器流程变成稳定命令。
- 代表 Skills`opencli-browser``opencli-adapter-author``opencli-autofix``opencli-usage`
@@ -168,7 +168,7 @@
### Follow Builders
- 仓库:[zarazhangrui/follow-builders](https://github.com/zarazhangrui/follow-builders)
- 仓库:[zarazhangrui/follow-builders](https://www.xinmi.cloud/zarazhangrui/follow-builders)
- 方向:跟踪 AI builders 的 X、博客和 YouTube 播客内容。
- 适合:关注 builder 而不是 influencer,生成摘要和内容 digest。
- 代表内容:X feed、blog feed、podcast feed、prompts 和状态文件。
@@ -176,7 +176,7 @@
### SlowMist Agent Security
- 仓库:[slowmist/slowmist-agent-security](https://github.com/slowmist/slowmist-agent-security)
- 仓库:[slowmist/slowmist-agent-security](https://www.xinmi.cloud/slowmist/slowmist-agent-security)
- 方向:AI Agent 安全审计框架。
- 适合:检查 Skill、MCP、仓库、URL、Prompt 和链上地址的安全风险。
- 核心原则:所有外部输入在验证前都不可信。
@@ -186,7 +186,7 @@
### 华叔 Nuwa Skill
- 仓库:[alchaincyf/nuwa-skill](https://github.com/alchaincyf/nuwa-skill)
- 仓库:[alchaincyf/nuwa-skill](https://www.xinmi.cloud/alchaincyf/nuwa-skill)
- 方向:把某个人或视角蒸馏成可复用 Skill。
- 适合:顾问团式思考、心智模型、决策启发式和特定视角写作。
- 代表视角:华叔 Nuwa、Feynman、Jobs、Musk、Naval、Paul Graham、Taleb。
@@ -194,7 +194,7 @@
### PUA / 反 PUA 类 Skills
- 仓库:[tanweai/pua](https://github.com/tanweai/pua)
- 仓库:[tanweai/pua](https://www.xinmi.cloud/tanweai/pua)
- 方向:高能动性、强反馈、反操控或尖锐教练风格的 Agent 行为。
- 适合:动机强化、批判反馈、反操控和刻意强风格交互。
- 代表 Skills`pua``pua-en``pua-ja``pua-loop``mama``p7``p9``p10``pro``shot``yes`
@@ -202,7 +202,7 @@
### Ex Skill
- 仓库:[therealXiaomanChu/ex-skill](https://github.com/therealXiaomanChu/ex-skill)
- 仓库:[therealXiaomanChu/ex-skill](https://www.xinmi.cloud/therealXiaomanChu/ex-skill)
- 方向:把某个前任/人格风格蒸馏成 AI Skill。
- 适合:Persona 实验、情绪化角色扮演和特定语气模拟。
- 代表 Skill`create-ex`
@@ -212,17 +212,17 @@
如果你只想先装一批实用的,可以从这些开始:
- [Anthropic 官方 Skills](https://github.com/anthropics/skills/tree/main/skills):参考实现和通用能力。
- [Matt Pocock Skills](https://github.com/mattpocock/skills):工程流程。
- [宝玉 Skills](https://github.com/JimLiu/baoyu-skills):图片、媒体和发布。
- [华叔 Design](https://github.com/alchaincyf/huashu-design):高保真 HTML 设计。
- [归藏 PPT Skill](https://github.com/op7418/guizang-ppt-skill) 或 [HTML PPT Skill](https://github.com/lewislulu/html-ppt-skill):浏览器演示稿。
- [华叔 Markdown To HTML](https://github.com/alchaincyf/huashu-md-html)Markdown/HTML 文档转换。
- [Web Access](https://github.com/eze-is/web-access):网页研究。
- [OpenCLI](https://github.com/jackwener/opencli):已登录浏览器自动化和可复用网站 CLI 适配器。
- [Fireworks Tech Graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph):技术图表。
- [SlowMist Agent Security](https://github.com/slowmist/slowmist-agent-security):社区 Skill 安全审计。
- [Anthropic 官方 Skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills):参考实现和通用能力。
- [Matt Pocock Skills](https://www.xinmi.cloud/mattpocock/skills):工程流程。
- [宝玉 Skills](https://www.xinmi.cloud/JimLiu/baoyu-skills):图片、媒体和发布。
- [华叔 Design](https://www.xinmi.cloud/alchaincyf/huashu-design):高保真 HTML 设计。
- [归藏 PPT Skill](https://www.xinmi.cloud/op7418/guizang-ppt-skill) 或 [HTML PPT Skill](https://www.xinmi.cloud/lewislulu/html-ppt-skill):浏览器演示稿。
- [华叔 Markdown To HTML](https://www.xinmi.cloud/alchaincyf/huashu-md-html)Markdown/HTML 文档转换。
- [Web Access](https://www.xinmi.cloud/eze-is/web-access):网页研究。
- [OpenCLI](https://www.xinmi.cloud/jackwener/opencli):已登录浏览器自动化和可复用网站 CLI 适配器。
- [Fireworks Tech Graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph):技术图表。
- [SlowMist Agent Security](https://www.xinmi.cloud/slowmist/slowmist-agent-security):社区 Skill 安全审计。
## 来源说明
本文档基于一份 Hermes / Claude Skills 分享清单整理,并补充了公开 GitHub 仓库描述与目录信息。
本文档基于一份 Hermes / Claude Skills 分享清单整理,并补充了公开 新觅源码库 仓库描述与目录信息。
@@ -355,10 +355,10 @@ function openChangelog() {
</div>
<div class="version-info">
<div class="version-links">
<a class="github-link" href="https://github.com/EKKOLearnAI/hermes-web-ui" target="_blank" rel="noopener noreferrer" title="GitHub">
<a class="github-link" href="http://192.168.6.101:3001/root/Hermes-ui" target="_blank" rel="noopener noreferrer" title="新觅源码库">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
</a>
<a class="website-link" href="https://ekkolearnai.com/" target="_blank" rel="noopener noreferrer" title="Website">
<a class="website-link" href="https://www.xinmi.cloud/" target="_blank" rel="noopener noreferrer" title="Website">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
</a>
</div>
+1 -1
View File
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job ausgelost',
// Anderungsprotokoll
changelog: {
new_0_6_7_1: 'Die Desktop-App nutzt jetzt standardmäßig Port 8748, unterstützt Zugriff im lokalen Netzwerk und kann direkt im lokalen Browser geöffnet werden',
new_0_6_7_9: 'Desktop-Download-Links sind jetzt auf der offiziellen Website https://ekkolearnai.com/ verfügbar, aktuelle Installer bleiben außerdem über GitHub Releases verfügbar',
new_0_6_7_9: 'Desktop-Download-Links sind jetzt auf der offiziellen Website https://www.xinmi.cloud/ verfügbar, aktuelle Installer bleiben außerdem über GitHub Releases verfügbar',
new_0_6_7_2: 'MCP-Tools sind vollständiger: Bridge Tool Discovery, MCP-Management-Lifecycle und Tool-Sichtbarkeit pro Modell im Manager wurden verbessert',
new_0_6_7_3: 'Nachrichtenlisten zentrieren leere Zustände korrekt, reduzieren Scroll-Jitter, zeigen beim Laden von History keine Live-Chat-Nachrichten mehr, behalten Scrollpositionen pro Session und blenden beim Session-Wechsel 1,5 Sekunden ein',
new_0_6_7_4: 'Bridge und Runtime sind stabiler durch erhaltene Text/tool-call-Reihenfolge, korrektes Profile runtime status loading, bessere Node/npm-Erkennung und übersprungene Produktionsdatenverzeichnis-Erstellung',
+2 -2
View File
@@ -1,7 +1,7 @@
export default {
// Login
login: {
title: 'Hermes Web UI',
title: '新觅 Web UI',
description: 'Enter your username and password to continue.',
placeholder: 'Access token',
submit: 'Login',
@@ -1483,7 +1483,7 @@ export default {
// Changelog
changelog: {
new_0_6_7_1: 'The desktop app now defaults to port 8748, supports LAN access, and can be opened directly from a local browser',
new_0_6_7_9: 'Desktop download links are now available on the official website at https://ekkolearnai.com/, and the latest installers remain available from GitHub Releases',
new_0_6_7_9: 'Desktop download links are now available on the official website at https://www.xinmi.cloud/, and the latest installers remain available from GitHub Releases',
new_0_6_7_2: 'MCP tooling is more complete with bridge tool discovery fixes, MCP management lifecycle fixes, and per-model tool visibility controls in the manager',
new_0_6_7_3: 'Message lists now center empty states correctly, reduce scroll jitter, avoid leaking live chat messages into History while loading, preserve per-session scroll positions, and fade in over 1.5 seconds on session switches',
new_0_6_7_4: 'Bridge and runtime stability improved by preserving text/tool-call ordering, fixing Profile runtime status loading, improving Node/npm detection, and skipping production data directory creation',
+1 -1
View File
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job ejecutado',
// Registro de cambios
changelog: {
new_0_6_7_1: 'La app de escritorio ahora usa el puerto 8748 por defecto, permite acceso desde la red local y puede abrirse directamente desde un navegador local',
new_0_6_7_9: 'Los enlaces de descarga de escritorio ya están disponibles en el sitio oficial https://ekkolearnai.com/, y los instaladores más recientes siguen disponibles en GitHub Releases',
new_0_6_7_9: 'Los enlaces de descarga de escritorio ya están disponibles en el sitio oficial https://www.xinmi.cloud/, y los instaladores más recientes siguen disponibles en GitHub Releases',
new_0_6_7_2: 'Las herramientas MCP quedan más completas con arreglos de discovery en bridge, ciclo de vida de gestión MCP y controles de visibilidad por modelo en el gestor',
new_0_6_7_3: 'Las listas de mensajes centran mejor el estado vacío, reducen saltos de scroll, evitan mostrar mensajes del chat activo mientras carga History, preservan la posición por sesión y hacen fade-in de 1,5 segundos al cambiar de sesión',
new_0_6_7_4: 'Bridge y runtime son más estables al preservar el orden texto/tool-call, corregir la carga de estado runtime de Profile, mejorar detección Node/npm y evitar crear directorios de datos en producción',
+1 -1
View File
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job declenche',
// Journal des modifications
changelog: {
new_0_6_7_1: 'L application desktop utilise maintenant le port 8748 par défaut, prend en charge l accès LAN et peut être ouverte directement depuis un navigateur local',
new_0_6_7_9: 'Les liens de téléchargement desktop sont maintenant disponibles sur le site officiel https://ekkolearnai.com/, et les derniers installateurs restent disponibles via GitHub Releases',
new_0_6_7_9: 'Les liens de téléchargement desktop sont maintenant disponibles sur le site officiel https://www.xinmi.cloud/, et les derniers installateurs restent disponibles via GitHub Releases',
new_0_6_7_2: 'Les outils MCP sont plus complets avec des corrections de découverte bridge, de cycle de vie MCP et des contrôles de visibilité par modèle dans le gestionnaire',
new_0_6_7_3: 'Les listes de messages centrent mieux les états vides, réduisent les sauts de scroll, évitent d afficher le chat actif pendant le chargement de History, conservent la position par session et ajoutent un fondu de 1,5 seconde au changement de session',
new_0_6_7_4: 'Bridge et runtime sont plus stables avec ordre texte/tool-call préservé, chargement du statut runtime de Profile corrigé, meilleure détection Node/npm et création du dossier de données production évitée',
+1 -1
View File
@@ -1260,7 +1260,7 @@ export default {
// 更新履歴
changelog: {
new_0_6_7_1: 'Desktop アプリは既定で port 8748 を使用し、LAN アクセスとローカルブラウザからの直接アクセスに対応しました',
new_0_6_7_9: 'Desktop のダウンロードリンクを公式サイト https://ekkolearnai.com/ に追加し、最新インストーラーは引き続き GitHub Releases からも取得できます',
new_0_6_7_9: 'Desktop のダウンロードリンクを公式サイト https://www.xinmi.cloud/ に追加し、最新インストーラーは引き続き GitHub Releases からも取得できます',
new_0_6_7_2: 'MCP ツールは bridge の tool discovery 修正、MCP 管理ライフサイクル修正、管理画面のモデル別 tool visibility によりさらに整備されました',
new_0_6_7_3: 'メッセージ一覧は empty state の中央揃え、scroll jitter、History 読み込み中のライブチャット混入を修正し、セッション別スクロール位置保持と 1.5 秒のフェードインに対応しました',
new_0_6_7_4: 'Bridge と runtime は text/tool-call の順序保持、Profile runtime status loading 修正、Node/npm 検出改善、本番 data directory 作成スキップで安定しました',
+1 -1
View File
@@ -1260,7 +1260,7 @@ export default {
// 변경 이력
changelog: {
new_0_6_7_1: 'Desktop 앱은 기본적으로 8748 포트를 사용하며 LAN 접근과 로컬 브라우저 직접 열기를 지원합니다',
new_0_6_7_9: 'Desktop 다운로드 링크가 공식 웹사이트 https://ekkolearnai.com/ 에 추가되었으며 최신 설치 파일은 GitHub Releases 에서도 계속 받을 수 있습니다',
new_0_6_7_9: 'Desktop 다운로드 링크가 공식 웹사이트 https://www.xinmi.cloud/ 에 추가되었으며 최신 설치 파일은 GitHub Releases 에서도 계속 받을 수 있습니다',
new_0_6_7_2: 'MCP 도구는 bridge tool discovery 수정, MCP 관리 라이프사이클 수정, 관리자 화면의 모델별 tool visibility 제어로 더 완성되었습니다',
new_0_6_7_3: '메시지 목록은 빈 상태 중앙 정렬, 스크롤 튐, History 로딩 중 라이브 채팅 메시지 노출을 수정하고 세션별 스크롤 위치 보존과 1.5초 페이드인을 지원합니다',
new_0_6_7_4: 'Bridge 와 runtime 은 text/tool-call 순서 보존, Profile runtime status loading 수정, Node/npm 감지 개선, 운영 데이터 디렉터리 생성 생략으로 더 안정적입니다',
+1 -1
View File
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job acionado',
// Registro de alteracoes
changelog: {
new_0_6_7_1: 'O app desktop agora usa a porta 8748 por padrão, permite acesso pela rede local e pode ser aberto diretamente em um navegador local',
new_0_6_7_9: 'Links de download do desktop agora estão disponíveis no site oficial https://ekkolearnai.com/, e os instaladores mais recentes continuam disponíveis no GitHub Releases',
new_0_6_7_9: 'Links de download do desktop agora estão disponíveis no site oficial https://www.xinmi.cloud/, e os instaladores mais recentes continuam disponíveis no GitHub Releases',
new_0_6_7_2: 'As ferramentas MCP ficam mais completas com correções de discovery no bridge, ciclo de vida de gestão MCP e controles de visibilidade por modelo no gestor',
new_0_6_7_3: 'Listas de mensagens centralizam melhor estados vazios, reduzem saltos de rolagem, evitam mostrar o chat ativo enquanto History carrega, preservam posição por sessão e fazem fade-in de 1,5 segundo ao trocar sessão',
new_0_6_7_4: 'Bridge e runtime ficam mais estáveis preservando a ordem texto/tool-call, corrigindo carregamento de status runtime de Profile, melhorando detecção Node/npm e evitando criação de diretório de dados em produção',
+1 -1
View File
@@ -1488,7 +1488,7 @@ export default {
// 更新日誌
changelog: {
new_0_6_7_1: '桌面版預設使用 8748 連接埠,支援區域網路內存取,也可以直接用本機瀏覽器開啟 Web UI',
new_0_6_7_9: '桌面端下載入口已補充到官網 https://ekkolearnai.com/,也可以繼續從 GitHub Releases 取得最新安裝包',
new_0_6_7_9: '桌面端下載入口已補充到官網 https://www.xinmi.cloud/,也可以繼續從 GitHub Releases 取得最新安裝包',
new_0_6_7_2: 'MCP 工具鏈繼續完善:修復 bridge 工具發現與 MCP 管理生命週期,並在管理頁支援按模型控制工具可見性',
new_0_6_7_3: '訊息列表體驗優化:修復空狀態置中、捲動抖動、歷史會話載入串訊息,並在切換會話時保留捲動位置與 1.5 秒淡入效果',
new_0_6_7_4: 'Bridge 與執行狀態更穩定:保持文字和 tool-call 順序、修復 Profile runtime 狀態載入、改進 Node/npm 偵測,並避免正式環境自動建立資料目錄',
+2 -2
View File
@@ -1,7 +1,7 @@
export default {
// 登录
login: {
title: 'Hermes Web UI',
title: '新觅 Web UI',
description: '输入用户名和密码以继续。',
placeholder: '访问令牌',
submit: '登录',
@@ -1485,7 +1485,7 @@ export default {
// 更新日志
changelog: {
new_0_6_7_1: '桌面版默认使用 8748 端口,支持局域网内访问,也可以直接用本机浏览器打开 Web UI',
new_0_6_7_9: '桌面端下载入口已补充到官网 https://ekkolearnai.com/,也可以继续从 GitHub Releases 获取最新安装包',
new_0_6_7_9: '桌面端下载入口已补充到官网 https://www.xinmi.cloud/,也可以继续从 GitHub Releases 获取最新安装包',
new_0_6_7_2: 'MCP 工具链继续完善:修复 bridge 工具发现与 MCP 管理生命周期,并在管理页支持按模型控制工具可见性',
new_0_6_7_3: '消息列表体验优化:修复空状态居中、滚动抖动、历史会话加载串消息,并在切换会话时保留滚动位置与 1.5 秒淡入效果',
new_0_6_7_4: 'Bridge 与运行态更稳定:保持文本和 tool-call 顺序、修复 Profile runtime 状态加载、改进 Node/npm 检测,并避免生产环境自动创建数据目录',
+3 -3
View File
@@ -62,7 +62,7 @@ async function handlePasswordLogin() {
<div class="login-view">
<div class="login-card">
<div class="login-logo">
<img src="/logo.png" alt="Hermes" width="80" height="80" />
<img src="/logo.png" alt="新觅源码库" width="80" height="80" />
</div>
<h1 class="login-title">{{ t("login.title") }}</h1>
<p class="login-desc">{{ t("login.description") }}</p>
@@ -87,9 +87,9 @@ async function handlePasswordLogin() {
<div v-if="errorMsg" class="login-error">{{ errorMsg }}</div>
<div v-if="showLockResetHint" class="login-lock-hint">
<span>{{ t("login.lockResetHint") }}</span>
<code>hermes-web-ui clear-login-locks --restart</code>
<code>xinmi-hermes-ui clear-login-locks --restart</code>
<span>{{ t("login.defaultLoginResetHint") }}</span>
<code>hermes-web-ui reset-default-login</code>
<code>xinmi-hermes-ui reset-default-login</code>
</div>
<button type="submit" class="login-btn" :disabled="loading">
{{ loading ? "..." : t("login.submit") }}
+3 -3
View File
@@ -6,7 +6,7 @@ Electron desktop distribution for Hermes Studio.
Download the latest macOS, Windows, or Linux installer for your CPU
architecture from the project
[GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest).
[新觅源码库 Releases](https://www.xinmi.cloud/root/Hermes-ui/releases/latest).
The desktop app bundles the Web UI runtime and launches it locally from the
native shell app.
@@ -32,9 +32,9 @@ export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
export ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
```
If GitHub release downloads are slow, `fetch-python.mjs` can also use a compatible
If 新觅源码库 release downloads are slow, `fetch-python.mjs` can also use a compatible
python-build-standalone release mirror:
```sh
export PBS_BASE_URL=https://github.com/astral-sh/python-build-standalone/releases/download
export PBS_BASE_URL=https://www.xinmi.cloud/astral-sh/python-build-standalone/releases/download
```
@@ -0,0 +1,3 @@
{
"tag": "hermes-0.15.2-runtime"
}
+3 -5
View File
@@ -22,7 +22,8 @@ files:
- "!**/node_modules/.bin"
- "!**/{.DS_Store,.git,.gitignore,.eslintrc*,.prettierrc*,*.map,tsconfig.json}"
# Web UI source (built dist) and bundled Python live outside the asar.
# Web UI source (built dist) lives outside the asar. Python/Node/Git runtime
# assets are downloaded into the user's Web UI home on first launch.
# This package lives at packages/desktop, so ../.. is the hermes-web-ui repo root.
extraResources:
- from: "build"
@@ -32,6 +33,7 @@ extraResources:
- "icon.ico"
- "trayTemplate.png"
- "trayWindows.png"
- "runtime-release.json"
- from: "../.."
to: "webui"
filter:
@@ -44,10 +46,6 @@ extraResources:
- "!packages/desktop/**"
- "!**/{.git,.github,docs,tests,playwright.config.ts,README*,scripts,*.map}"
- "!node_modules/**/*.md"
- from: "resources/python/${os}-${arch}"
to: "python"
filter:
- "**/*"
asarUnpack:
- "**/*.node"
+58 -58
View File
@@ -7,7 +7,7 @@
"": {
"name": "hermes-studio",
"version": "0.6.8",
"license": "MIT",
"license": "BSL-1.1",
"dependencies": {
"electron-updater": "^6.3.9"
},
@@ -169,7 +169,7 @@
"node": ">= 8.0.0"
},
"funding": {
"url": "https://github.com/sponsors/gjtorikian/"
"url": "https://www.xinmi.cloud/sponsors/gjtorikian/"
}
},
"node_modules/@electron/rebuild": {
@@ -265,7 +265,7 @@
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/@gar/promisify": {
@@ -303,7 +303,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
@@ -316,7 +316,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
@@ -341,7 +341,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
@@ -357,7 +357,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
@@ -375,7 +375,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@malept/cross-spawn-promise": {
@@ -386,7 +386,7 @@
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/malept"
"url": "https://www.xinmi.cloud/sponsors/malept"
},
{
"type": "tidelift",
@@ -483,7 +483,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
"url": "https://www.xinmi.cloud/sindresorhus/is?sponsor=1"
}
},
"node_modules/@szmarczak/http-timer": {
@@ -692,7 +692,7 @@
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
"url": "https://www.xinmi.cloud/sponsors/epoberezkin"
}
},
"node_modules/ajv-keywords": {
@@ -728,7 +728,7 @@
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/app-builder-bin": {
@@ -967,7 +967,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
"url": "https://www.xinmi.cloud/sponsors/feross"
},
{
"type": "patreon",
@@ -1030,7 +1030,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
"url": "https://www.xinmi.cloud/sponsors/feross"
},
{
"type": "patreon",
@@ -1168,7 +1168,7 @@
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/cacache/node_modules/lru-cache": {
@@ -1251,7 +1251,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/chalk?sponsor=1"
}
},
"node_modules/chownr": {
@@ -1279,7 +1279,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
"url": "https://www.xinmi.cloud/sponsors/sibiraj-s"
}
],
"license": "MIT",
@@ -1320,7 +1320,7 @@
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
@@ -1338,7 +1338,7 @@
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/cliui": {
@@ -1376,7 +1376,7 @@
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/color-convert": {
@@ -1513,7 +1513,7 @@
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/config-file-ts/node_modules/minimatch": {
@@ -1529,7 +1529,7 @@
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/config-file-ts/node_modules/minipass": {
@@ -1641,7 +1641,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
@@ -1654,7 +1654,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/defaults": {
@@ -1667,7 +1667,7 @@
"clone": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/defer-to-connect": {
@@ -2015,7 +2015,7 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/err-code": {
@@ -2201,7 +2201,7 @@
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
@@ -2214,7 +2214,7 @@
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/form-data": {
@@ -2283,7 +2283,7 @@
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/gauge": {
@@ -2339,7 +2339,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/get-proto": {
@@ -2369,7 +2369,7 @@
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/glob": {
@@ -2391,7 +2391,7 @@
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/glob/node_modules/balanced-match": {
@@ -2435,7 +2435,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/got": {
@@ -2461,7 +2461,7 @@
"node": ">=10.19.0"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
"url": "https://www.xinmi.cloud/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
@@ -2490,7 +2490,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
@@ -2506,7 +2506,7 @@
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
@@ -2640,7 +2640,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
"url": "https://www.xinmi.cloud/sponsors/feross"
},
{
"type": "patreon",
@@ -2759,7 +2759,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/isarray": {
@@ -2780,7 +2780,7 @@
"node": ">= 18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/gjtorikian/"
"url": "https://www.xinmi.cloud/sponsors/gjtorikian/"
}
},
"node_modules/isexe": {
@@ -2800,7 +2800,7 @@
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
@@ -3015,7 +3015,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/lowercase-keys": {
@@ -3200,7 +3200,7 @@
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/minimist": {
@@ -3210,7 +3210,7 @@
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://www.xinmi.cloud/sponsors/ljharb"
}
},
"node_modules/minipass": {
@@ -3443,7 +3443,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/npmlog": {
@@ -3486,7 +3486,7 @@
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/ora": {
@@ -3510,7 +3510,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/p-cancelable": {
@@ -3536,7 +3536,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/p-map": {
@@ -3552,7 +3552,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/package-json-from-dist": {
@@ -3596,7 +3596,7 @@
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
@@ -3628,7 +3628,7 @@
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jet2jet"
"url": "https://www.xinmi.cloud/sponsors/jet2jet"
}
},
"node_modules/pend": {
@@ -3730,7 +3730,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/read-binary-file-arch": {
@@ -3830,7 +3830,7 @@
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jet2jet"
"url": "https://www.xinmi.cloud/sponsors/jet2jet"
}
},
"node_modules/resolve-alpn": {
@@ -3850,7 +3850,7 @@
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor": {
@@ -3891,7 +3891,7 @@
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"url": "https://www.xinmi.cloud/sponsors/isaacs"
}
},
"node_modules/safe-buffer": {
@@ -3902,7 +3902,7 @@
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
"url": "https://www.xinmi.cloud/sponsors/feross"
},
{
"type": "patreon",
@@ -4464,7 +4464,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
@@ -4483,7 +4483,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
@@ -4570,7 +4570,7 @@
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
}
},
"node_modules/zip-stream": {
+9 -3
View File
@@ -2,21 +2,27 @@
"name": "hermes-studio",
"version": "0.6.8",
"description": "Hermes Studio desktop distribution with bundled Python runtime and hermes-agent",
"homepage": "https://ekkolearnai.com",
"homepage": "https://www.xinmi.cloud",
"author": {
"name": "Hermes Studio Contributors",
"email": "noreply@hermes-studio.local"
},
"license": "MIT",
"license": "BSL-1.1",
"private": true,
"main": "dist/main/index.js",
"scripts": {
"build:main": "tsc -p tsconfig.json",
"build": "npm run build:main",
"fetch:node": "node scripts/fetch-node.mjs",
"fetch:git": "node scripts/fetch-git.mjs",
"fetch:python": "node scripts/fetch-python.mjs",
"install:hermes": "node scripts/install-hermes.mjs",
"patch:hermes": "node scripts/apply-hermes-patches.mjs",
"prepare:python": "npm run fetch:python && npm run install:hermes && npm run patch:hermes && npm run prune:python",
"write:runtime-release": "node scripts/write-runtime-release.mjs",
"prepare:runtime": "npm run fetch:node && npm run fetch:git && npm run fetch:python && npm run install:hermes && npm run patch:hermes && npm run prune:python",
"prepare:python": "npm run prepare:runtime",
"package:runtime": "node scripts/package-runtime.mjs",
"runtime:asset-name": "node scripts/runtime-asset-name.mjs",
"prune:python": "node scripts/prune-python.mjs",
"dev": "npm run build:main && electron .",
"dist": "npm run build && electron-builder",
@@ -33,6 +33,7 @@ const sitePkgs = process.env.HERMES_AGENT_SITE_PACKAGES ?? (
)
const dtPath = join(sitePkgs, 'gateway', 'platforms', 'dingtalk.py')
const browserToolPath = join(sitePkgs, 'tools', 'browser_tool.py')
const sitecustomizePath = join(sitePkgs, 'sitecustomize.py')
if (!existsSync(dtPath)) {
console.error(`dingtalk.py not found at ${dtPath} — is hermes-agent installed?`)
@@ -59,6 +60,21 @@ function patch(id, marker, find, replace) {
applied++
}
function patchText(text, id, marker, find, replace) {
if (text.includes(marker)) {
console.log(` · ${id} (already applied)`)
skipped++
return text
}
if (!text.includes(find)) {
console.log(`${id} (anchor not found — upstream changed?)`)
return text
}
applied++
console.log(`${id}`)
return text.replace(find, replace)
}
console.log(`Patching ${dtPath}`)
// NOTE: the former `dt-pre-start` patch was retired — hermes-agent now ships
@@ -179,6 +195,63 @@ if (src !== before) {
writeFileSync(dtPath, src)
}
if (existsSync(browserToolPath)) {
console.log(`Patching ${browserToolPath}`)
let browserSrc = readFileSync(browserToolPath, 'utf-8')
const browserBefore = browserSrc
browserSrc = patchText(
browserSrc,
'browser-stdout-decode-fallback',
'# patch:browser-stdout-decode-fallback',
`from hermes_cli.config import cfg_get\n`,
`from hermes_cli.config import cfg_get
# patch:browser-stdout-decode-fallback
def _hermes_read_browser_output(path: str) -> str:
data = Path(path).read_bytes()
for encoding in ("utf-8", "gb18030"):
try:
return data.decode(encoding)
except UnicodeDecodeError:
pass
return data.decode("utf-8", errors="replace")
`,
)
for (const [id, find, replace] of [
[
'browser-fallback-stdout-read',
` with open(stdout_path, "r", encoding="utf-8") as f:
stdout = f.read().strip()`,
` # patch:browser-fallback-stdout-read
stdout = _hermes_read_browser_output(stdout_path).strip()`,
],
[
'browser-command-stdout-read',
` with open(stdout_path, "r", encoding="utf-8") as f:
stdout = f.read()
with open(stderr_path, "r", encoding="utf-8") as f:
stderr = f.read()`,
` # patch:browser-command-stdout-read
stdout = _hermes_read_browser_output(stdout_path)
stderr = _hermes_read_browser_output(stderr_path)`,
],
]) {
browserSrc = patchText(
browserSrc,
id,
`# patch:${id}`,
find,
replace,
)
}
if (browserSrc !== browserBefore) {
writeFileSync(browserToolPath, browserSrc)
}
}
const brotlicffiCompatMarker = '# patch:brotlicffi-error-compat'
const brotlicffiCompat = `
${brotlicffiCompatMarker}
@@ -194,15 +267,76 @@ except Exception:
pass
`
const desktopHiddenSubprocessMarker = '# patch:desktop-hidden-subprocess-defaults'
const desktopHiddenSubprocessDefaults = `
${desktopHiddenSubprocessMarker}
try:
import os as _hermes_os
if _hermes_os.name == "nt" and _hermes_os.environ.get("HERMES_DESKTOP", "").strip().lower() == "true":
import asyncio as _hermes_asyncio
import subprocess as _hermes_subprocess
if not getattr(_hermes_subprocess, "_hermes_desktop_hidden_defaults_installed", False):
_hermes_create_no_window = getattr(_hermes_subprocess, "CREATE_NO_WINDOW", 0) or 0x08000000
def _hermes_apply_hidden_process_options(kwargs):
flags = kwargs.get("creationflags", 0) or 0
try:
kwargs["creationflags"] = int(flags) | _hermes_create_no_window
except Exception:
kwargs["creationflags"] = _hermes_create_no_window
startupinfo = kwargs.get("startupinfo")
if startupinfo is None:
try:
startupinfo = _hermes_subprocess.STARTUPINFO()
except Exception:
return
kwargs["startupinfo"] = startupinfo
try:
startupinfo.dwFlags |= getattr(_hermes_subprocess, "STARTF_USESHOWWINDOW", 1)
startupinfo.wShowWindow = getattr(_hermes_subprocess, "SW_HIDE", 0)
except Exception:
pass
_hermes_original_popen = _hermes_subprocess.Popen
_hermes_original_create_subprocess_exec = _hermes_asyncio.create_subprocess_exec
_hermes_original_create_subprocess_shell = _hermes_asyncio.create_subprocess_shell
class _HermesHiddenPopen(_hermes_original_popen):
def __init__(self, *args, **kwargs):
_hermes_apply_hidden_process_options(kwargs)
super().__init__(*args, **kwargs)
async def _hermes_hidden_create_subprocess_exec(*args, **kwargs):
_hermes_apply_hidden_process_options(kwargs)
return await _hermes_original_create_subprocess_exec(*args, **kwargs)
async def _hermes_hidden_create_subprocess_shell(*args, **kwargs):
_hermes_apply_hidden_process_options(kwargs)
return await _hermes_original_create_subprocess_shell(*args, **kwargs)
_hermes_subprocess.Popen = _HermesHiddenPopen
_hermes_asyncio.create_subprocess_exec = _hermes_hidden_create_subprocess_exec
_hermes_asyncio.create_subprocess_shell = _hermes_hidden_create_subprocess_shell
_hermes_subprocess._hermes_desktop_hidden_defaults_installed = True
except Exception:
pass
`
function appendSitecustomizePatch(id, marker, body) {
const sitecustomize = existsSync(sitecustomizePath) ? readFileSync(sitecustomizePath, 'utf-8') : ''
if (sitecustomize.includes(brotlicffiCompatMarker)) {
console.log(' · brotlicffi-error-compat (already applied)')
if (sitecustomize.includes(marker)) {
console.log(` · ${id} (already applied)`)
skipped++
} else {
const nextSitecustomize = `${sitecustomize.replace(/\s*$/, '')}\n${brotlicffiCompat.trim()}\n`
return
}
const nextSitecustomize = `${sitecustomize.replace(/\s*$/, '')}\n${body.trim()}\n`
writeFileSync(sitecustomizePath, nextSitecustomize)
console.log(' ✓ brotlicffi-error-compat')
console.log(`${id}`)
applied++
}
appendSitecustomizePatch('brotlicffi-error-compat', brotlicffiCompatMarker, brotlicffiCompat)
appendSitecustomizePatch('desktop-hidden-subprocess-defaults', desktopHiddenSubprocessMarker, desktopHiddenSubprocessDefaults)
console.log(`Done. Applied ${applied}, skipped ${skipped}.`)
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env node
// Download Git for Windows MinGit for Windows builds. Other platforms create
// an empty resource directory so electron-builder can use the same resource map.
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { arch as osArch, platform as osPlatform, tmpdir } from 'node:os'
import { spawnSync } from 'node:child_process'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dirname, '..')
const TARGET_OS = process.env.TARGET_OS || osPlatform()
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
const OUT_DIR = resolve(ROOT, 'resources', 'git', `${OS_LABEL}-${TARGET_ARCH}`)
mkdirSync(OUT_DIR, { recursive: true })
if (TARGET_OS !== 'win32') {
writeFileSync(resolve(OUT_DIR, '.placeholder'), 'Git for Windows is only bundled on Windows.\n')
console.log(`Git resource placeholder ready at ${OUT_DIR}`)
process.exit(0)
}
if (TARGET_ARCH !== 'x64') {
console.error(`Unsupported Git for Windows target: ${TARGET_OS}-${TARGET_ARCH}`)
process.exit(1)
}
if (existsSync(resolve(OUT_DIR, 'cmd', 'git.exe'))) {
console.log(`Git for Windows already present at ${OUT_DIR}, skipping`)
process.exit(0)
}
async function latestMinGitUrl() {
if (process.env.GIT_FOR_WINDOWS_URL?.trim()) return process.env.GIT_FOR_WINDOWS_URL.trim()
const headers = { 'User-Agent': 'hermes-studio-desktop-build' }
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
if (token?.trim()) headers.Authorization = `Bearer ${token.trim()}`
const response = await fetch('https://api.github.com/repos/git-for-windows/git/releases/latest', {
headers,
})
if (!response.ok) {
throw new Error(`GitHub API returned ${response.status}`)
}
const release = await response.json()
const asset = release.assets?.find(candidate =>
typeof candidate?.name === 'string'
&& /^MinGit-.*-64-bit\.zip$/.test(candidate.name)
&& typeof candidate.browser_download_url === 'string',
)
if (!asset) throw new Error('Could not find MinGit 64-bit zip in latest Git for Windows release')
return asset.browser_download_url
}
let url
try {
url = await latestMinGitUrl()
} catch (err) {
console.error(`Failed to resolve Git for Windows download URL: ${err instanceof Error ? err.message : String(err)}`)
process.exit(1)
}
const file = url.split('/').pop() || 'mingit.zip'
const archivePath = resolve(tmpdir(), file)
console.log(`Fetching ${url}`)
const curl = spawnSync('curl', ['-fL', '--retry', '3', '-o', archivePath, url], { stdio: 'inherit' })
if (curl.status !== 0) {
console.error('curl failed')
process.exit(curl.status ?? 1)
}
console.log(`Extracting into ${OUT_DIR}`)
const extract = spawnSync('tar', ['-xf', archivePath, '-C', OUT_DIR], { stdio: 'inherit' })
if (extract.status !== 0) {
console.error('extract failed')
process.exit(extract.status ?? 1)
}
rmSync(archivePath, { force: true })
console.log(`Git for Windows ready at ${OUT_DIR}`)
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env node
// Download a portable Node.js runtime for the current (or target) platform/arch
// and extract into resources/node/<os>-<arch>/.
import { existsSync, mkdirSync, rmSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { arch as osArch, platform as osPlatform, tmpdir } from 'node:os'
import { spawnSync } from 'node:child_process'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dirname, '..')
const TARGET_OS = process.env.TARGET_OS || osPlatform()
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
const NODE_VERSION = (process.env.HERMES_DESKTOP_NODE_VERSION || process.env.NODE_VERSION || process.versions.node).replace(/^v/, '')
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
const OUT_DIR = resolve(ROOT, 'resources', 'node', `${OS_LABEL}-${TARGET_ARCH}`)
const DIST_PLATFORM = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'darwin' : TARGET_OS
const DIST_ARCH = TARGET_ARCH === 'x64' ? 'x64' : TARGET_ARCH === 'arm64' ? 'arm64' : ''
if (!DIST_ARCH || !['win', 'darwin', 'linux'].includes(DIST_PLATFORM)) {
console.error(`Unsupported target: ${TARGET_OS}-${TARGET_ARCH}`)
process.exit(1)
}
const ext = TARGET_OS === 'win32' ? 'zip' : 'tar.gz'
const file = `node-v${NODE_VERSION}-${DIST_PLATFORM}-${DIST_ARCH}.${ext}`
const baseUrl = (process.env.NODE_DIST_BASE_URL || 'https://nodejs.org/dist').replace(/\/$/, '')
const url = `${baseUrl}/v${NODE_VERSION}/${file}`
const marker = TARGET_OS === 'win32' ? 'node.exe' : join('bin', 'node')
if (existsSync(resolve(OUT_DIR, marker))) {
console.log(`Node.js already present at ${OUT_DIR}, skipping`)
process.exit(0)
}
mkdirSync(OUT_DIR, { recursive: true })
const archivePath = resolve(tmpdir(), file)
console.log(`Fetching ${url}`)
const curl = spawnSync('curl', ['-fL', '--retry', '3', '-o', archivePath, url], { stdio: 'inherit' })
if (curl.status !== 0) {
console.error('curl failed')
process.exit(curl.status ?? 1)
}
console.log(`Extracting into ${OUT_DIR}`)
let extract
if (TARGET_OS === 'win32') {
extract = spawnSync('tar', ['-xf', archivePath, '-C', OUT_DIR, '--strip-components=1'], { stdio: 'inherit' })
} else {
extract = spawnSync('tar', ['-xzf', archivePath, '-C', OUT_DIR, '--strip-components=1'], { stdio: 'inherit' })
}
if (extract.status !== 0) {
console.error('extract failed')
process.exit(extract.status ?? 1)
}
rmSync(archivePath, { force: true })
console.log(`Node.js ready at ${OUT_DIR}`)
+364 -31
View File
@@ -1,22 +1,69 @@
#!/usr/bin/env node
// Install hermes-agent into the bundled Python at resources/python/<os>-<arch>/.
// Prefers `uv` (10-100x faster, more deterministic) and falls back to pip.
import { existsSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import {
chmodSync,
copyFileSync,
cpSync,
existsSync,
lstatSync,
mkdirSync,
readdirSync,
rmSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from 'node:fs'
import { basename, resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
import { platform as osPlatform, arch as osArch } from 'node:os'
import { platform as osPlatform, arch as osArch, homedir as osHomedir } from 'node:os'
import { hermesVersion } from './runtime-config.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dirname, '..')
const TARGET_OS = process.env.TARGET_OS || osPlatform()
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
const HERMES_VERSION = process.env.HERMES_VERSION || '0.15.2'
const HERMES_PACKAGE = process.env.HERMES_PACKAGE || `hermes-agent[mcp]==${HERMES_VERSION}`
const HERMES_VERSION = hermesVersion()
// Match the packaged runtime to the channel list exposed at /hermes/channels.
// Telegram, Discord, and Slack are covered by "messaging". We intentionally
// install Matrix's plaintext deps below instead of using the "matrix" extra:
// that extra pulls mautrix[encryption] -> python-olm, which needs a fragile
// native build on desktop packaging machines. WhatsApp, QQBot, and Weixin do
// not expose dedicated hermes-agent extras; their deps are covered by base or
// the channel extras below.
const HERMES_EXTRAS = [
'mcp',
'messaging',
'slack',
'wecom',
'dingtalk',
'feishu',
].join(',')
const HERMES_PACKAGE = process.env.HERMES_PACKAGE || `hermes-agent[${HERMES_EXTRAS}]==${HERMES_VERSION}`
const EXTRA_PYTHON_PACKAGES = splitPackageList(
process.env.HERMES_EXTRA_PYTHON_PACKAGES || [
'websockets',
'mautrix==0.21.0',
'Markdown==3.10.2',
'aiosqlite==0.22.1',
'asyncpg==0.31.0',
'aiohttp-socks==0.11.0',
].join(' '),
)
const BROWSER_PACKAGES = splitPackageList(
process.env.HERMES_BROWSER_PACKAGES || 'agent-browser@^0.26.0 @askjo/camofox-browser@^1.5.2',
)
const SKIP_BROWSER_RUNTIME = process.env.HERMES_SKIP_BROWSER_RUNTIME === '1'
|| process.env.HERMES_SKIP_BROWSER_RUNTIME?.toLowerCase() === 'true'
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
const PY_DIR = resolve(ROOT, 'resources', 'python', `${OS_LABEL}-${TARGET_ARCH}`)
const NODE_DIR = resolve(ROOT, 'resources', 'node', `${OS_LABEL}-${TARGET_ARCH}`)
const NODE_PREFIX = resolve(PY_DIR, 'node')
const AGENT_BROWSER_HOME = resolve(PY_DIR, 'agent-browser')
const PLAYWRIGHT_BROWSERS_PATH = resolve(PY_DIR, 'ms-playwright')
const pyBin = TARGET_OS === 'win32'
? resolve(PY_DIR, 'python.exe')
@@ -32,33 +79,308 @@ function hasUv() {
return r.status === 0
}
let r
function splitPackageList(value) {
return value
.split(/[,\s]+/)
.map(part => part.trim())
.filter(Boolean)
}
function run(command, args, options = {}) {
const result = spawnSync(command, args, { stdio: 'inherit', ...options })
if (result.status !== 0) process.exit(result.status ?? 1)
return result
}
function optionalRun(command, args, options = {}) {
return spawnSync(command, args, { stdio: 'inherit', ...options })
}
function commandInvocation(command) {
if (TARGET_OS === 'win32' && command.toLowerCase().endsWith('.cmd')) {
const cmdCommand = /[\s&()[\]{}^=;!'+,`~]/.test(command) ? `"${command}"` : command
return { command: 'cmd.exe', argsPrefix: ['/d', '/s', '/c', cmdCommand] }
}
return { command, argsPrefix: [] }
}
function runInvocation(invocation, args, options = {}) {
return run(invocation.command, [...invocation.argsPrefix, ...args], options)
}
function optionalRunInvocation(invocation, args, options = {}) {
return optionalRun(invocation.command, [...invocation.argsPrefix, ...args], options)
}
function pythonBuildEnv() {
if (TARGET_OS !== 'darwin') return process.env
const env = { ...process.env }
if (!env.AR && existsSync('/usr/bin/ar')) env.AR = '/usr/bin/ar'
if (!env.RANLIB && existsSync('/usr/bin/ranlib')) env.RANLIB = '/usr/bin/ranlib'
return env
}
function installPythonPackages(packages, label) {
if (packages.length === 0) return
const env = pythonBuildEnv()
if (hasUv()) {
console.log(`→ Installing ${HERMES_PACKAGE} via uv`)
r = spawnSync('uv', [
console.log(`→ Installing ${label} via uv: ${packages.join(' ')}`)
run('uv', [
'pip', 'install',
'--python', pyBin,
HERMES_PACKAGE,
], { stdio: 'inherit' })
...packages,
], { env })
} else {
console.log(`→ Installing ${HERMES_PACKAGE} via pip`)
r = spawnSync(pyBin, [
console.log(`→ Installing ${label} via pip: ${packages.join(' ')}`)
run(pyBin, [
'-m', 'pip', 'install',
HERMES_PACKAGE,
...packages,
'--no-warn-script-location',
'--disable-pip-version-check',
], { stdio: 'inherit' })
], { env })
}
}
if (r.status !== 0) process.exit(r.status ?? 1)
r = spawnSync(pyBin, [
'-c',
'import mcp; import tools.mcp_tool as t; assert t._MCP_AVAILABLE',
], { stdio: 'inherit' })
if (r.status !== 0) {
console.error('MCP Python SDK sanity check failed')
process.exit(r.status ?? 1)
function npmCommand() {
const bundled = TARGET_OS === 'win32'
? resolve(NODE_DIR, 'npm.cmd')
: resolve(NODE_DIR, 'bin', 'npm')
const candidates = TARGET_OS === 'win32'
? [bundled, 'npm.cmd', 'npm.exe', 'npm']
: [bundled, 'npm']
for (const candidate of candidates) {
if (candidate === bundled && !existsSync(candidate)) continue
const invocation = commandInvocation(candidate)
const result = optionalRunInvocation(invocation, ['--version'], { stdio: 'ignore', env: browserRuntimeEnv(false) })
if (result.status === 0) return invocation
}
return null
}
function agentBrowserCommand() {
if (TARGET_OS === 'win32') {
return resolve(NODE_PREFIX, 'agent-browser.cmd')
}
return resolve(NODE_PREFIX, 'bin', 'agent-browser')
}
function browserRuntimeEnv(includeAgentBrowser = true) {
const bundledNodeBin = TARGET_OS === 'win32'
? NODE_DIR
: resolve(NODE_DIR, 'bin')
const nodePath = TARGET_OS === 'win32'
? NODE_PREFIX
: resolve(NODE_PREFIX, 'bin')
const inheritedPath = process.env.PATH || process.env.Path || ''
const pathKey = TARGET_OS === 'win32' ? 'Path' : 'PATH'
const browserExecutable = includeAgentBrowser ? ensureBundledBrowserExecutable() : null
const pathEntries = includeAgentBrowser
? [nodePath, bundledNodeBin, inheritedPath]
: [bundledNodeBin, inheritedPath]
const env = {
...process.env,
[pathKey]: pathEntries.filter(Boolean).join(TARGET_OS === 'win32' ? ';' : ':'),
HERMES_AGENT_NODE: TARGET_OS === 'win32' ? resolve(NODE_DIR, 'node.exe') : resolve(NODE_DIR, 'bin', 'node'),
HERMES_AGENT_NODE_ROOT: NODE_DIR,
AGENT_BROWSER_HOME,
PLAYWRIGHT_BROWSERS_PATH,
}
if (browserExecutable) env.AGENT_BROWSER_EXECUTABLE_PATH = browserExecutable
return env
}
function bundledBrowserExecutableNames() {
if (TARGET_OS === 'win32') return new Set(['chrome.exe'])
if (TARGET_OS === 'darwin') return new Set(['Google Chrome for Testing', 'Google Chrome', 'Chromium', 'chrome'])
return new Set(['chrome', 'chromium', 'chromium-browser'])
}
function defaultAgentBrowserHomes() {
const candidates = [
process.env.USERPROFILE,
process.env.UserProfile,
process.env.HOME,
process.env.HOMEDRIVE && process.env.HOMEPATH
? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`
: null,
osHomedir(),
]
return Array.from(new Set(
candidates
.map(home => home?.trim())
.filter(Boolean)
.map(home => resolve(home, '.agent-browser')),
))
}
function findBrowserInstallInHome(home) {
const names = bundledBrowserExecutableNames()
const browsersDir = join(home, 'browsers')
const bundleDirs = []
if (existsSync(browsersDir)) {
try {
for (const entry of readdirSync(browsersDir, { withFileTypes: true })) {
if (entry.isDirectory()) bundleDirs.push(join(browsersDir, entry.name))
}
} catch {}
}
for (const bundleDir of bundleDirs) {
const executable = findBrowserExecutableUnder(bundleDir, names)
if (executable) return { executable, bundleDir }
}
return null
}
function findBrowserExecutableUnder(root, names) {
const stack = [root].filter(existsSync)
const visited = new Set()
while (stack.length > 0) {
const dir = stack.pop()
if (!dir || visited.has(dir)) continue
visited.add(dir)
let entries
try {
entries = readdirSync(dir, { withFileTypes: true })
} catch {
continue
}
for (const entry of entries) {
const path = join(dir, entry.name)
if (entry.isFile() && names.has(entry.name)) return path
if (entry.isDirectory()) stack.push(path)
}
}
return null
}
function findBundledBrowserExecutable() {
return findBrowserInstallInHome(AGENT_BROWSER_HOME)?.executable ?? null
}
function ensureBundledBrowserExecutable() {
const bundled = findBrowserInstallInHome(AGENT_BROWSER_HOME)
if (bundled) return bundled.executable
const searchedHomes = []
for (const fallbackHome of defaultAgentBrowserHomes()) {
if (fallbackHome === AGENT_BROWSER_HOME) continue
searchedHomes.push(fallbackHome)
const fallback = findBrowserInstallInHome(fallbackHome)
if (!fallback) continue
const targetBrowsersDir = join(AGENT_BROWSER_HOME, 'browsers')
const targetBundleDir = join(targetBrowsersDir, basename(fallback.bundleDir))
mkdirSync(targetBrowsersDir, { recursive: true })
cpSync(fallback.bundleDir, targetBundleDir, { recursive: true, force: true, verbatimSymlinks: true })
console.log(`✓ copied Chrome bundle into ${targetBundleDir}`)
return findBundledBrowserExecutable()
}
if (searchedHomes.length > 0) {
console.warn(`! no Chrome bundle found in fallback agent-browser homes: ${searchedHomes.join(', ')}`)
}
return null
}
function sitePackagesDir() {
if (TARGET_OS === 'win32') {
return resolve(PY_DIR, 'Lib', 'site-packages')
}
const libDir = resolve(PY_DIR, 'lib')
const py = readdirSync(libDir).find(n => /^python\d+\.\d+$/.test(n))
if (!py) throw new Error(`Could not locate pythonX.Y under ${libDir}`)
return resolve(libDir, py, 'site-packages')
}
function pythonModuleExists(moduleName) {
const result = optionalRun(pyBin, [
'-c',
`import importlib.util, sys; sys.exit(0 if importlib.util.find_spec(${JSON.stringify(moduleName)}) else 1)`,
], { stdio: 'ignore' })
return result.status === 0
}
function removeBrokenDashboardAuthPlugin() {
if (pythonModuleExists('hermes_cli.dashboard_auth')) return
const pluginDir = resolve(sitePackagesDir(), 'plugins', 'dashboard_auth', 'nous')
if (!existsSync(pluginDir)) return
rmSync(pluginDir, { recursive: true, force: true })
console.warn(
'! Removed bundled dashboard_auth/nous plugin because hermes_cli.dashboard_auth is missing from the hermes-agent package',
)
}
function installBrowserRuntime() {
if (SKIP_BROWSER_RUNTIME) {
console.warn('! Skipping bundled browser runtime because HERMES_SKIP_BROWSER_RUNTIME is set')
return
}
if (BROWSER_PACKAGES.length === 0) {
console.warn('! Skipping bundled browser runtime because HERMES_BROWSER_PACKAGES is empty')
return
}
const npm = npmCommand()
if (!npm) {
console.error('npm not found; bundled browser runtime requires Node.js/npm')
process.exit(1)
}
console.log(`→ Installing browser runtime via npm prefix ${NODE_PREFIX}`)
runInvocation(npm, [
'install',
'-g',
'--prefix',
NODE_PREFIX,
'--silent',
'--ignore-scripts',
...BROWSER_PACKAGES,
])
const ab = agentBrowserCommand()
if (!existsSync(ab)) {
console.error(`agent-browser binary not found at ${ab} after npm install`)
process.exit(1)
}
console.log(`→ Installing Chromium for bundled agent-browser at ${AGENT_BROWSER_HOME}`)
runInvocation(commandInvocation(ab), ['install'], { env: browserRuntimeEnv() })
const browserExecutable = ensureBundledBrowserExecutable()
if (!browserExecutable) {
console.error(`Bundled Chrome executable not found under ${AGENT_BROWSER_HOME} after agent-browser install`)
process.exit(1)
}
console.log(`✓ bundled Chrome executable available at ${browserExecutable}`)
}
installPythonPackages([HERMES_PACKAGE], 'hermes-agent')
installPythonPackages(EXTRA_PYTHON_PACKAGES, 'extra Python runtime packages')
removeBrokenDashboardAuthPlugin()
installBrowserRuntime()
run(pyBin, [
'-c',
[
'import importlib.util',
'import mcp',
'import tools.mcp_tool as t',
'assert t._MCP_AVAILABLE',
'assert importlib.util.find_spec("websockets") is not None',
].join('; '),
])
const hermesBin = TARGET_OS === 'win32'
? resolve(PY_DIR, 'Scripts', 'hermes.exe')
@@ -76,14 +398,11 @@ if (!existsSync(hermesBin)) {
// it at the venv root with a *relative* symlink so the venv stays portable when copied
// into the packaged .app/.exe (an absolute symlink would break the moment the bundle
// is moved to /Applications/...).
const { readdirSync, symlinkSync, copyFileSync, unlinkSync, lstatSync } = await import('node:fs')
function siteRunAgentRelative() {
if (TARGET_OS === 'win32') {
return ['Lib', 'site-packages', 'run_agent.py'].join('\\')
}
const libDir = resolve(PY_DIR, 'lib')
const py = readdirSync(libDir).find(n => /^python\d+\.\d+$/.test(n))
return ['lib', py, 'site-packages', 'run_agent.py'].join('/')
return `${sitePackagesDir().slice(PY_DIR.length + 1)}/run_agent.py`
}
{
const relSrc = siteRunAgentRelative()
@@ -102,7 +421,6 @@ function siteRunAgentRelative() {
// Relocate: replace the pip-generated launcher (which embeds an absolute
// shebang to the build-time Python path) with a relative wrapper so the
// bundled venv works after being moved into the .app/.exe payload.
const { writeFileSync, chmodSync } = await import('node:fs')
if (TARGET_OS === 'win32') {
// Windows: pip generates a .exe launcher that embeds a relative shebang
// already. Add a .cmd wrapper that prefers the colocated python.exe.
@@ -139,8 +457,23 @@ if (TARGET_OS === 'win32') {
console.log(`✓ hermes installed at ${hermesBin} (relocatable launcher)`)
r = spawnSync(hermesCheckCommand, hermesCheckArgs, { stdio: 'inherit' })
if (r.status !== 0) {
console.error('hermes --version failed')
process.exit(r.status ?? 1)
run(hermesCheckCommand, hermesCheckArgs)
if (!SKIP_BROWSER_RUNTIME) {
run(pyBin, [
'-c',
[
'import os, shutil',
`os.environ["PLAYWRIGHT_BROWSERS_PATH"] = ${JSON.stringify(PLAYWRIGHT_BROWSERS_PATH)}`,
'from tools.browser_tool import _chromium_installed',
'assert shutil.which("agent-browser") is not None',
'assert _chromium_installed()',
].join('; '),
], { env: browserRuntimeEnv() })
}
if (SKIP_BROWSER_RUNTIME) {
console.log('✓ hermes Python, MCP, and websockets checks passed; browser runtime skipped')
} else {
console.log('✓ hermes Python, MCP, websockets, agent-browser, and Chromium checks passed')
}
@@ -0,0 +1,121 @@
#!/usr/bin/env node
// Package prepared Python/Node/Git runtime resources into a release asset.
import {
cpSync,
createReadStream,
existsSync,
mkdirSync,
mkdtempSync,
rmSync,
statSync,
writeFileSync,
} from 'node:fs'
import { createHash } from 'node:crypto'
import { arch as osArch, platform as osPlatform, tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dirname, '..')
const TARGET_OS = process.env.TARGET_OS || osPlatform()
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
const PLATFORM = `${OS_LABEL}-${TARGET_ARCH}`
const OUT_DIR = resolve(ROOT, 'release', 'runtime')
const PY_DIR = resolve(ROOT, 'resources', 'python', PLATFORM)
const NODE_DIR = resolve(ROOT, 'resources', 'node', PLATFORM)
const GIT_DIR = resolve(ROOT, 'resources', 'git', PLATFORM)
const pyBin = TARGET_OS === 'win32'
? resolve(PY_DIR, 'python.exe')
: resolve(PY_DIR, 'bin', 'python3')
function run(command, args, options = {}) {
const result = spawnSync(command, args, { stdio: 'inherit', ...options })
if (result.status !== 0) process.exit(result.status ?? 1)
return result
}
function output(command, args) {
const result = spawnSync(command, args, { encoding: 'utf-8' })
if (result.status !== 0) {
process.stderr.write(result.stderr || result.stdout || '')
process.exit(result.status ?? 1)
}
return result.stdout.trim()
}
async function sha256File(file) {
const hash = createHash('sha256')
await new Promise((resolvePromise, rejectPromise) => {
const stream = createReadStream(file)
stream.on('data', chunk => hash.update(chunk))
stream.on('end', resolvePromise)
stream.on('error', rejectPromise)
})
return hash.digest('hex')
}
for (const dir of [PY_DIR, NODE_DIR]) {
if (!existsSync(dir)) {
console.error(`Runtime directory missing: ${dir}`)
process.exit(1)
}
}
const hermesAgentVersion = output(pyBin, [
'-c',
'import importlib.metadata as m; print(m.version("hermes-agent"))',
])
const assetName = `hermes-runtime-hermes-agent-${hermesAgentVersion}-${PLATFORM}.tar.gz`
const manifestName = `hermes-runtime-${PLATFORM}.json`
mkdirSync(OUT_DIR, { recursive: true })
const stage = mkdtempSync(join(tmpdir(), `hermes-runtime-${PLATFORM}-`))
try {
cpSync(PY_DIR, join(stage, 'python'), { recursive: true, force: true, verbatimSymlinks: true })
cpSync(NODE_DIR, join(stage, 'node'), { recursive: true, force: true, verbatimSymlinks: true })
if (existsSync(GIT_DIR)) {
cpSync(GIT_DIR, join(stage, 'git'), { recursive: true, force: true, verbatimSymlinks: true })
} else {
mkdirSync(join(stage, 'git'), { recursive: true })
writeFileSync(join(stage, 'git', '.placeholder'), 'Git for Windows is only bundled on Windows.\n')
}
const runtimeManifest = {
schema: 1,
platform: PLATFORM,
targetOs: TARGET_OS,
targetArch: TARGET_ARCH,
hermesAgentVersion,
asset: {
name: assetName,
},
}
writeFileSync(join(stage, 'runtime-manifest.json'), JSON.stringify(runtimeManifest, null, 2) + '\n')
const assetPath = resolve(OUT_DIR, assetName)
rmSync(assetPath, { force: true })
run('tar', ['-czf', assetPath, '-C', stage, '.'])
const sha256 = await sha256File(assetPath)
writeFileSync(`${assetPath}.sha256`, `${sha256} ${assetName}\n`)
const platformManifest = {
...runtimeManifest,
createdAt: new Date().toISOString(),
asset: {
name: assetName,
sha256,
size: statSync(assetPath).size,
},
}
writeFileSync(resolve(OUT_DIR, manifestName), JSON.stringify(platformManifest, null, 2) + '\n')
console.log(`Runtime asset: ${assetPath}`)
console.log(`Runtime manifest: ${resolve(OUT_DIR, manifestName)}`)
} finally {
rmSync(stage, { recursive: true, force: true })
}
@@ -0,0 +1,28 @@
#!/usr/bin/env node
import { arch as osArch, platform as osPlatform } from 'node:os'
import { hermesVersion, runtimeReleaseTag } from './runtime-config.mjs'
const TARGET_OS = process.env.TARGET_OS || osPlatform()
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
const HERMES_VERSION = hermesVersion()
const RUNTIME_RELEASE_TAG = runtimeReleaseTag()
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
if (!['win', 'mac', 'linux'].includes(OS_LABEL) || !['x64', 'arm64'].includes(TARGET_ARCH)) {
console.error(`Unsupported runtime target: ${TARGET_OS}-${TARGET_ARCH}`)
process.exit(1)
}
const platform = `${OS_LABEL}-${TARGET_ARCH}`
const asset = `hermes-runtime-hermes-agent-${HERMES_VERSION}-${platform}.tar.gz`
const manifest = `hermes-runtime-${platform}.json`
if (process.argv.includes('--manifest')) {
console.log(manifest)
} else if (process.argv.includes('--platform')) {
console.log(platform)
} else if (process.argv.includes('--release-tag')) {
console.log(RUNTIME_RELEASE_TAG)
} else {
console.log(asset)
}
@@ -0,0 +1,12 @@
export const DEFAULT_HERMES_VERSION = '0.15.2'
export function hermesVersion(env = process.env) {
return env.HERMES_VERSION || DEFAULT_HERMES_VERSION
}
export function runtimeReleaseTag(env = process.env) {
const version = hermesVersion(env)
return env.HERMES_DESKTOP_RUNTIME_RELEASE_TAG
|| env.RUNTIME_RELEASE_TAG
|| `hermes-${version}-runtime`
}
@@ -0,0 +1,14 @@
#!/usr/bin/env node
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { runtimeReleaseTag } from './runtime-config.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dirname, '..')
const outFile = resolve(ROOT, 'build', 'runtime-release.json')
const tag = runtimeReleaseTag()
mkdirSync(dirname(outFile), { recursive: true })
writeFileSync(outFile, JSON.stringify({ tag }, null, 2) + '\n')
console.log(`Runtime release metadata: ${tag}`)
@@ -0,0 +1 @@
export const HERMES_CLI_ARG = '--hermes-cli'
+232
View File
@@ -0,0 +1,232 @@
import { execFile } from 'node:child_process'
import {
appendFileSync,
chmodSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from 'node:fs'
import { homedir } from 'node:os'
import { delimiter, dirname, join, resolve } from 'node:path'
import { promisify } from 'node:util'
import { HERMES_CLI_ARG } from './cli-constants'
const execFileAsync = promisify(execFile)
const SHIM_MARKER = 'HERMES_STUDIO_CLI_SHIM'
const PATH_MARKER_START = '# >>> Hermes Studio CLI shim >>>'
const PATH_MARKER_END = '# <<< Hermes Studio CLI shim <<<'
type ShimInstallStatus = 'installed' | 'updated' | 'unchanged' | 'skipped'
export interface CliShimInstallResult {
shimPath: string
status: ShimInstallStatus
pathUpdated: boolean
reason?: string
}
interface CliShimInstallOptions {
env?: NodeJS.ProcessEnv
executablePath?: string
homeDir?: string
platform?: NodeJS.Platform
}
function platformDelimiter(platform: NodeJS.Platform): string {
return platform === 'win32' ? ';' : delimiter
}
function pathKey(value: string, platform: NodeJS.Platform): string {
const normalized = resolve(value)
return platform === 'win32' ? normalized.toLowerCase() : normalized
}
export function pathContainsDir(pathValue: string | undefined, binDir: string, platform: NodeJS.Platform = process.platform): boolean {
if (!pathValue) return false
const target = pathKey(binDir, platform)
return pathValue
.split(platformDelimiter(platform))
.map(entry => entry.trim())
.filter(Boolean)
.some(entry => pathKey(entry, platform) === target)
}
function executableForShim(options: Required<Pick<CliShimInstallOptions, 'env' | 'executablePath' | 'platform'>>): string {
const appImage = options.platform === 'linux' ? options.env.APPIMAGE?.trim() : ''
return appImage || options.executablePath
}
export function shimPathForPlatform(binDir: string, platform: NodeJS.Platform = process.platform): string {
return join(binDir, platform === 'win32' ? 'hermes-studio.cmd' : 'hermes-studio')
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`
}
export function createShimContent(executablePath: string, platform: NodeJS.Platform = process.platform): string {
if (platform === 'win32') {
return [
'@echo off',
`rem ${SHIM_MARKER}`,
`set "APP=${executablePath}"`,
'if not exist "%APP%" (',
' echo Hermes Studio executable not found at "%APP%" 1>&2',
' exit /b 127',
')',
'set ELECTRON_RUN_AS_NODE=',
`"${'%APP%'}" -- ${HERMES_CLI_ARG} %*`,
'exit /b %ERRORLEVEL%',
'',
].join('\r\n')
}
return [
'#!/bin/sh',
`# ${SHIM_MARKER}`,
`APP=${shellQuote(executablePath)}`,
'if [ ! -x "$APP" ]; then',
' echo "Hermes Studio executable not found at $APP" >&2',
' exit 127',
'fi',
'unset ELECTRON_RUN_AS_NODE',
`exec "$APP" -- ${HERMES_CLI_ARG} "$@"`,
'',
].join('\n')
}
function isManagedShim(content: string): boolean {
return content.includes(SHIM_MARKER) && content.includes(HERMES_CLI_ARG)
}
function writeShim(shimPath: string, content: string, platform: NodeJS.Platform): ShimInstallStatus {
if (existsSync(shimPath)) {
const existing = readFileSync(shimPath, 'utf-8')
if (existing === content) return 'unchanged'
if (!isManagedShim(existing)) return 'skipped'
writeFileSync(shimPath, content, 'utf-8')
if (platform !== 'win32') chmodSync(shimPath, 0o755)
return 'updated'
}
writeFileSync(shimPath, content, { encoding: 'utf-8', mode: platform === 'win32' ? 0o644 : 0o755 })
if (platform !== 'win32') chmodSync(shimPath, 0o755)
return 'installed'
}
function shellProfilePaths(homeDir: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string[] {
if (platform === 'win32') return []
const shell = env.SHELL?.trim() || ''
const name = shell.split('/').pop() || ''
if (name === 'fish') return [join(homeDir, '.config', 'fish', 'conf.d', 'hermes-studio.fish')]
if (name === 'bash') return [join(homeDir, '.bash_profile'), join(homeDir, '.bashrc')]
if (name === 'zsh' || platform === 'darwin') return [join(homeDir, '.zprofile'), join(homeDir, '.zshrc')]
return [join(homeDir, '.profile')]
}
function profileMentionsUserBin(content: string, homeDir: string): boolean {
return content.includes('$HOME/bin')
|| content.includes('~/bin')
|| content.includes(resolve(homeDir, 'bin'))
}
function shellPathSnippet(platform: NodeJS.Platform, profilePath: string): string {
if (platform !== 'win32' && profilePath.endsWith('.fish')) {
return [
'',
PATH_MARKER_START,
'fish_add_path -m "$HOME/bin"',
PATH_MARKER_END,
'',
].join('\n')
}
return [
'',
PATH_MARKER_START,
'case ":$PATH:" in',
' *":$HOME/bin:"*) ;;',
' *) export PATH="$HOME/bin:$PATH" ;;',
'esac',
PATH_MARKER_END,
'',
].join('\n')
}
async function ensureWindowsUserPath(binDir: string): Promise<boolean> {
let currentPath = ''
try {
const { stdout } = await execFileAsync('reg.exe', ['query', 'HKCU\\Environment', '/v', 'Path'], {
encoding: 'utf-8',
timeout: 1500,
windowsHide: true,
})
const line = stdout.split(/\r?\n/).find(row => /^\s*Path\s+REG_/.test(row))
if (line) currentPath = line.replace(/^\s*Path\s+REG_\w+\s+/, '').trim()
} catch {
currentPath = process.env.Path || process.env.PATH || ''
}
if (pathContainsDir(currentPath, binDir, 'win32')) return false
const separator = currentPath ? ';' : ''
await execFileAsync('reg.exe', ['add', 'HKCU\\Environment', '/v', 'Path', '/t', 'REG_EXPAND_SZ', '/d', `${binDir}${separator}${currentPath}`, '/f'], {
encoding: 'utf-8',
timeout: 1500,
windowsHide: true,
})
return true
}
function ensureUnixShellPath(homeDir: string, binDir: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): boolean {
if (pathContainsDir(env.PATH, binDir, platform)) return false
let updated = false
for (const profilePath of shellProfilePaths(homeDir, platform, env)) {
const existing = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : ''
if (existing.includes(PATH_MARKER_START) || profileMentionsUserBin(existing, homeDir)) continue
mkdirSync(dirname(profilePath), { recursive: true })
appendFileSync(profilePath, shellPathSnippet(platform, profilePath), 'utf-8')
updated = true
break
}
return updated
}
async function ensureUserBinOnPath(homeDir: string, binDir: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): Promise<boolean> {
if (platform === 'win32') {
return await ensureWindowsUserPath(binDir)
}
return ensureUnixShellPath(homeDir, binDir, platform, env)
}
export async function installHermesStudioCliShim(options: CliShimInstallOptions = {}): Promise<CliShimInstallResult> {
const platform = options.platform || process.platform
const env = options.env || process.env
const homeDir = options.homeDir || homedir()
const binDir = resolve(homeDir, 'bin')
const executablePath = executableForShim({
env,
executablePath: options.executablePath || process.execPath,
platform,
})
const shimPath = shimPathForPlatform(binDir, platform)
mkdirSync(binDir, { recursive: true })
const status = writeShim(shimPath, createShimContent(executablePath, platform), platform)
const pathUpdated = await ensureUserBinOnPath(homeDir, binDir, platform, env).catch((err) => {
console.warn(`[cli-shim] failed to update PATH: ${err instanceof Error ? err.message : String(err)}`)
return false
})
return {
shimPath,
status,
pathUpdated,
reason: status === 'skipped' ? 'existing hermes-studio shim is not managed by Hermes Studio' : undefined,
}
}
+96
View File
@@ -0,0 +1,96 @@
import { spawn } from 'node:child_process'
import { existsSync, mkdirSync } from 'node:fs'
import { delimiter, dirname, join } from 'node:path'
import {
bundledBrowserExecutable,
bundledGit,
bundledNode,
bundledPython,
gitPathDirs,
hermesBin,
hermesHome,
nodeBinDir,
pythonDir,
webUiHome,
} from './paths'
import { HERMES_CLI_ARG } from './cli-constants'
import { ensureDesktopRuntime } from './runtime-manager'
export function parseHermesCliArgs(argv: string[] = process.argv): string[] | null {
const index = argv.indexOf(HERMES_CLI_ARG)
if (index < 0) return null
return argv.slice(index + 1)
}
export async function runBundledHermesCli(args: string[]): Promise<number> {
try {
await ensureDesktopRuntime()
} catch (err) {
console.error(`Failed to prepare Hermes runtime: ${err instanceof Error ? err.message : String(err)}`)
return 1
}
const command = hermesBin()
if (!existsSync(command)) {
console.error(`hermes binary missing at ${command}`)
console.error('Run: npm run prepare:runtime (to build a local Hermes runtime)')
return 127
}
mkdirSync(webUiHome(), { recursive: true })
mkdirSync(hermesHome(), { recursive: true })
const binDir = dirname(command)
const bundledNodeBin = nodeBinDir()
const bundledAgentBrowserBin = process.platform === 'win32'
? join(pythonDir(), 'node')
: join(pythonDir(), 'node', 'bin')
const inheritedPath = process.env.PATH || process.env.Path || ''
const pathValue = [
binDir,
bundledAgentBrowserBin,
bundledNodeBin,
gitPathDirs().join(delimiter),
inheritedPath,
].filter(Boolean).join(delimiter)
const gitBin = bundledGit()
const browserExecutable = process.env.AGENT_BROWSER_EXECUTABLE_PATH?.trim() || bundledBrowserExecutable()
const env: NodeJS.ProcessEnv = {
...process.env,
HERMES_DESKTOP: 'true',
HERMES_BIN: command,
HERMES_AGENT_BRIDGE_PYTHON: bundledPython(),
HERMES_AGENT_CLI_PYTHON: bundledPython(),
HERMES_AGENT_ROOT: pythonDir(),
HERMES_AGENT_NODE: bundledNode(),
HERMES_AGENT_NODE_ROOT: process.platform === 'win32' ? bundledNodeBin : dirname(bundledNodeBin),
AGENT_BROWSER_HOME: process.env.AGENT_BROWSER_HOME?.trim() || join(hermesHome(), 'agent-browser'),
...(browserExecutable ? { AGENT_BROWSER_EXECUTABLE_PATH: browserExecutable } : {}),
PLAYWRIGHT_BROWSERS_PATH: process.env.PLAYWRIGHT_BROWSERS_PATH || join(pythonDir(), 'ms-playwright'),
...(gitBin ? { HERMES_AGENT_GIT: gitBin } : {}),
HERMES_HOME: hermesHome(),
HERMES_WEB_UI_HOME: webUiHome(),
HERMES_WEBUI_STATE_DIR: webUiHome(),
PATH: pathValue,
}
return await new Promise(resolve => {
const child = spawn(command, args, {
env,
stdio: 'inherit',
windowsHide: false,
})
child.once('error', (err) => {
console.error(`Failed to run bundled Hermes CLI: ${err.message}`)
resolve(1)
})
child.once('exit', (code, signal) => {
if (typeof code === 'number') {
resolve(code)
return
}
console.error(`Bundled Hermes CLI exited from signal ${signal || 'unknown'}`)
resolve(1)
})
})
}
+109 -5
View File
@@ -4,6 +4,9 @@ import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
import { desktopIcon, desktopTrayTemplateIcon, desktopWindowsTrayIcon, hermesBinExists, hermesBin } from './paths'
import { checkForDesktopUpdates, initAutoUpdater } from './updater'
import { t } from './desktop-i18n'
import { installHermesStudioCliShim } from './cli-shim'
import { parseHermesCliArgs, runBundledHermesCli } from './hermes-cli'
import { ensureDesktopRuntime, type RuntimeProgress } from './runtime-manager'
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
const START_HIDDEN = process.argv.includes('--hidden')
@@ -13,6 +16,7 @@ let mainWindow: BrowserWindow | null = null
let serverUrl: string | null = null
let tray: Tray | null = null
let isQuitting = false
let isBootstrapping = false
function showMainWindow() {
if (!mainWindow) {
@@ -166,25 +170,91 @@ function splashHtml(): string {
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Hermes Studio</title>
<style>
html,body{margin:0;height:100%;background:#1a1a1a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;}
.wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}
.wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:20px}
.dot{width:10px;height:10px;border-radius:50%;background:#888;animation:pulse 1.2s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
.row{display:flex;gap:8px}
.row .dot:nth-child(2){animation-delay:.2s}.row .dot:nth-child(3){animation-delay:.4s}
.label{font-size:14px;color:#999}
.label{font-size:14px;color:#b8b8b8}
.detail{min-height:18px;font-size:12px;color:#7f7f7f}
.progress{width:320px;height:6px;border-radius:999px;background:#2b2b2b;overflow:hidden}
.bar{width:0;height:100%;background:#d8d8d8;transition:width .18s ease}
h1{font-weight:500;margin:0;font-size:18px}
</style></head><body><div class="wrap">
<h1>Hermes Studio</h1>
<div class="row"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
<div class="label">Starting local services</div>
<div id="label" class="label">Starting local services...</div>
<div class="progress"><div id="bar" class="bar"></div></div>
<div id="detail" class="detail"></div>
</div></body></html>`
return 'data:text/html;charset=utf-8,' + encodeURIComponent(html)
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
const units = ['KB', 'MB', 'GB']
let value = bytes / 1024
let unit = units[0]
for (let i = 1; i < units.length && value >= 1024; i += 1) {
value /= 1024
unit = units[i]
}
return `${value.toFixed(value >= 100 ? 0 : 1)} ${unit}`
}
function updateSplash(progress: RuntimeProgress) {
if (!mainWindow || mainWindow.isDestroyed()) return
const label = progress.message
const percent = typeof progress.percent === 'number' ? Math.round(progress.percent) : null
let detail = ''
if (progress.receivedBytes && progress.totalBytes) {
detail = `${formatBytes(progress.receivedBytes)} / ${formatBytes(progress.totalBytes)}`
if (percent !== null) detail += ` (${percent}%)`
} else if (percent !== null) {
detail = `${percent}%`
}
mainWindow.webContents.executeJavaScript(`
{
const label = document.getElementById('label');
const detail = document.getElementById('detail');
const bar = document.getElementById('bar');
if (label) label.textContent = ${JSON.stringify(label)};
if (detail) detail.textContent = ${JSON.stringify(detail)};
if (bar) bar.style.width = ${JSON.stringify(percent === null ? '100%' : `${percent}%`)};
}
`).catch(() => undefined)
}
async function bootstrap() {
if (isBootstrapping) return
isBootstrapping = true
try {
await ensureDesktopRuntime(updateSplash)
} catch (err) {
console.error('Failed to prepare Hermes runtime:', err)
if (mainWindow) {
const msg = String(err instanceof Error ? err.message : err).replace(/[<>]/g, '')
mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(
`<html><body style="font-family:system-ui;padding:32px;background:#1a1a1a;color:#eee">
<h2>Failed to prepare Hermes runtime</h2><pre style="white-space:pre-wrap;color:#f88">${msg}</pre>
<button id="retry" style="margin-top:16px;padding:8px 14px;border:1px solid #555;border-radius:6px;background:#2b2b2b;color:#eee;cursor:pointer">Retry</button>
<script>
document.getElementById('retry')?.addEventListener('click', () => {
window.hermesDesktop?.retryBootstrap?.()
})
</script>
</body></html>`,
))
}
isBootstrapping = false
return
}
if (!hermesBinExists()) {
console.error(`hermes binary missing at ${hermesBin()}`)
console.error('Run: npm run prepare:python (to bundle Python + hermes-agent)')
console.error('Run: npm run prepare:runtime (to build a local Hermes runtime)')
}
try {
@@ -201,15 +271,28 @@ async function bootstrap() {
</body></html>`,
))
}
} finally {
isBootstrapping = false
}
}
ipcMain.handle('hermes-desktop:get-token', () => getToken())
ipcMain.handle('hermes-desktop:retry-bootstrap', async () => {
if (serverUrl) {
await mainWindow?.loadURL(serverUrl)
return
}
await mainWindow?.loadURL(splashHtml())
await bootstrap()
})
function runDesktopApp() {
const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
app.quit()
} else {
return
}
app.on('second-instance', (_event, argv) => {
if (argv.includes('--quit')) {
quitApp()
@@ -229,6 +312,15 @@ if (!gotLock) {
// visual clutter. macOS keeps a menu (system requirement) but Electron's
// default is fine there.
if (process.platform !== 'darwin') Menu.setApplicationMenu(null)
if (app.isPackaged) {
installHermesStudioCliShim().then(result => {
if (result.status === 'skipped') {
console.warn(`[cli-shim] ${result.reason}: ${result.shimPath}`)
}
}).catch(err => {
console.warn(`[cli-shim] failed to install hermes-studio command: ${err instanceof Error ? err.message : String(err)}`)
})
}
createTray()
createWindow()
bootstrap()
@@ -262,3 +354,15 @@ if (!gotLock) {
app.exit(0)
})
}
const hermesCliArgs = parseHermesCliArgs(process.argv)
if (hermesCliArgs) {
runBundledHermesCli(hermesCliArgs)
.then(code => app.exit(code))
.catch(err => {
console.error(`Failed to run bundled Hermes CLI: ${err instanceof Error ? err.message : String(err)}`)
app.exit(1)
})
} else {
runDesktopApp()
}
+124 -10
View File
@@ -1,5 +1,5 @@
import { app } from 'electron'
import { existsSync } from 'node:fs'
import { existsSync, readdirSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { homedir, platform, arch } from 'node:os'
@@ -23,17 +23,120 @@ export function webuiServerEntry(): string {
return join(webuiDir(), 'dist', 'server', 'index.js')
}
// Bundled Python directory.
export function runtimePlatformKey(): string {
return `${osLabel}-${archLabel}`
}
export function desktopRuntimeDir(): string {
const override = process.env.HERMES_DESKTOP_RUNTIME_DIR?.trim()
if (override) return resolve(override)
return join(webUiHome(), 'desktop-runtime', runtimePlatformKey())
}
function packagedResourceDir(name: string): string {
return resolve(process.resourcesPath, name)
}
// dev: packages/desktop/resources/python/<os>-<arch>
// prod: <resources>/python
// prod: <resources>/python when present, otherwise downloaded runtime cache.
export function pythonDir(): string {
if (app.isPackaged) return resolve(process.resourcesPath, 'python')
return resolve(app.getAppPath(), 'resources', 'python', `${osLabel}-${archLabel}`)
if (app.isPackaged) {
const packaged = packagedResourceDir('python')
return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'python')
}
return resolve(app.getAppPath(), 'resources', 'python', runtimePlatformKey())
}
export function nodeDir(): string {
if (app.isPackaged) {
const packaged = packagedResourceDir('node')
return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'node')
}
return resolve(app.getAppPath(), 'resources', 'node', runtimePlatformKey())
}
export function nodeBinDir(): string {
const dir = nodeDir()
return isWin ? dir : join(dir, 'bin')
}
export function bundledNode(): string {
return isWin ? join(nodeDir(), 'node.exe') : join(nodeBinDir(), 'node')
}
export function gitDir(): string {
if (app.isPackaged) {
const packaged = packagedResourceDir('git')
return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'git')
}
return resolve(app.getAppPath(), 'resources', 'git', runtimePlatformKey())
}
export function gitPathDirs(): string[] {
if (!isWin) return []
const dir = gitDir()
return [
join(dir, 'cmd'),
join(dir, 'mingw64', 'bin'),
join(dir, 'usr', 'bin'),
].filter(existsSync)
}
export function bundledGit(): string | undefined {
if (!isWin) return undefined
const git = join(gitDir(), 'cmd', 'git.exe')
return existsSync(git) ? git : undefined
}
export function bundledAgentBrowserHome(): string {
return join(pythonDir(), 'agent-browser')
}
function browserExecutableNames(): Set<string> {
if (isWin) return new Set(['chrome.exe'])
if (platform() === 'darwin') return new Set(['Google Chrome for Testing', 'Google Chrome', 'Chromium', 'chrome'])
return new Set(['chrome', 'chromium', 'chromium-browser'])
}
export function bundledBrowserExecutable(): string | undefined {
const names = browserExecutableNames()
const stack = [join(bundledAgentBrowserHome(), 'browsers'), bundledAgentBrowserHome()].filter(existsSync)
const visited = new Set<string>()
while (stack.length > 0) {
const dir = stack.pop()
if (!dir || visited.has(dir)) continue
visited.add(dir)
let entries
try {
entries = readdirSync(dir, { withFileTypes: true })
} catch {
continue
}
for (const entry of entries) {
const path = join(dir, entry.name)
if (entry.isFile() && names.has(entry.name)) return path
if (entry.isDirectory()) stack.push(path)
}
}
return undefined
}
export function pythonBinDir(): string {
const dir = pythonDir()
return isWin ? join(dir, 'Scripts') : join(dir, 'bin')
}
export function bundledPython(): string {
const dir = pythonDir()
return isWin ? join(dir, 'python.exe') : join(dir, 'bin', 'python3')
}
export function hermesBin(): string {
const dir = pythonDir()
return isWin ? join(dir, 'Scripts', 'hermes.exe') : join(dir, 'bin', 'hermes')
return isWin ? join(pythonBinDir(), 'hermes.exe') : join(pythonBinDir(), 'hermes')
}
export function hermesBinExists(): boolean {
@@ -63,12 +166,23 @@ export function hermesHome(): string {
const override = process.env.HERMES_HOME?.trim()
if (override) return resolve(override)
const defaultHome = resolve(homedir(), '.hermes')
if (isWin) {
const localAppData = process.env.LOCALAPPDATA?.trim() || process.env.APPDATA?.trim()
if (localAppData) return resolve(localAppData, 'hermes')
const candidates = [
process.env.LOCALAPPDATA,
process.env.APPDATA,
]
.map(value => value?.trim())
.filter((value): value is string => !!value)
.map(value => resolve(value, 'hermes'))
for (const candidate of candidates) {
if (existsSync(candidate)) return candidate
}
}
return resolve(homedir(), '.hermes')
return defaultHome
}
export function tokenFile(): string {
@@ -0,0 +1,319 @@
import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
createReadStream,
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
renameSync,
rmSync,
statSync,
writeFileSync,
} from 'node:fs'
import { get as httpGet } from 'node:http'
import { get as httpsGet } from 'node:https'
import { basename, dirname, join, relative } from 'node:path'
import { promisify } from 'node:util'
import { app } from 'electron'
import {
bundledGit,
bundledNode,
desktopRuntimeDir,
hermesBinExists,
runtimePlatformKey,
} from './paths'
const execFileAsync = promisify(execFile)
const DEFAULT_RUNTIME_BASE_URL = 'https://download.www.xinmi.cloud'
const RUNTIME_MANIFEST_NAME = 'runtime-manifest.json'
const PACKAGED_RUNTIME_RELEASE_NAME = 'runtime-release.json'
type RuntimeManifest = {
schema: number
platform: string
hermesAgentVersion?: string
asset?: {
name: string
url?: string
sha256?: string
size?: number
}
}
type RuntimeDescriptor = {
name: string
url: string
sha256?: string
hermesAgentVersion?: string
}
export type RuntimeProgress = {
stage: 'resolve' | 'download' | 'verify' | 'extract' | 'ready'
message: string
percent?: number
receivedBytes?: number
totalBytes?: number
}
type RuntimeProgressHandler = (progress: RuntimeProgress) => void
function requiredRuntimeFiles(root: string): string[] {
const pythonBin = process.platform === 'win32'
? join(root, 'python', 'python.exe')
: join(root, 'python', 'bin', 'python3')
const hermesBin = process.platform === 'win32'
? join(root, 'python', 'Scripts', 'hermes.exe')
: join(root, 'python', 'bin', 'hermes')
const nodeBin = process.platform === 'win32'
? join(root, 'node', 'node.exe')
: join(root, 'node', 'bin', 'node')
const files = [pythonBin, hermesBin, nodeBin, join(root, RUNTIME_MANIFEST_NAME)]
if (process.platform === 'win32') files.push(join(root, 'git', 'cmd', 'git.exe'))
return files
}
function missingRuntimeFiles(root: string): string[] {
return requiredRuntimeFiles(root).filter(file => !existsSync(file))
}
function runtimeReady(): boolean {
const gitReady = process.platform !== 'win32' || !!bundledGit()
return hermesBinExists() && existsSync(bundledNode()) && gitReady
}
function releaseTagCandidates(): string[] {
const override = process.env.HERMES_DESKTOP_RUNTIME_RELEASE_TAG?.trim()
if (override) return [override]
const version = app.getVersion()
const candidates = [packagedRuntimeReleaseTag(), version, `v${version}`, 'latest']
return Array.from(new Set(candidates.filter((tag): tag is string => typeof tag === 'string' && tag.length > 0)))
}
function packagedRuntimeReleaseTag(): string | null {
const candidates = app.isPackaged
? [join(process.resourcesPath, 'build', PACKAGED_RUNTIME_RELEASE_NAME)]
: [join(app.getAppPath(), 'build', PACKAGED_RUNTIME_RELEASE_NAME)]
for (const candidate of candidates) {
if (!existsSync(candidate)) continue
try {
const metadata = JSON.parse(readFileSync(candidate, 'utf-8')) as { tag?: unknown }
if (typeof metadata.tag === 'string' && metadata.tag.trim()) return metadata.tag.trim()
} catch {}
}
return null
}
function runtimeAssetUrl(assetName: string, tag: string): string {
const repo = process.env.HERMES_DESKTOP_RUNTIME_REPO?.trim()
if (repo) {
if (tag === 'latest') {
return `https://www.xinmi.cloud/${repo}/releases/latest/download/${encodeURIComponent(assetName)}`
}
return `https://www.xinmi.cloud/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(assetName)}`
}
const template = process.env.HERMES_DESKTOP_RUNTIME_BASE_URL?.trim() || DEFAULT_RUNTIME_BASE_URL
if (template.includes('{asset}') || template.includes('{tag}')) {
return template
.replace(/\{asset\}/g, encodeURIComponent(assetName))
.replace(/\{tag\}/g, encodeURIComponent(tag))
}
return `${template.replace(/\/$/, '')}/${encodeURIComponent(tag)}/${encodeURIComponent(assetName)}`
}
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`GET ${url} returned ${response.status}`)
}
return await response.json() as T
}
async function resolveRuntimeDescriptor(): Promise<RuntimeDescriptor> {
const directUrl = process.env.HERMES_DESKTOP_RUNTIME_URL?.trim()
if (directUrl) {
return { name: basename(new URL(directUrl).pathname) || 'hermes-runtime.tar.gz', url: directUrl }
}
const platformManifestName = `hermes-runtime-${runtimePlatformKey()}.json`
const manifestOverride = process.env.HERMES_DESKTOP_RUNTIME_MANIFEST_URL?.trim()
const candidates = manifestOverride
? [{ tag: '', url: manifestOverride }]
: releaseTagCandidates().map(tag => ({ tag, url: runtimeAssetUrl(platformManifestName, tag) }))
let lastError: Error | null = null
for (const candidate of candidates) {
try {
const manifest = await fetchJson<RuntimeManifest>(candidate.url)
if (!manifest.asset?.name) {
throw new Error(`runtime manifest is missing asset.name: ${candidate.url}`)
}
return {
name: manifest.asset.name,
url: manifest.asset.url || runtimeAssetUrl(manifest.asset.name, candidate.tag),
sha256: manifest.asset.sha256,
hermesAgentVersion: manifest.hermesAgentVersion,
}
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err))
}
}
throw lastError || new Error('Unable to resolve Hermes desktop runtime package')
}
function readCachedRuntimeManifest(root: string): RuntimeManifest | null {
const file = join(root, RUNTIME_MANIFEST_NAME)
if (!existsSync(file)) return null
try {
return JSON.parse(readFileSync(file, 'utf-8')) as RuntimeManifest
} catch {
return null
}
}
function cachedRuntimeMatches(root: string, descriptor: RuntimeDescriptor): boolean {
if (!runtimeReady()) return false
const manifest = readCachedRuntimeManifest(root)
if (!manifest?.asset?.name) return true
return manifest.asset.name === descriptor.name
}
function downloadFile(
url: string,
target: string,
onProgress?: RuntimeProgressHandler,
redirects = 5,
): Promise<void> {
return new Promise((resolve, reject) => {
const parsed = new URL(url)
const getter = parsed.protocol === 'http:' ? httpGet : httpsGet
const req = getter(parsed, response => {
const status = response.statusCode || 0
const location = response.headers.location
if (status >= 300 && status < 400 && location && redirects > 0) {
response.resume()
downloadFile(new URL(location, url).toString(), target, onProgress, redirects - 1).then(resolve, reject)
return
}
if (status < 200 || status >= 300) {
response.resume()
reject(new Error(`GET ${url} returned ${status}`))
return
}
const totalBytes = Number(response.headers['content-length']) || undefined
let receivedBytes = 0
response.on('data', chunk => {
receivedBytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk)
onProgress?.({
stage: 'download',
message: 'Downloading Hermes runtime...',
percent: totalBytes ? Math.min(100, (receivedBytes / totalBytes) * 100) : undefined,
receivedBytes,
totalBytes,
})
})
const file = createWriteStream(target)
response.pipe(file)
file.on('finish', () => file.close(() => resolve()))
file.on('error', reject)
})
req.on('error', reject)
})
}
async function sha256File(file: string): Promise<string> {
const hash = createHash('sha256')
await new Promise<void>((resolve, reject) => {
const stream = createReadStream(file)
stream.on('data', chunk => hash.update(chunk))
stream.on('end', resolve)
stream.on('error', reject)
})
return hash.digest('hex')
}
async function extractRuntimeArchive(archive: string, targetRoot: string): Promise<void> {
const parent = dirname(targetRoot)
const tempRoot = join(parent, `.runtime-${process.pid}-${Date.now()}`)
rmSync(tempRoot, { recursive: true, force: true })
mkdirSync(tempRoot, { recursive: true })
try {
await execFileAsync(process.platform === 'win32' ? 'tar.exe' : 'tar', ['-xzf', archive, '-C', tempRoot], {
windowsHide: true,
})
const missing = missingRuntimeFiles(tempRoot)
if (missing.length > 0) {
throw new Error(`Runtime archive is missing required files: ${missing.map(file => relative(tempRoot, file)).join(', ')}`)
}
rmSync(targetRoot, { recursive: true, force: true })
mkdirSync(parent, { recursive: true })
renameSync(tempRoot, targetRoot)
} catch (err) {
rmSync(tempRoot, { recursive: true, force: true })
throw err
}
}
export async function ensureDesktopRuntime(onProgress?: RuntimeProgressHandler): Promise<void> {
const runtimeRoot = desktopRuntimeDir()
mkdirSync(runtimeRoot, { recursive: true })
let descriptor: RuntimeDescriptor
try {
onProgress?.({ stage: 'resolve', message: 'Checking Hermes runtime...' })
descriptor = await resolveRuntimeDescriptor()
} catch (err) {
if (runtimeReady() && !process.env.HERMES_DESKTOP_RUNTIME_FORCE_UPDATE) {
console.warn(`[runtime] using cached Hermes runtime because update check failed: ${err instanceof Error ? err.message : String(err)}`)
return
}
throw err
}
if (cachedRuntimeMatches(runtimeRoot, descriptor) && !process.env.HERMES_DESKTOP_RUNTIME_FORCE_UPDATE) return
const archive = join(dirname(runtimeRoot), `${descriptor.name}.download`)
console.log(`[runtime] downloading Hermes runtime ${descriptor.name}`)
onProgress?.({ stage: 'download', message: `Downloading ${descriptor.name}...` })
let archiveSize = 0
try {
await downloadFile(descriptor.url, archive, onProgress)
archiveSize = statSync(archive).size
if (descriptor.sha256) {
onProgress?.({ stage: 'verify', message: 'Verifying Hermes runtime...' })
const actual = await sha256File(archive)
if (actual !== descriptor.sha256) {
throw new Error(`Runtime checksum mismatch for ${descriptor.name}`)
}
}
onProgress?.({ stage: 'extract', message: 'Extracting Hermes runtime...' })
await extractRuntimeArchive(archive, runtimeRoot)
} finally {
rmSync(archive, { force: true })
}
const manifestPath = join(runtimeRoot, RUNTIME_MANIFEST_NAME)
if (!existsSync(manifestPath)) {
writeFileSync(manifestPath, JSON.stringify({
schema: 1,
platform: runtimePlatformKey(),
hermesAgentVersion: descriptor.hermesAgentVersion,
asset: {
name: descriptor.name,
sha256: descriptor.sha256,
size: archiveSize,
},
}, null, 2))
}
onProgress?.({ stage: 'ready', message: 'Hermes runtime ready.' })
console.log(`[runtime] Hermes runtime ready at ${runtimeRoot}`)
}
+2 -2
View File
@@ -6,8 +6,8 @@ let initialized = false
let checking = false
let updateDownloaded = false
const LATEST_RELEASE_URL = 'https://api.github.com/repos/EKKOLearnAI/hermes-web-ui/releases/latest'
const CLOUDFLARE_DOWNLOAD_BASE_URL = 'https://download.ekkolearnai.com'
const LATEST_RELEASE_URL = 'https://api.www.xinmi.cloud/repos/EKKOLearnAI/hermes-web-ui/releases/latest'
const CLOUDFLARE_DOWNLOAD_BASE_URL = 'https://download.www.xinmi.cloud'
interface GitHubRelease {
tag_name?: string
+104 -5
View File
@@ -6,10 +6,25 @@ import { dirname, delimiter, join } from 'node:path'
import { randomBytes } from 'node:crypto'
import { promisify } from 'node:util'
import { app } from 'electron'
import { webuiServerEntry, webuiDir, hermesBin, webUiHome, hermesHome, tokenFile, pythonDir } from './paths'
import {
bundledBrowserExecutable,
bundledGit,
bundledNode,
gitPathDirs,
webuiServerEntry,
webuiDir,
hermesBin,
webUiHome,
hermesHome,
nodeBinDir,
tokenFile,
pythonDir,
} from './paths'
const DEFAULT_PORT = 8748
const DEFAULT_READY_TIMEOUT_MS = 30_000
const DEFAULT_READY_TIMEOUT_MS = 120_000
const AGENT_BRIDGE_STARTED_MARKER = '[bootstrap] agent bridge started'
const AGENT_BRIDGE_FAILED_MARKER = '[bootstrap] agent bridge failed to start'
const execFileAsync = promisify(execFile)
let serverProc: ChildProcess | null = null
@@ -47,6 +62,60 @@ function readyTimeoutMs(): number {
return envPositiveInt('HERMES_DESKTOP_READY_TIMEOUT_MS') || DEFAULT_READY_TIMEOUT_MS
}
function createAgentBridgeStartupTracker(): {
observe: (chunk: Buffer) => void
wait: (timeoutMs: number) => Promise<void>
} {
let output = ''
let state: 'pending' | 'started' | 'failed' = 'pending'
let resolveReady: (() => void) | null = null
let rejectReady: ((err: Error) => void) | null = null
const settle = (nextState: 'started' | 'failed') => {
if (state !== 'pending') return
state = nextState
if (nextState === 'started') {
resolveReady?.()
} else {
rejectReady?.(new Error('Agent bridge failed to start'))
}
}
const observe = (chunk: Buffer) => {
if (state !== 'pending') return
output = (output + chunk.toString('utf-8')).slice(-4096)
if (output.includes(AGENT_BRIDGE_STARTED_MARKER)) {
settle('started')
} else if (output.includes(AGENT_BRIDGE_FAILED_MARKER)) {
settle('failed')
}
}
const wait = (timeoutMs: number) => {
if (state === 'started') return Promise.resolve()
if (state === 'failed') return Promise.reject(new Error('Agent bridge failed to start'))
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
if (state !== 'pending') return
state = 'failed'
reject(new Error(`Agent bridge did not become ready within ${timeoutMs}ms`))
}, timeoutMs)
resolveReady = () => {
clearTimeout(timer)
resolve()
}
rejectReady = (err) => {
clearTimeout(timer)
reject(err)
}
})
}
return { observe, wait }
}
function ensureToken(): string {
if (cachedToken) return cachedToken
const file = tokenFile()
@@ -231,17 +300,28 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
const bundledPython = isWin
? join(pythonDir(), 'python.exe')
: join(pythonDir(), 'bin', 'python3')
const bundledAgentBrowserBin = isWin
? join(pythonDir(), 'node')
: join(pythonDir(), 'node', 'bin')
const bundledNodeBin = nodeBinDir()
const bundledGitPath = gitPathDirs().join(delimiter)
const bridgePort = await getFreeTcpPort()
const workerPortBase = await getFreeTcpPortInRange(20000, 59000)
const loginShellPath = await getLoginShellPath()
const nvmNodeBinPaths = getNvmNodeBinPaths()
const runtimePath = mergePathEntries(
dirname(hermesBin()),
bundledAgentBrowserBin,
bundledNodeBin,
bundledGitPath,
loginShellPath,
nvmNodeBinPaths,
process.env.PATH,
process.env.Path,
COMMON_USER_BIN_DIRS.join(delimiter),
)
const browserExecutable = process.env.AGENT_BROWSER_EXECUTABLE_PATH?.trim() || bundledBrowserExecutable()
const gitBin = bundledGit()
// Run via Electron's "run as Node" mode — Electron binary doubles as Node.
const env: NodeJS.ProcessEnv = {
@@ -256,11 +336,21 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
HERMES_AGENT_BRIDGE_PYTHON: bundledPython,
HERMES_AGENT_CLI_PYTHON: bundledPython,
HERMES_AGENT_ROOT: pythonDir(),
HERMES_AGENT_NODE: bundledNode(),
HERMES_AGENT_NODE_ROOT: isWin ? bundledNodeBin : dirname(bundledNodeBin),
AGENT_BROWSER_HOME: process.env.AGENT_BROWSER_HOME?.trim() || join(agentHome, 'agent-browser'),
...(browserExecutable ? { AGENT_BROWSER_EXECUTABLE_PATH: browserExecutable } : {}),
PLAYWRIGHT_BROWSERS_PATH: process.env.PLAYWRIGHT_BROWSERS_PATH || join(pythonDir(), 'ms-playwright'),
...(gitBin ? { HERMES_AGENT_GIT: gitBin } : {}),
// Force TCP loopback for the agent bridge. The default `ipc:///tmp/...`
// unix socket is rejected on macOS in some EDR/sandbox setups (silent
// SIGKILL of the bridge child within ~150ms). TCP on 127.0.0.1 works
// identically and avoids the issue cross-platform.
HERMES_AGENT_BRIDGE_ENDPOINT: `tcp://127.0.0.1:${bridgePort}`,
// Desktop opens the UI as soon as the Web UI HTTP server is ready, while
// the Python bridge starts in the background. Let the first chat/context
// request wait for broker readiness instead of failing during cold start.
HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS: process.env.HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS ?? '120000',
// Force TCP for worker endpoints too (upstream #1106). Same EDR/sandbox
// reason as above — default ipc:// unix sockets in /tmp get killed.
HERMES_AGENT_BRIDGE_WORKER_TRANSPORT: 'tcp',
@@ -278,8 +368,9 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
// HERMES_HOME/.env or by configuring per-platform allowlists.
GATEWAY_ALLOW_ALL_USERS: process.env.GATEWAY_ALLOW_ALL_USERS ?? 'true',
// Keep the bundled Hermes Agent, bridge, gateway, and Web UI path helpers
// on the same data directory. Native Windows uses %LOCALAPPDATA%\hermes;
// macOS/Linux keep the standard ~/.hermes layout.
// on the same data directory. Native Windows uses an existing
// %LOCALAPPDATA%\hermes or %APPDATA%\hermes; otherwise all platforms keep
// the standard ~/.hermes layout.
HERMES_HOME: agentHome,
HERMES_WEB_UI_HOME: home,
HERMES_WEBUI_STATE_DIR: home,
@@ -295,10 +386,14 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
windowsHide: true,
})
const bridgeStartup = createAgentBridgeStartupTracker()
serverProc.stdout?.on('data', (chunk: Buffer) => {
bridgeStartup.observe(chunk)
process.stdout.write(`[webui] ${chunk}`)
})
serverProc.stderr?.on('data', (chunk: Buffer) => {
bridgeStartup.observe(chunk)
process.stderr.write(`[webui] ${chunk}`)
})
serverProc.on('exit', (code, signal) => {
@@ -309,7 +404,11 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
}
})
await waitForReady(port, readyTimeoutMs())
const timeoutMs = readyTimeoutMs()
void bridgeStartup.wait(timeoutMs).catch(err => {
console.warn(`[webui] agent bridge was not ready during startup: ${err instanceof Error ? err.message : String(err)}`)
})
await waitForReady(port, timeoutMs)
return getServerUrl(port)
}
+1
View File
@@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('hermesDesktop', {
getToken: (): Promise<string> => ipcRenderer.invoke('hermes-desktop:get-token'),
retryBootstrap: (): Promise<void> => ipcRenderer.invoke('hermes-desktop:retry-bootstrap'),
platform: process.platform,
isDesktop: true,
})
@@ -23,6 +23,7 @@ function appendPinoContext(message: string, obj: any): string {
parts.push(`profile=${obj.profile}`)
}
if (obj.request?.action) parts.push(`action=${obj.request.action}`)
if (obj.err?.message) parts.push(`error=${obj.err.message}`)
if (obj.sessionId) parts.push(`session=${obj.sessionId}`)
if (obj.runId) parts.push(`run=${obj.runId}`)
if (obj.status) parts.push(`status=${obj.status}`)
+3 -3
View File
@@ -107,7 +107,7 @@ function normalizeGithubRepoUrl(raw: string): string {
return raw
.trim()
.replace(/^git\+/, '')
.replace(/^git@github\.com:/, 'https://github.com/')
.replace(/^git@github\.com:/, 'https://www.xinmi.cloud/')
.replace(/\.git$/, '')
}
@@ -127,7 +127,7 @@ function getPreviewRepoApiUrl(): string {
const baseUrl = getPreviewRepoBaseUrl()
const match = baseUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/)
if (!match) throw new Error(`Preview zip fallback only supports GitHub repositories: ${baseUrl}`)
return `https://api.github.com/repos/${match[1]}/${match[2]}`
return `https://api.www.xinmi.cloud/repos/${match[1]}/${match[2]}`
}
function getPreviewGithubRepoParts(): { owner: string; repo: string } {
@@ -902,7 +902,7 @@ async function downloadGithubZip(ref: string, targetDir: string, type: 'tag' | '
const { owner, repo } = getPreviewGithubRepoParts()
const refKind = type === 'branch' ? 'heads' : 'tags'
const archiveKind = process.platform === 'win32' ? 'zip' : 'tar.gz'
const url = `https://codeload.github.com/${owner}/${repo}/${archiveKind}/refs/${refKind}/${encodeURIComponent(ref)}`
const url = `https://codeload.www.xinmi.cloud/${owner}/${repo}/${archiveKind}/refs/${refKind}/${encodeURIComponent(ref)}`
appendPreviewActionLog(`download archive: ${url}`)
const res = await fetch(url, {
headers: { 'User-Agent': 'hermes-web-ui-preview' },
@@ -10,7 +10,7 @@ const DEFAULT_AGENT_BRIDGE_STARTUP_TIMEOUT_MS = 120000
const DEFAULT_AGENT_BRIDGE_RESTART_DELAY_MS = 1000
const MAX_AGENT_BRIDGE_RESTART_DELAY_MS = 30000
const OPENROUTER_WEB_UI_ATTRIBUTION_ENV = {
HERMES_OPENROUTER_APP_REFERER: 'https://ekkolearnai.com',
HERMES_OPENROUTER_APP_REFERER: 'https://www.xinmi.cloud',
HERMES_OPENROUTER_APP_TITLE: 'Hermes Web UI',
HERMES_OPENROUTER_APP_CATEGORIES: 'cli-agent,personal-agent',
} as const
@@ -3,16 +3,16 @@
*
* Mirrors the upstream hermes-agent implementation
* (`hermes_cli/copilot_auth.py:155-275`):
* - POST https://github.com/login/device/code → device_code, user_code, verification_uri
* - POST https://github.com/login/oauth/access_token → access_token (after user approves)
* - POST https://www.xinmi.cloud/login/device/code → device_code, user_code, verification_uri
* - POST https://www.xinmi.cloud/login/oauth/access_token → access_token (after user approves)
* - Polling rules per RFC 8628: authorization_pending, slow_down, expired_token, access_denied
*
* Client ID `Ov23li8tweQw6odWQebz` is reused from upstream hermes-agent for now;
* a dedicated web-ui OAuth App can be registered later without changing the protocol.
*/
const GITHUB_DEVICE_CODE_URL = 'https://github.com/login/device/code'
const GITHUB_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'
const GITHUB_DEVICE_CODE_URL = 'https://www.xinmi.cloud/login/device/code'
const GITHUB_ACCESS_TOKEN_URL = 'https://www.xinmi.cloud/login/oauth/access_token'
export const COPILOT_OAUTH_CLIENT_ID = 'Ov23li8tweQw6odWQebz'
export const COPILOT_OAUTH_SCOPE = 'read:user'
const FETCH_TIMEOUT_MS = 15_000
@@ -6,7 +6,7 @@ import { join } from 'path'
const execFileAsync = promisify(execFile)
const COPILOT_API_TOKEN_URL = 'https://api.github.com/copilot_internal/v2/token'
const COPILOT_API_TOKEN_URL = 'https://api.www.xinmi.cloud/copilot_internal/v2/token'
const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models'
const EDITOR_VERSION = 'vscode/1.104.1'
const PLUGIN_VERSION = 'copilot-chat/0.20.0'
@@ -2,11 +2,12 @@
* Hermes -
*
* Hermes
* - Windows : %LOCALAPPDATA%\hermes
* - Windows : %LOCALAPPDATA%\hermes when it exists
* - Linux/macOS/WSL2: ~/.hermes
* - 用户自定义: HERMES_HOME
*/
import { existsSync } from 'fs'
import { basename, dirname, isAbsolute, relative, resolve, join } from 'path'
import { homedir } from 'os'
@@ -15,7 +16,7 @@ import { homedir } from 'os'
*
*
* 1. HERMES_HOME
* 2. Windows: %LOCALAPPDATA%\hermes
* 2. Windows: existing %LOCALAPPDATA%\hermes or %APPDATA%\hermes
* 3. : ~/.hermesLinux/macOS/WSL2
*
* @returns Hermes
@@ -26,16 +27,25 @@ export function detectHermesHome(): string {
return resolve(process.env.HERMES_HOME)
}
// 2. Windows:直接使用 %LOCALAPPDATA%\hermes
const defaultHome = resolve(homedir(), '.hermes')
// 2. Windows:优先使用存在的原生安装数据目录;不存在时回退到 ~/.hermes。
if (process.platform === 'win32') {
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA
if (localAppData) {
return join(localAppData, 'hermes')
const candidates = [
process.env.LOCALAPPDATA,
process.env.APPDATA,
]
.map(value => value?.trim())
.filter((value): value is string => !!value)
.map(value => resolve(value, 'hermes'))
for (const candidate of candidates) {
if (existsSync(candidate)) return candidate
}
}
// 3. Linux/macOS~/.hermes
return resolve(homedir(), '.hermes')
return defaultHome
}
/**
@@ -137,7 +137,7 @@ onMounted(() => {
</button>
<a
class="btn-outline"
href="https://github.com/EKKOLearnAI/hermes-web-ui"
href="https://www.xinmi.cloud/root/Hermes-ui"
target="_blank"
rel="noopener"
>
@@ -15,10 +15,10 @@ const activeTab = ref<'desktop' | 'npm' | 'docker' | 'source'>('desktop')
const releaseVersion = __APP_VERSION__.replace(/^v/, '')
const releaseTag = `v${releaseVersion}`
const releaseBaseUrl = 'https://github.com/EKKOLearnAI/hermes-web-ui/releases'
const releaseBaseUrl = 'https://www.xinmi.cloud/root/Hermes-ui/releases'
const releaseUrl = `${releaseBaseUrl}/tag/${releaseTag}`
const githubDownloadUrl = `${releaseBaseUrl}/download/${releaseTag}`
const cloudflareDownloadUrl = `https://download.ekkolearnai.com/${releaseTag}`
const cloudflareDownloadUrl = `https://download.www.xinmi.cloud/${releaseTag}`
const desktopDownloads = computed(() =>
(tm('install.desktop.downloads') as DesktopDownload[]).map((item) => {
const assetName = `Hermes.Studio-${releaseVersion}-${item.assetSuffix}`
@@ -17,7 +17,7 @@ const chartSrc = computed(() => {
onMounted(async () => {
try {
const res = await fetch('https://api.github.com/repos/EKKOLearnAI/hermes-web-ui')
const res = await fetch('https://api.www.xinmi.cloud/repos/EKKOLearnAI/hermes-web-ui')
const data = await res.json()
stars.value = data.stargazers_count
} catch {}
@@ -32,7 +32,7 @@ onMounted(async () => {
<div class="star-badges reveal reveal-delay-1">
<a
class="star-btn"
href="https://github.com/EKKOLearnAI/hermes-web-ui"
href="https://www.xinmi.cloud/root/Hermes-ui"
target="_blank"
rel="noopener"
>
@@ -19,7 +19,7 @@ const { t } = useI18n()
<p class="footer-meta">{{ t('footer.license') }}</p>
<a
class="footer-github"
href="https://github.com/EKKOLearnAI/hermes-web-ui"
href="https://www.xinmi.cloud/root/Hermes-ui"
target="_blank"
rel="noopener"
>
@@ -39,7 +39,7 @@ function goHome() {
<a class="nav-link" @click.prevent="navigateTo('docs.getting-started')">{{ t('nav.docs') }}</a>
<a
class="nav-link"
href="https://github.com/EKKOLearnAI/hermes-web-ui"
href="https://www.xinmi.cloud/root/Hermes-ui"
target="_blank"
rel="noopener"
>
@@ -84,7 +84,7 @@ function goHome() {
<div class="mobile-menu-inner" @click.stop>
<a class="mobile-link" @click.prevent="navigateTo('landing')">{{ t('nav.home') }}</a>
<a class="mobile-link" @click.prevent="navigateTo('docs.getting-started')">{{ t('nav.docs') }}</a>
<a class="mobile-link" href="https://github.com/EKKOLearnAI/hermes-web-ui" target="_blank" rel="noopener">{{ t('nav.github') }}</a>
<a class="mobile-link" href="https://www.xinmi.cloud/root/Hermes-ui" target="_blank" rel="noopener">{{ t('nav.github') }}</a>
<div class="mobile-actions">
<button class="mobile-action-btn" @click="switchLocale">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="action-icon">
+1 -1
View File
@@ -156,7 +156,7 @@ export default {
},
source: {
title: 'From Source',
cmd1: 'git clone https://github.com/EKKOLearnAI/hermes-web-ui.git',
cmd1: 'git clone https://www.xinmi.cloud/root/Hermes-ui.git',
cmd2: 'cd hermes-web-ui && npm install && npm run dev',
},
prereq: 'Requires Node.js >= 23',
+1 -1
View File
@@ -156,7 +156,7 @@ export default {
},
source: {
title: '源码安装',
cmd1: 'git clone https://github.com/EKKOLearnAI/hermes-web-ui.git',
cmd1: 'git clone https://www.xinmi.cloud/root/Hermes-ui.git',
cmd2: 'cd hermes-web-ui && npm install && npm run dev',
},
prereq: '需要 Node.js >= 23',
+83
View File
@@ -117,7 +117,14 @@ if (!buildWorkflow.includes('npm run harness:check')) {
}
const desktopReleaseWorkflow = await readText('.github/workflows/desktop-release.yml')
const desktopRuntimeWorkflow = await readText('.github/workflows/desktop-runtime.yml')
const electronBuilderConfig = await readText('packages/desktop/electron-builder.yml')
const desktopPackageJson = await readText('packages/desktop/package.json')
const desktopInstallHermes = await readText('packages/desktop/scripts/install-hermes.mjs')
const desktopWebuiServer = await readText('packages/desktop/src/main/webui-server.ts')
const desktopRuntimeManager = await readText('packages/desktop/src/main/runtime-manager.ts')
const desktopPaths = await readText('packages/desktop/src/main/paths.ts')
const desktopRuntimeAssetName = await readText('packages/desktop/scripts/runtime-asset-name.mjs')
if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) {
fail('desktop-release.yml must upload matrix-specific artifact_files')
}
@@ -142,6 +149,82 @@ if (!desktopReleaseWorkflow.includes('fail_on_unmatched_files: true')) {
fail('desktop-release.yml must keep fail_on_unmatched_files: true')
}
for (const phrase of [
'resources/python/${os}-${arch}',
'resources/node/${os}-${arch}',
'resources/git/${os}-${arch}',
]) {
if (electronBuilderConfig.includes(phrase)) {
fail(`electron-builder.yml must not bundle desktop runtime resource: ${phrase}`)
}
}
for (const phrase of [
'"fetch:node"',
'"fetch:git"',
'"prepare:runtime"',
'"package:runtime"',
'"runtime:asset-name"',
]) {
if (!desktopPackageJson.includes(phrase)) {
fail(`packages/desktop/package.json must support runtime package publishing: ${phrase}`)
}
}
for (const phrase of [
'steps.check.outputs.missing',
'npm --prefix packages/desktop run prepare:runtime',
'npm --prefix packages/desktop run package:runtime',
]) {
if (!desktopRuntimeWorkflow.includes(phrase)) {
fail(`desktop-runtime.yml must build and publish missing runtime package assets: ${phrase}`)
}
}
if (!desktopRuntimeAssetName.includes('hermes-runtime-hermes-agent-')) {
fail('runtime asset naming must include hermes-agent version')
}
for (const phrase of [
'websockets',
'agent-browser@^0.26.0',
'AGENT_BROWSER_HOME',
'AGENT_BROWSER_EXECUTABLE_PATH',
'PLAYWRIGHT_BROWSERS_PATH',
'ms-playwright',
'removeBrokenDashboardAuthPlugin',
]) {
if (!desktopInstallHermes.includes(phrase)) {
fail(`install-hermes.mjs must bundle Hermes browser runtime support: ${phrase}`)
}
}
for (const phrase of [
'bundledNodeBin',
'HERMES_AGENT_NODE',
'HERMES_AGENT_GIT',
'PLAYWRIGHT_BROWSERS_PATH',
'ms-playwright',
]) {
if (!desktopWebuiServer.includes(phrase)) {
fail(`desktop webui server must expose bundled browser runtime: ${phrase}`)
}
}
for (const phrase of [
'HERMES_DESKTOP_RUNTIME_URL',
'HERMES_DESKTOP_RUNTIME_BASE_URL',
'runtime-manifest.json',
]) {
if (!desktopRuntimeManager.includes(phrase)) {
fail(`desktop runtime manager must support downloadable runtime packages: ${phrase}`)
}
}
if (!desktopPaths.includes('HERMES_DESKTOP_RUNTIME_DIR')) {
fail('desktop paths must allow HERMES_DESKTOP_RUNTIME_DIR override')
}
if (failures.length > 0) {
console.error('Harness check failed:')
for (const failure of failures) {
+4 -4
View File
@@ -45,7 +45,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-1',
user_code: 'ABCD-1234',
verification_url: 'https://github.com/login/device',
verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
})
@@ -62,7 +62,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-2',
user_code: 'WXYZ-9999',
verification_url: 'https://github.com/login/device',
verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
})
@@ -87,7 +87,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-3',
user_code: 'EXPI-RED!',
verification_url: 'https://github.com/login/device',
verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
})
@@ -116,7 +116,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-4',
user_code: 'NOPE',
verification_url: 'https://github.com/login/device',
verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
})
+64
View File
@@ -0,0 +1,64 @@
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
createShimContent,
installHermesStudioCliShim,
pathContainsDir,
shimPathForPlatform,
} from '../../packages/desktop/src/main/cli-shim'
let tempDirs: string[] = []
afterEach(() => {
for (const dir of tempDirs) {
rmSync(dir, { recursive: true, force: true })
}
tempDirs = []
})
function tempHome(): string {
const dir = mkdtempSync(join(tmpdir(), 'hermes-studio-shim-'))
tempDirs.push(dir)
return dir
}
describe('Hermes Studio CLI shim', () => {
it('quotes Unix app paths and forwards args through --hermes-cli', () => {
const content = createShimContent("/Applications/Hermes Studio's.app/Contents/MacOS/Hermes Studio", 'darwin')
expect(content).toContain("--hermes-cli")
expect(content).toContain("APP='/Applications/Hermes Studio'\\''s.app/Contents/MacOS/Hermes Studio'")
expect(content).toContain('unset ELECTRON_RUN_AS_NODE')
expect(content).toContain('exec "$APP" -- --hermes-cli "$@"')
})
it('clears Electron Node mode in Windows shims before launching the app', () => {
const content = createShimContent('C:\\Users\\Example\\AppData\\Local\\Programs\\Hermes Studio\\Hermes Studio.exe', 'win32')
expect(content).toContain('set ELECTRON_RUN_AS_NODE=')
expect(content).toContain('"%APP%" -- --hermes-cli %*')
})
it('detects user bin paths with platform-specific separators', () => {
expect(pathContainsDir('/usr/bin:/Users/example/bin', '/Users/example/bin', 'darwin')).toBe(true)
expect(pathContainsDir('C:\\Windows;C:\\Users\\Example\\bin', 'C:\\Users\\Example\\bin', 'win32')).toBe(true)
})
it('installs a managed Unix shim and adds ~/bin to a shell profile', async () => {
const homeDir = tempHome()
const result = await installHermesStudioCliShim({
homeDir,
platform: 'darwin',
executablePath: '/Applications/Hermes Studio.app/Contents/MacOS/Hermes Studio',
env: { PATH: '/usr/bin', SHELL: '/bin/zsh' },
})
expect(result.status).toBe('installed')
expect(result.pathUpdated).toBe(true)
expect(result.shimPath).toBe(shimPathForPlatform(join(homeDir, 'bin'), 'darwin'))
expect(readFileSync(result.shimPath, 'utf-8')).toContain('exec "$APP" -- --hermes-cli "$@"')
expect(readFileSync(join(homeDir, '.zprofile'), 'utf-8')).toContain('export PATH="$HOME/bin:$PATH"')
})
})
+10 -1
View File
@@ -97,7 +97,7 @@ describe('agent bridge manager command resolution', () => {
const { buildAgentBridgeProcessEnv } = await import('../../packages/server/src/services/hermes/agent-bridge/manager')
const env = buildAgentBridgeProcessEnv('ipc:///tmp/test.sock', '/tmp/hermes-home', '/tmp/hermes-agent')
expect(env.HERMES_OPENROUTER_APP_REFERER).toBe('https://ekkolearnai.com')
expect(env.HERMES_OPENROUTER_APP_REFERER).toBe('https://www.xinmi.cloud')
expect(env.HERMES_OPENROUTER_APP_TITLE).toBe('Hermes Web UI')
expect(env.HERMES_OPENROUTER_APP_CATEGORIES).toBe('cli-agent,personal-agent')
})
@@ -122,6 +122,15 @@ describe('agent bridge manager command resolution', () => {
expect(DEFAULT_AGENT_BRIDGE_ENDPOINT).not.toBe('ipc:///tmp/hermes-agent-bridge.sock')
})
it('honors the bridge connect retry environment override', async () => {
process.env.HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS = '120000'
const { AgentBridgeClient } = await import('../../packages/server/src/services/hermes/agent-bridge/client')
const client = new AgentBridgeClient({ endpoint: 'tcp://127.0.0.1:1' })
expect(client.connectRetryMs).toBe(120000)
})
it('waits briefly for a restarting bridge socket before failing', async () => {
const endpoint = process.platform === 'win32'
? `tcp://127.0.0.1:${32000 + (process.pid % 10000)}`
+5 -5
View File
@@ -22,19 +22,19 @@ describe('startDeviceFlow', () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
device_code: 'DC-1',
user_code: 'USER-1234',
verification_uri: 'https://github.com/login/device',
verification_uri: 'https://www.xinmi.cloud/login/device',
expires_in: 900,
interval: 5,
}))
const data = await startDeviceFlow(fetchSpy as any)
expect(data.device_code).toBe('DC-1')
expect(data.user_code).toBe('USER-1234')
expect(data.verification_uri).toBe('https://github.com/login/device')
expect(data.verification_uri).toBe('https://www.xinmi.cloud/login/device')
expect(data.expires_in).toBe(900)
expect(data.interval).toBe(5)
const [url, init] = fetchSpy.mock.calls[0]
expect(url).toBe('https://github.com/login/device/code')
expect(url).toBe('https://www.xinmi.cloud/login/device/code')
expect(init.method).toBe('POST')
const body = String(init.body)
expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`)
@@ -57,7 +57,7 @@ describe('startDeviceFlow', () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
device_code: 'DC-2',
user_code: 'AAAA',
verification_uri: 'https://github.com/login/device',
verification_uri: 'https://www.xinmi.cloud/login/device',
}))
const data = await startDeviceFlow(fetchSpy as any)
expect(data.expires_in).toBe(900)
@@ -130,7 +130,7 @@ describe('pollDeviceFlow', () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ access_token: 'gho_x' }))
await pollDeviceFlow('DEVICE-CODE-XYZ', fetchSpy as any)
const [url, init] = fetchSpy.mock.calls[0]
expect(url).toBe('https://github.com/login/oauth/access_token')
expect(url).toBe('https://www.xinmi.cloud/login/oauth/access_token')
const body = String(init.body)
expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`)
expect(body).toContain('device_code=DEVICE-CODE-XYZ')
+2 -2
View File
@@ -82,7 +82,7 @@ describe('resolveCopilotOAuthToken', () => {
mockReadFile.mockImplementation(async (p: string) => {
if (p.includes('apps.json')) {
return JSON.stringify({
'github.com:abc': { oauth_token: 'gho_from_apps_json', user: 'me' },
'www.xinmi.cloud:abc': { oauth_token: 'gho_from_apps_json', user: 'me' },
})
}
throw new Error('ENOENT')
@@ -93,7 +93,7 @@ describe('resolveCopilotOAuthToken', () => {
it('apps.json 中的 ghp_ token 也应跳过', async () => {
mockReadFile.mockImplementation(async (p: string) => {
if (p.includes('apps.json')) {
return JSON.stringify({ 'github.com:a': { oauth_token: 'ghp_pat_in_apps' } })
return JSON.stringify({ 'www.xinmi.cloud:a': { oauth_token: 'ghp_pat_in_apps' } })
}
throw new Error('ENOENT')
})
+61
View File
@@ -0,0 +1,61 @@
import { mkdirSync, mkdtempSync, rmSync } from 'fs'
import { homedir, tmpdir } from 'os'
import { join, resolve } from 'path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { detectHermesHome } from '../../packages/server/src/services/hermes/hermes-path'
describe('Hermes path detection', () => {
const originalEnv = { ...process.env }
const originalPlatform = process.platform
let tempDir = ''
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'hermes-path-'))
process.env = { ...originalEnv }
delete process.env.HERMES_HOME
delete process.env.LOCALAPPDATA
delete process.env.APPDATA
})
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform })
process.env = { ...originalEnv }
if (tempDir) rmSync(tempDir, { recursive: true, force: true })
tempDir = ''
})
it('keeps explicit HERMES_HOME even when the path does not exist', () => {
process.env.HERMES_HOME = join(tempDir, 'custom-home')
expect(detectHermesHome()).toBe(resolve(tempDir, 'custom-home'))
})
it('falls back to ~/.hermes on Windows when LOCALAPPDATA hermes is missing', () => {
Object.defineProperty(process, 'platform', { value: 'win32' })
process.env.LOCALAPPDATA = join(tempDir, 'Local')
expect(detectHermesHome()).toBe(resolve(homedir(), '.hermes'))
})
it('uses existing Windows LOCALAPPDATA hermes before APPDATA', () => {
Object.defineProperty(process, 'platform', { value: 'win32' })
const localHermes = join(tempDir, 'Local', 'hermes')
const roamingHermes = join(tempDir, 'Roaming', 'hermes')
mkdirSync(localHermes, { recursive: true })
mkdirSync(roamingHermes, { recursive: true })
process.env.LOCALAPPDATA = join(tempDir, 'Local')
process.env.APPDATA = join(tempDir, 'Roaming')
expect(detectHermesHome()).toBe(resolve(localHermes))
})
it('falls back to existing Windows APPDATA hermes when LOCALAPPDATA hermes is missing', () => {
Object.defineProperty(process, 'platform', { value: 'win32' })
const roamingHermes = join(tempDir, 'Roaming', 'hermes')
mkdirSync(roamingHermes, { recursive: true })
process.env.LOCALAPPDATA = join(tempDir, 'Local')
process.env.APPDATA = join(tempDir, 'Roaming')
expect(detectHermesHome()).toBe(resolve(roamingHermes))
})
})
+5 -5
View File
@@ -20,7 +20,7 @@ async function loadUpdateController(overrides: Partial<UpdateControllerMocks> =
const readFileSync = overrides.readFileSync ?? vi.fn(() => JSON.stringify({
name: 'hermes-web-ui',
version: '0.0.0',
repository: { url: 'https://github.com/EKKOLearnAI/hermes-web-ui.git' },
repository: { url: 'https://www.xinmi.cloud/root/Hermes-ui.git' },
}))
const appendFileSync = overrides.appendFileSync ?? vi.fn()
@@ -210,7 +210,7 @@ describe('update controller', () => {
})
it('loads preview tags through async git with a short timeout', async () => {
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://github.com/EKKOLearnAI/hermes-web-ui'
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://www.xinmi.cloud/root/Hermes-ui'
const execFile = vi.fn((_command: string, _args: string[], _options: any, callback: any) => {
callback(null, [
'abc123\trefs/tags/v0.6.6',
@@ -233,14 +233,14 @@ describe('update controller', () => {
})
expect(mocks.execFile).toHaveBeenCalledWith(
'git',
['ls-remote', '--tags', '--refs', 'https://github.com/EKKOLearnAI/hermes-web-ui.git'],
['ls-remote', '--tags', '--refs', 'https://www.xinmi.cloud/root/Hermes-ui.git'],
expect.objectContaining({ timeout: 8000 }),
expect.any(Function),
)
})
it('falls back to GitHub API when async git tag loading fails', async () => {
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://github.com/EKKOLearnAI/hermes-web-ui'
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://www.xinmi.cloud/root/Hermes-ui'
const execFile = vi.fn((_command: string, _args: string[], _options: any, callback: any) => {
callback(new Error('git timeout'), '', '')
})
@@ -267,7 +267,7 @@ describe('update controller', () => {
],
})
expect(fetchMock).toHaveBeenCalledWith(
'https://api.github.com/repos/EKKOLearnAI/hermes-web-ui/tags?per_page=100',
'https://api.www.xinmi.cloud/repos/EKKOLearnAI/hermes-web-ui/tags?per_page=100',
expect.objectContaining({
headers: { 'User-Agent': 'hermes-web-ui-preview' },
signal: expect.any(AbortSignal),