Compare commits

...

10 Commits

Author SHA1 Message Date
新觅源码库 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
73 changed files with 2412 additions and 1365 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: Desktop packaging is intentionally split:
- Pull requests run a Linux desktop smoke test in `.github/workflows/build.yml`. - Pull requests run the web UI build and tests in `.github/workflows/build.yml`.
- Published releases and manual dispatches run `.github/workflows/desktop-release.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. - 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 Do not make a Windows job require macOS `.dmg` files or a Linux job require
+10 -10
View File
@@ -4,29 +4,29 @@
</p> </p>
<p align="center"> <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/> Manage AI chat sessions, monitor usage & costs, configure platform channels,<br/>
schedule cron jobs, browse skills — all from a clean, responsive web interface. schedule cron jobs, browse skills — all from a clean, responsive web interface.
</p> </p>
<p align="center"> <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> <code>npm install -g hermes-web-ui && hermes-web-ui start</code>
</p> </p>
<p align="center"> <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>
<p align="center"> <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>
<p align="center"> <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://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://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://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/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="stars"/></a>
</p> </p>
--- ---
@@ -176,7 +176,7 @@ hermes-web-ui reset-default-login
### Desktop App (Recommended) ### Desktop App (Recommended)
Download the latest **Hermes Studio** desktop installer from 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 Desktop builds are published for macOS, Windows, and Linux, with separate
architecture assets where applicable. The desktop app bundles the Web UI 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_TOOLSETS` | profile/default | Toolset override for bridge runs. |
| `HERMES_BRIDGE_MAX_TURNS` | profile/default | Maximum turn 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_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_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_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_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. | | `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. | | `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_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_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. | | `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 ## Development
```bash ```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 cd hermes-web-ui
npm install npm install
npm run dev npm run dev
+11 -11
View File
@@ -4,23 +4,23 @@
</p> </p>
<p align="center"> <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/> 管理 AI 聊天会话、监控用量与成本、配置平台渠道、<br/>
管理定时任务、浏览技能 —— 全部在一个简洁响应式的 Web 界面中完成。 管理定时任务、浏览技能 —— 全部在一个简洁响应式的 Web 界面中完成。
</p> </p>
<p align="center"> <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> <code>npm install -g hermes-web-ui && hermes-web-ui start</code>
</p> </p>
<p align="center"> <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>
<p align="center"> <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>
<p align="center"> <p align="center">
@@ -28,13 +28,13 @@
</p> </p>
<p align="center"> <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>
<p align="center"> <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://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://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://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/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="Star"/></a>
</p> </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** 桌面安装包。 下载最新的 **Hermes Studio** 桌面安装包。
桌面版会发布 macOS、Windows 和 Linux 构建;适用时会区分不同 CPU 架构。 桌面版会发布 macOS、Windows 和 Linux 构建;适用时会区分不同 CPU 架构。
@@ -274,13 +274,13 @@ Web UI 启动后端聊天能力时,会优先使用包含 `run_agent.py` 的源
| `HERMES_BRIDGE_TOOLSETS` | profile/默认值 | bridge 运行时的 toolset 覆盖。 | | `HERMES_BRIDGE_TOOLSETS` | profile/默认值 | bridge 运行时的 toolset 覆盖。 |
| `HERMES_BRIDGE_MAX_TURNS` | profile/默认值 | bridge 运行时的最大轮数覆盖。 | | `HERMES_BRIDGE_MAX_TURNS` | profile/默认值 | bridge 运行时的最大轮数覆盖。 |
| `HERMES_BRIDGE_SUPPRESS_PLATFORM_HINT` | `cli` | 控制传给 Hermes Agent 的 bridge platform hint suppression。 | | `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_TITLE` | `Hermes Web UI` | bridge 运行发送给 OpenRouter 的 attribution title。 |
| `HERMES_OPENROUTER_APP_CATEGORIES` | `cli-agent,personal-agent` | bridge 运行发送给 OpenRouter 的 attribution categories。 | | `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_MANAGED_GATEWAY` | 由平台/运行环境决定 | 强制启用旧 gateway 进程托管;设为 `1``true``yes``on` 开启。 |
| `HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN` | 生产环境默认开启 | Web UI 关闭时是否同时停止托管的 gateway 进程;设为 `0``false` 可让 gateway 分离运行。 | | `HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN` | 生产环境默认开启 | Web UI 关闭时是否同时停止托管的 gateway 进程;设为 `0``false` 可让 gateway 分离运行。 |
| `GATEWAY_HOST` | `127.0.0.1` | 旧 gateway 兼容配置中写入 profile 的默认 gateway host。 | | `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_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_PREVIEW_AGENT_BRIDGE_ENDPOINT` | 隔离的预览 endpoint | 直接覆盖 Version Preview 的 broker endpoint。 |
| `HERMES_WEB_UI_BACKEND_PORT` | `8648` | Vite dev proxy 使用的后端端口。 | | `HERMES_WEB_UI_BACKEND_PORT` | `8648` | Vite dev proxy 使用的后端端口。 |
@@ -315,7 +315,7 @@ Web UI 启动后端聊天能力时,会优先使用包含 `run_agent.py` 的源
## 开发 ## 开发
```bash ```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 cd hermes-web-ui
npm install npm install
npm run dev npm run dev
+5 -5
View File
@@ -28,17 +28,17 @@ npm run build
| Auth, profile, or credential behavior | focused server tests plus relevant e2e auth tests | | 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 | | 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 | | 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 | | Package manifests | `npm ci --ignore-scripts` and lockfile workflow expectations |
## CI Mapping ## CI Mapping
- Build workflow: installs dependencies, runs coverage, builds production assets, - Build workflow: installs dependencies, runs coverage, and builds production
then runs a Linux desktop smoke test on pull requests. assets on pushes and pull requests.
- Playwright workflow: runs browser e2e tests. - Playwright workflow: runs browser e2e tests.
- NPM lockfile workflow: verifies `package-lock.json` is synchronized. - NPM lockfile workflow: verifies `package-lock.json` is synchronized.
- Desktop release workflow: builds and uploads platform-specific desktop artifacts - Desktop release and manual desktop build workflows build and upload
for release tags. platform-specific desktop artifacts.
- Docker workflow: builds and publishes release images. - Docker workflow: builds and publishes release images.
## Release Workflow Guardrail ## Release Workflow Guardrail
+103 -103
View File
@@ -116,7 +116,7 @@
"tinyexec": "^1.0.1" "tinyexec": "^1.0.1"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://www.xinmi.cloud/sponsors/antfu"
} }
}, },
"node_modules/@asamuzakjp/css-color": { "node_modules/@asamuzakjp/css-color": {
@@ -280,7 +280,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/csstools" "url": "https://www.xinmi.cloud/sponsors/csstools"
}, },
{ {
"type": "opencollective", "type": "opencollective",
@@ -300,7 +300,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/csstools" "url": "https://www.xinmi.cloud/sponsors/csstools"
}, },
{ {
"type": "opencollective", "type": "opencollective",
@@ -324,7 +324,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/csstools" "url": "https://www.xinmi.cloud/sponsors/csstools"
}, },
{ {
"type": "opencollective", "type": "opencollective",
@@ -352,7 +352,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/csstools" "url": "https://www.xinmi.cloud/sponsors/csstools"
}, },
{ {
"type": "opencollective", "type": "opencollective",
@@ -375,7 +375,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/csstools" "url": "https://www.xinmi.cloud/sponsors/csstools"
}, },
{ {
"type": "opencollective", "type": "opencollective",
@@ -400,7 +400,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/csstools" "url": "https://www.xinmi.cloud/sponsors/csstools"
}, },
{ {
"type": "opencollective", "type": "opencollective",
@@ -1299,7 +1299,7 @@
"node": ">= 22" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://www.xinmi.cloud/sponsors/kazupon"
} }
}, },
"node_modules/@intlify/devtools-types": { "node_modules/@intlify/devtools-types": {
@@ -1316,7 +1316,7 @@
"node": ">= 22" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://www.xinmi.cloud/sponsors/kazupon"
} }
}, },
"node_modules/@intlify/message-compiler": { "node_modules/@intlify/message-compiler": {
@@ -1333,7 +1333,7 @@
"node": ">= 22" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://www.xinmi.cloud/sponsors/kazupon"
} }
}, },
"node_modules/@intlify/shared": { "node_modules/@intlify/shared": {
@@ -1346,7 +1346,7 @@
"node": ">= 22" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://www.xinmi.cloud/sponsors/kazupon"
} }
}, },
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
@@ -1507,7 +1507,7 @@
}, },
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/Brooooooklyn" "url": "https://www.xinmi.cloud/sponsors/Brooooooklyn"
}, },
"peerDependencies": { "peerDependencies": {
"@emnapi/core": "^1.7.1", "@emnapi/core": "^1.7.1",
@@ -1528,7 +1528,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/Boshen" "url": "https://www.xinmi.cloud/sponsors/Boshen"
} }
}, },
"node_modules/@parcel/watcher": { "node_modules/@parcel/watcher": {
@@ -1852,7 +1852,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
} }
}, },
"node_modules/@pinia/testing": { "node_modules/@pinia/testing": {
@@ -1862,7 +1862,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/posva" "url": "https://www.xinmi.cloud/sponsors/posva"
}, },
"peerDependencies": { "peerDependencies": {
"pinia": ">=3.0.4" "pinia": ">=3.0.4"
@@ -3474,7 +3474,7 @@
"integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
"funding": [ "funding": [
"https://opencollective.com/katex", "https://opencollective.com/katex",
"https://github.com/sponsors/katex" "https://www.xinmi.cloud/sponsors/katex"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3600,7 +3600,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
} }
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
@@ -3801,7 +3801,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1" "url": "https://www.xinmi.cloud/chalk/ansi-regex?sponsor=1"
} }
}, },
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
@@ -3816,7 +3816,7 @@
"node": ">=8" "node": ">=8"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/anymatch": { "node_modules/anymatch": {
@@ -3937,7 +3937,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/feross" "url": "https://www.xinmi.cloud/sponsors/feross"
}, },
{ {
"type": "patreon", "type": "patreon",
@@ -3979,7 +3979,7 @@
"node": ">=8" "node": ">=8"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/birpc": { "node_modules/birpc": {
@@ -3989,7 +3989,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://www.xinmi.cloud/sponsors/antfu"
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
@@ -4139,7 +4139,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/camelcase": { "node_modules/camelcase": {
@@ -4183,7 +4183,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "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": { "node_modules/chalk/node_modules/supports-color": {
@@ -4303,7 +4303,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/co": { "node_modules/co": {
@@ -4411,7 +4411,7 @@
"node": ">=18" "node": ">=18"
}, },
"funding": { "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": { "node_modules/config-chain": {
@@ -4491,7 +4491,7 @@
"node": ">=18" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/mesqueeb" "url": "https://www.xinmi.cloud/sponsors/mesqueeb"
} }
}, },
"node_modules/cors": { "node_modules/cors": {
@@ -5199,7 +5199,7 @@
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://www.xinmi.cloud/sponsors/kossnocorp"
} }
}, },
"node_modules/date-fns-tz": { "node_modules/date-fns-tz": {
@@ -5545,7 +5545,7 @@
"node": ">=0.12" "node": ">=0.12"
}, },
"funding": { "funding": {
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
} }
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
@@ -5904,7 +5904,7 @@
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh" "url": "https://www.xinmi.cloud/sponsors/RubenVerborgh"
} }
], ],
"license": "MIT", "license": "MIT",
@@ -5931,7 +5931,7 @@
"node": ">=14" "node": ">=14"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
@@ -6015,7 +6015,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/generator-function": { "node_modules/generator-function": {
@@ -6059,7 +6059,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/get-proto": { "node_modules/get-proto": {
@@ -6095,7 +6095,7 @@
"glob": "dist/esm/bin.mjs" "glob": "dist/esm/bin.mjs"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/glob-parent": { "node_modules/glob-parent": {
@@ -6121,7 +6121,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
@@ -6180,7 +6180,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": { "node_modules/has-tostringtag": {
@@ -6196,7 +6196,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
@@ -6401,7 +6401,7 @@
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/wooorm" "url": "https://www.xinmi.cloud/sponsors/wooorm"
} }
}, },
"node_modules/inflation": { "node_modules/inflation": {
@@ -6486,7 +6486,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/is-extglob": { "node_modules/is-extglob": {
@@ -6525,7 +6525,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/is-glob": { "node_modules/is-glob": {
@@ -6574,7 +6574,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/is-what": { "node_modules/is-what": {
@@ -6587,7 +6587,7 @@
"node": ">=18" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/mesqueeb" "url": "https://www.xinmi.cloud/sponsors/mesqueeb"
} }
}, },
"node_modules/isexe": { "node_modules/isexe": {
@@ -6674,7 +6674,7 @@
"@isaacs/cliui": "^8.0.2" "@isaacs/cliui": "^8.0.2"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
}, },
"optionalDependencies": { "optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0" "@pkgjs/parseargs": "^0.11.0"
@@ -6834,7 +6834,7 @@
"integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==", "integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==",
"funding": [ "funding": [
"https://opencollective.com/katex", "https://opencollective.com/katex",
"https://github.com/sponsors/katex" "https://www.xinmi.cloud/sponsors/katex"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7317,11 +7317,11 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/puzrin" "url": "https://www.xinmi.cloud/sponsors/puzrin"
}, },
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/markdown-it" "url": "https://www.xinmi.cloud/sponsors/markdown-it"
} }
], ],
"license": "MIT", "license": "MIT",
@@ -7415,7 +7415,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/make-error": { "node_modules/make-error": {
@@ -7433,11 +7433,11 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/puzrin" "url": "https://www.xinmi.cloud/sponsors/puzrin"
}, },
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/markdown-it" "url": "https://www.xinmi.cloud/sponsors/markdown-it"
} }
], ],
"license": "MIT", "license": "MIT",
@@ -7463,7 +7463,7 @@
"node": ">=0.12" "node": ">=0.12"
}, },
"funding": { "funding": {
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
} }
}, },
"node_modules/marked": { "node_modules/marked": {
@@ -7526,7 +7526,7 @@
"node": ">=12.13" "node": ">=12.13"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/mesqueeb" "url": "https://www.xinmi.cloud/sponsors/mesqueeb"
} }
}, },
"node_modules/merge-anything/node_modules/is-what": { "node_modules/merge-anything/node_modules/is-what": {
@@ -7539,7 +7539,7 @@
"node": ">=12.13" "node": ">=12.13"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/mesqueeb" "url": "https://www.xinmi.cloud/sponsors/mesqueeb"
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
@@ -7549,7 +7549,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/mermaid": { "node_modules/mermaid": {
@@ -7599,7 +7599,7 @@
"dev": true, "dev": true,
"funding": [ "funding": [
"https://opencollective.com/katex", "https://opencollective.com/katex",
"https://github.com/sponsors/katex" "https://www.xinmi.cloud/sponsors/katex"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7666,7 +7666,7 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
@@ -7676,7 +7676,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/minipass": { "node_modules/minipass": {
@@ -7797,7 +7797,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://www.xinmi.cloud/sponsors/ai"
} }
], ],
"license": "MIT", "license": "MIT",
@@ -7951,7 +7951,7 @@
"node": "18 || 20 || >=22" "node": "18 || 20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/nodemon/node_modules/supports-color": { "node_modules/nodemon/node_modules/supports-color": {
@@ -8012,7 +8012,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/on-exit-leak-free": { "node_modules/on-exit-leak-free": {
@@ -8067,7 +8067,7 @@
"node": ">=6" "node": ">=6"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/p-locate": { "node_modules/p-locate": {
@@ -8117,7 +8117,7 @@
"entities": "^8.0.0" "entities": "^8.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1" "url": "https://www.xinmi.cloud/inikulin/parse5?sponsor=1"
} }
}, },
"node_modules/parse5/node_modules/entities": { "node_modules/parse5/node_modules/entities": {
@@ -8130,7 +8130,7 @@
"node": ">=20.19.0" "node": ">=20.19.0"
}, },
"funding": { "funding": {
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
} }
}, },
"node_modules/parseurl": { "node_modules/parseurl": {
@@ -8208,7 +8208,7 @@
"node": ">=16 || 14 >=14.18" "node": ">=16 || 14 >=14.18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/path-scurry/node_modules/lru-cache": { "node_modules/path-scurry/node_modules/lru-cache": {
@@ -8270,7 +8270,7 @@
"node": ">=8.6" "node": ">=8.6"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
} }
}, },
"node_modules/pinia": { "node_modules/pinia": {
@@ -8283,7 +8283,7 @@
"@vue/devtools-api": "^7.7.7" "@vue/devtools-api": "^7.7.7"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/posva" "url": "https://www.xinmi.cloud/sponsors/posva"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": ">=4.5.0", "typescript": ">=4.5.0",
@@ -8451,7 +8451,7 @@
}, },
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://www.xinmi.cloud/sponsors/ai"
} }
], ],
"license": "MIT", "license": "MIT",
@@ -8472,7 +8472,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/fastify" "url": "https://www.xinmi.cloud/sponsors/fastify"
}, },
{ {
"type": "opencollective", "type": "opencollective",
@@ -8697,7 +8697,7 @@
"node": ">=0.6" "node": ">=0.6"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/quick-format-unescaped": { "node_modules/quick-format-unescaped": {
@@ -8821,7 +8821,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/resolve-path": { "node_modules/resolve-path": {
@@ -8939,7 +8939,7 @@
"node": "*" "node": "*"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/rimraf/node_modules/minimatch": { "node_modules/rimraf/node_modules/minimatch": {
@@ -9086,7 +9086,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/feross" "url": "https://www.xinmi.cloud/sponsors/feross"
}, },
{ {
"type": "patreon", "type": "patreon",
@@ -9114,7 +9114,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/safe-stable-stringify": { "node_modules/safe-stable-stringify": {
@@ -9206,7 +9206,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/fastify" "url": "https://www.xinmi.cloud/sponsors/fastify"
}, },
{ {
"type": "opencollective", "type": "opencollective",
@@ -9360,7 +9360,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/side-channel": { "node_modules/side-channel": {
@@ -9380,7 +9380,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/side-channel-list": { "node_modules/side-channel-list": {
@@ -9397,7 +9397,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/side-channel-map": { "node_modules/side-channel-map": {
@@ -9416,7 +9416,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/side-channel-weakmap": { "node_modules/side-channel-weakmap": {
@@ -9436,7 +9436,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/siginfo": { "node_modules/siginfo": {
@@ -9456,7 +9456,7 @@
"node": ">=14" "node": ">=14"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/simple-update-notifier": { "node_modules/simple-update-notifier": {
@@ -9649,7 +9649,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/string-width-cjs": { "node_modules/string-width-cjs": {
@@ -9711,7 +9711,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "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": { "node_modules/strip-ansi-cjs": {
@@ -9758,7 +9758,7 @@
"node": ">=14.16" "node": ">=14.16"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/strip-literal": { "node_modules/strip-literal": {
@@ -9771,7 +9771,7 @@
"js-tokens": "^9.0.1" "js-tokens": "^9.0.1"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://www.xinmi.cloud/sponsors/antfu"
} }
}, },
"node_modules/strip-literal/node_modules/js-tokens": { "node_modules/strip-literal/node_modules/js-tokens": {
@@ -9814,7 +9814,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "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": { "node_modules/supports-preserve-symlinks-flag": {
@@ -9827,7 +9827,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/symbol-tree": { "node_modules/symbol-tree": {
@@ -9888,7 +9888,7 @@
"node": "18 || 20 || >=22" "node": "18 || 20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/thread-stream": { "node_modules/thread-stream": {
@@ -9942,7 +9942,7 @@
"node": ">=12.0.0" "node": ">=12.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/SuperchupuDev" "url": "https://www.xinmi.cloud/sponsors/SuperchupuDev"
} }
}, },
"node_modules/tinyglobby/node_modules/fdir": { "node_modules/tinyglobby/node_modules/fdir": {
@@ -9973,7 +9973,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
} }
}, },
"node_modules/tinypool": { "node_modules/tinypool": {
@@ -10357,8 +10357,8 @@
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
"dev": true, "dev": true,
"funding": [ "funding": [
"https://github.com/sponsors/broofa", "https://www.xinmi.cloud/sponsors/broofa",
"https://github.com/sponsors/ctavan" "https://www.xinmi.cloud/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -10424,7 +10424,7 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
@@ -10533,7 +10533,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
} }
}, },
"node_modules/vite-node/node_modules/vite": { "node_modules/vite-node/node_modules/vite": {
@@ -10557,7 +10557,7 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
@@ -10621,7 +10621,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
@@ -10762,7 +10762,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
} }
}, },
"node_modules/vitest/node_modules/tinyexec": { "node_modules/vitest/node_modules/tinyexec": {
@@ -10793,7 +10793,7 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
@@ -10912,7 +10912,7 @@
"node": ">= 22" "node": ">= 22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/kazupon" "url": "https://www.xinmi.cloud/sponsors/kazupon"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.0.0" "vue": "^3.0.0"
@@ -10935,7 +10935,7 @@
"@vue/devtools-api": "^6.6.4" "@vue/devtools-api": "^6.6.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/posva" "url": "https://www.xinmi.cloud/sponsors/posva"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.5.0" "vue": "^3.5.0"
@@ -11103,7 +11103,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "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/wrap-ansi-cjs": {
@@ -11122,7 +11122,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "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": { "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
@@ -11180,7 +11180,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/wrappy": { "node_modules/wrappy": {
@@ -11268,7 +11268,7 @@
"node": ">= 14.6" "node": ">= 14.6"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/eemeli" "url": "https://www.xinmi.cloud/sponsors/eemeli"
} }
}, },
"node_modules/yargs": { "node_modules/yargs": {
@@ -11360,4 +11360,4 @@
} }
} }
} }
} }
+8 -7
View File
@@ -4,9 +4,9 @@
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration", "description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
"repository": { "repository": {
"type": "git", "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", "license": "BSL-1.1",
"engines": { "engines": {
"node": ">=23.0.0" "node": ">=23.0.0"
@@ -50,11 +50,12 @@
"build:website": "vite build --config vite.config.website.ts", "build:website": "vite build --config vite.config.website.ts",
"preview:website": "vite preview --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: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", "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": "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 run desktop:prepare-python && npm --prefix packages/desktop run dist -- --mac --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 run desktop:prepare-python && npm --prefix packages/desktop run dist -- --win --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 run desktop:prepare-python && npm --prefix packages/desktop run dist -- --linux --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", "openapi:generate": "node scripts/generate-openapi.mjs",
"claude": "claude --dangerously-skip-permissions" "claude": "claude --dangerously-skip-permissions"
}, },
@@ -131,4 +132,4 @@
"vue-virtual-scroller": "^3.0.4", "vue-virtual-scroller": "^3.0.4",
"ws": "^8.20.0" "ws": "^8.20.0"
} }
} }
@@ -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. 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 ## Maintenance Guidelines
@@ -28,7 +28,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
### Anthropic Official Skills ### 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. - Focus: official reference skills for Claude-style agents.
- Good for: learning the expected skill structure, adapting stable examples, and bootstrapping common workflows. - 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`. - 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 ### 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. - 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. - 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`. - 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 ### 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. - Focus: creating web-native slide decks with frontend techniques.
- Good for: HTML/CSS slide decks, visual storytelling, and browser-rendered presentations. - 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. - Notes: useful when a deck should be designed as a rich web artifact rather than a traditional office file.
### Huashu Design ### 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. - 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. - 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. - Notes: includes design philosophy, review heuristics, and presentation-oriented workflows.
### Guizang PPT Skill ### 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. - Focus: polished HTML slide decks with editorial, magazine, and Swiss-style layouts.
- Good for: presentation decks, social covers, image prompts, and visual narrative work. - Good for: presentation decks, social covers, image prompts, and visual narrative work.
- Notes: includes a presentation runtime and style-oriented slide generation patterns. - Notes: includes a presentation runtime and style-oriented slide generation patterns.
### HTML PPT Skill ### 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. - Focus: HTML PPT Studio for professional HTML presentations.
- Good for: themed slide decks, layout-rich presentations, and animated browser 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. - Representative capabilities: multiple themes, layout templates, animation patterns, and HTML presentation scaffolding.
### PPT Image First ### 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. - Focus: image-first presentation generation.
- Good for: decks where the visual direction should lead the content structure. - Good for: decks where the visual direction should lead the content structure.
- Notes: designed for Codex, Claude Code, and OpenCode-style CLI agents. - Notes: designed for Codex, Claude Code, and OpenCode-style CLI agents.
### GPT Image To PPT ### 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. - 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. - 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. - Notes: useful for template-driven presentations, but review external image generation/API behavior before use.
### Fireworks Tech Graph ### 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. - Focus: technical diagram generation.
- Good for: architecture diagrams, workflow charts, UML-style visuals, AI agent workflow diagrams, and production-ready SVG/PNG outputs. - 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. - Notes: a practical choice when you need diagrams rather than full slide decks.
### Diagram Skill ### 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. - Focus: diagram generation inside a broader Claude skill collection.
- Good for: generating structured diagrams, templates, and visual explanations. - 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. - 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 ### 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. - 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. - 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. - 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 ### 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. - Focus: Chinese web novel writing workflows.
- Good for: long-form fiction planning, chapter writing, style continuity, and web-novel oriented drafting. - Good for: long-form fiction planning, chapter writing, style continuity, and web-novel oriented drafting.
- Representative skill: `webnovel-writing`. - Representative skill: `webnovel-writing`.
### Software Copyright Skill ### 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. - Focus: preparing Chinese software copyright application materials.
- Good for: generating `.docx` application documents from a local software project. - Good for: generating `.docx` application documents from a local software project.
- Representative skills: `software-copyright-materials`, `docx-toolkit`. - 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 ### 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. - Focus: patent disclosure drafting.
- Good for: extracting patentable points from project documents, novelty checks, desensitized drafting, and self-review loops. - 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. - 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 ### 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. - 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. - 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`. - 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 ### 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. - Focus: travel-vlog style media generation.
- Good for: short-form visual storytelling, character-based travel content, and repeatable media production prompts. - 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. - 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 ### 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. - 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. - 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. - Security note: browser access can expose logged-in sessions and local browser state. Audit before enabling.
### OpenCLI ### 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. - 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. - 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`. - 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 ### 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. - Focus: monitoring AI builders across X, blogs, and YouTube podcasts.
- Good for: tracking builders rather than influencers, summarizing feeds, and creating digest-style updates. - 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. - 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 ### 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. - 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. - 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. - 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 ### 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. - 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. - 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. - 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 ### 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. - Focus: high-agency, confrontational, coaching, or anti-PUA style agent behavior.
- Good for: motivation, critique, resistance to manipulation, and intentionally sharp agent feedback. - 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`. - 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 ### 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. - Focus: distilling an ex-partner/persona into an AI skill that speaks in that style.
- Good for: persona experiments, emotional roleplay, and style simulation. - Good for: persona experiments, emotional roleplay, and style simulation.
- Representative skill: `create-ex`. - 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: If you only want a practical starter set:
- [Anthropic Official Skills](https://github.com/anthropics/skills/tree/main/skills) for reference implementations. - [Anthropic Official Skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills) for reference implementations.
- [Matt Pocock Skills](https://github.com/mattpocock/skills) for engineering workflows. - [Matt Pocock Skills](https://www.xinmi.cloud/mattpocock/skills) for engineering workflows.
- [Baoyu Skills](https://github.com/JimLiu/baoyu-skills) for image, media, and publishing workflows. - [Baoyu Skills](https://www.xinmi.cloud/JimLiu/baoyu-skills) for image, media, and publishing workflows.
- [Huashu Design](https://github.com/alchaincyf/huashu-design) for high-fidelity HTML-native design. - [Huashu Design](https://www.xinmi.cloud/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. - [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://github.com/alchaincyf/huashu-md-html) for Markdown/HTML document conversion. - [Huashu Markdown To HTML](https://www.xinmi.cloud/alchaincyf/huashu-md-html) for Markdown/HTML document conversion.
- [Web Access](https://github.com/eze-is/web-access) for web research workflows. - [Web Access](https://www.xinmi.cloud/eze-is/web-access) for web research workflows.
- [OpenCLI](https://github.com/jackwener/opencli) for logged-in browser automation and reusable website CLI adapters. - [OpenCLI](https://www.xinmi.cloud/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. - [Fireworks Tech Graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph) for technical diagrams.
- [SlowMist Agent Security](https://github.com/slowmist/slowmist-agent-security) for reviewing risky community skills. - [SlowMist Agent Security](https://www.xinmi.cloud/slowmist/slowmist-agent-security) for reviewing risky community skills.
## Original Source List ## 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 本质上是第三方指令和代码。安装前请先审计,尤其是会读取 API Key、Cookie、浏览器登录态、本地文件、仓库内容,或者会执行 shell、安装依赖、自动发帖、访问外部 API 的 Skill。
欢迎大家推荐各种好用的 Skill。如果你发现值得收录的高质量 Skill,可以到 GitHub 提交 PR,并附上仓库链接、适用场景和必要的安全说明。 欢迎大家推荐各种好用的 Skill。如果你发现值得收录的高质量 Skill,可以到 新觅源码库 提交 PR,并附上仓库链接、适用场景和必要的安全说明。
## 维护规范 ## 维护规范
@@ -28,7 +28,7 @@
### Anthropic 官方 Skills ### 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。 - 方向:Claude 官方参考 Skill。
- 适合:学习标准 Skill 结构、参考稳定实现、搭建通用工作流。 - 适合:学习标准 Skill 结构、参考稳定实现、搭建通用工作流。
- 代表 Skills`docx``pdf``pptx``xlsx``frontend-design``webapp-testing``skill-creator``mcp-builder``theme-factory``web-artifacts-builder` - 代表 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 ### Matt Pocock Skills
- 仓库:[mattpocock/skills](https://github.com/mattpocock/skills) - 仓库:[mattpocock/skills](https://www.xinmi.cloud/mattpocock/skills)
- 方向:工程与生产力工作流。 - 方向:工程与生产力工作流。
- 适合:TypeScript 工程、TDD、问题诊断、代码评审、原型开发、PRD/Issue/Handoff 等开发流程。 - 适合:TypeScript 工程、TDD、问题诊断、代码评审、原型开发、PRD/Issue/Handoff 等开发流程。
- 代表 Skills`tdd``triage``diagnose``prototype``review``to-prd``to-issues``handoff``write-a-skill` - 代表 Skills`tdd``triage``diagnose``prototype``review``to-prd``to-issues``handoff``write-a-skill`
@@ -46,56 +46,56 @@
### Frontend Slides ### Frontend Slides
- 仓库:[zarazhangrui/frontend-slides](https://github.com/zarazhangrui/frontend-slides) - 仓库:[zarazhangrui/frontend-slides](https://www.xinmi.cloud/zarazhangrui/frontend-slides)
- 方向:用前端技术生成网页幻灯片。 - 方向:用前端技术生成网页幻灯片。
- 适合:HTML/CSS 幻灯片、视觉叙事、浏览器渲染的演示稿。 - 适合:HTML/CSS 幻灯片、视觉叙事、浏览器渲染的演示稿。
- 备注:适合把演示稿当成 Web Artifact 来做,而不是传统 Office 文件。 - 备注:适合把演示稿当成 Web Artifact 来做,而不是传统 Office 文件。
### 华叔 Design ### 华叔 Design
- 仓库:[alchaincyf/huashu-design](https://github.com/alchaincyf/huashu-design) - 仓库:[alchaincyf/huashu-design](https://www.xinmi.cloud/alchaincyf/huashu-design)
- 方向:Claude Code 中的 HTML 原生设计 Skill。 - 方向:Claude Code 中的 HTML 原生设计 Skill。
- 适合:高保真原型、幻灯片、动画概念、视觉评审和导出型设计流程。 - 适合:高保真原型、幻灯片、动画概念、视觉评审和导出型设计流程。
- 备注:包含设计哲学、评审维度和演示型工作流。 - 备注:包含设计哲学、评审维度和演示型工作流。
### 归藏 PPT 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 幻灯片。
- 适合:杂志风、编辑风、瑞士风等视觉风格的演示稿、社交封面、图片提示词和叙事型页面。 - 适合:杂志风、编辑风、瑞士风等视觉风格的演示稿、社交封面、图片提示词和叙事型页面。
- 备注:包含演示运行时和风格化生成模式。 - 备注:包含演示运行时和风格化生成模式。
### HTML PPT Skill ### 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 Studio。
- 适合:主题化幻灯片、复杂布局演示稿和带动画的浏览器演示。 - 适合:主题化幻灯片、复杂布局演示稿和带动画的浏览器演示。
- 代表能力:多主题、多布局、动画模式和 HTML 演示脚手架。 - 代表能力:多主题、多布局、动画模式和 HTML 演示脚手架。
### PPT Image First ### 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 生成。 - 方向:图片优先的 PPT 生成。
- 适合:视觉方向先行的演示稿创作。 - 适合:视觉方向先行的演示稿创作。
- 备注:面向 Codex、Claude Code、OpenCode CLI 等 Agent 工作流。 - 备注:面向 Codex、Claude Code、OpenCode CLI 等 Agent 工作流。
### GPT Image To PPT ### 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 视觉版式。 - 方向:用图像生成能力复刻或改造 PPT 视觉版式。
- 适合:从已有 `.pptx` 模板中学习版式,再替换成自己的内容。 - 适合:从已有 `.pptx` 模板中学习版式,再替换成自己的内容。
- 备注:涉及图像生成和外部 API 时请先检查配置与数据发送逻辑。 - 备注:涉及图像生成和外部 API 时请先检查配置与数据发送逻辑。
### Fireworks Tech Graph ### 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 输出。 - 适合:架构图、流程图、UML 风格图、AI Agent 工作流图,以及 SVG/PNG 输出。
- 备注:需要图表而不是整套演示稿时很实用。 - 备注:需要图表而不是整套演示稿时很实用。
### Diagram Skill ### 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` - 备注:这是一个直接指向 `SKILL.md` 的链接,安装前也要检查同目录下的 `references``scripts``templates`
@@ -104,7 +104,7 @@
### 华叔 Markdown To HTML ### 华叔 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 与 HTML 双向转换流水线。
- 适合:把文件或网页转 Markdown,把 Markdown 转精美 HTML,把 HTML 再转回 Markdown。 - 适合:把文件或网页转 Markdown,把 Markdown 转精美 HTML,把 HTML 再转回 Markdown。
- 代表工具:MarkItDown、Pandoc、html-to-markdown、trafilatura。 - 代表工具:MarkItDown、Pandoc、html-to-markdown、trafilatura。
@@ -112,14 +112,14 @@
### 中文网文写作 Skill ### 中文网文写作 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`webnovel-writing`
### 软件著作权材料 Skill ### 软件著作权材料 Skill
- 仓库:[Fokkyp/SoftwareCopyright-Skill](https://github.com/Fokkyp/SoftwareCopyright-Skill) - 仓库:[Fokkyp/SoftwareCopyright-Skill](https://www.xinmi.cloud/Fokkyp/SoftwareCopyright-Skill)
- 方向:中国软件著作权申请材料生成。 - 方向:中国软件著作权申请材料生成。
- 适合:根据本地项目生成 `.docx` 软著申请材料。 - 适合:根据本地项目生成 `.docx` 软著申请材料。
- 代表 Skills`software-copyright-materials``docx-toolkit` - 代表 Skills`software-copyright-materials``docx-toolkit`
@@ -127,7 +127,7 @@
### 专利交底书 Skill ### 专利交底书 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 ### 宝玉 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、社交平台发布。 - 适合:图片卡片、文章配图、幻灯片、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` - 代表 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 ### 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 风格媒体生成。 - 方向:旅行 vlog 风格媒体生成。
- 适合:短视频视觉叙事、角色化旅行内容和可复用媒体提示词。 - 适合:短视频视觉叙事、角色化旅行内容和可复用媒体提示词。
- 备注:这是一个大仓库里的子目录 Skill。 - 备注:这是一个大仓库里的子目录 Skill。
@@ -153,14 +153,14 @@
### Web Access ### 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 提供结构化联网能力。 - 方向:为 Agent 提供结构化联网能力。
- 适合:网页研究、浏览器辅助任务、并行信息收集和需要交互的网站。 - 适合:网页研究、浏览器辅助任务、并行信息收集和需要交互的网站。
- 安全提示:浏览器访问可能暴露已登录状态和本地浏览器数据,启用前要审计。 - 安全提示:浏览器访问可能暴露已登录状态和本地浏览器数据,启用前要审计。
### OpenCLI ### OpenCLI
- 仓库:[jackwener/opencli](https://github.com/jackwener/opencli) - 仓库:[jackwener/opencli](https://www.xinmi.cloud/jackwener/opencli)
- 方向:把网站、浏览器会话、Electron 应用和本地工具转换成 CLI 可调用的自动化入口。 - 方向:把网站、浏览器会话、Electron 应用和本地工具转换成 CLI 可调用的自动化入口。
- 适合:让 Agent 操作已登录的 Chrome 页面、编写可复用网站适配器、封装本地命令,以及把浏览器流程变成稳定命令。 - 适合:让 Agent 操作已登录的 Chrome 页面、编写可复用网站适配器、封装本地命令,以及把浏览器流程变成稳定命令。
- 代表 Skills`opencli-browser``opencli-adapter-author``opencli-autofix``opencli-usage` - 代表 Skills`opencli-browser``opencli-adapter-author``opencli-autofix``opencli-usage`
@@ -168,7 +168,7 @@
### Follow Builders ### 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 播客内容。 - 方向:跟踪 AI builders 的 X、博客和 YouTube 播客内容。
- 适合:关注 builder 而不是 influencer,生成摘要和内容 digest。 - 适合:关注 builder 而不是 influencer,生成摘要和内容 digest。
- 代表内容:X feed、blog feed、podcast feed、prompts 和状态文件。 - 代表内容:X feed、blog feed、podcast feed、prompts 和状态文件。
@@ -176,7 +176,7 @@
### SlowMist Agent Security ### 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 安全审计框架。 - 方向:AI Agent 安全审计框架。
- 适合:检查 Skill、MCP、仓库、URL、Prompt 和链上地址的安全风险。 - 适合:检查 Skill、MCP、仓库、URL、Prompt 和链上地址的安全风险。
- 核心原则:所有外部输入在验证前都不可信。 - 核心原则:所有外部输入在验证前都不可信。
@@ -186,7 +186,7 @@
### 华叔 Nuwa Skill ### 华叔 Nuwa Skill
- 仓库:[alchaincyf/nuwa-skill](https://github.com/alchaincyf/nuwa-skill) - 仓库:[alchaincyf/nuwa-skill](https://www.xinmi.cloud/alchaincyf/nuwa-skill)
- 方向:把某个人或视角蒸馏成可复用 Skill。 - 方向:把某个人或视角蒸馏成可复用 Skill。
- 适合:顾问团式思考、心智模型、决策启发式和特定视角写作。 - 适合:顾问团式思考、心智模型、决策启发式和特定视角写作。
- 代表视角:华叔 Nuwa、Feynman、Jobs、Musk、Naval、Paul Graham、Taleb。 - 代表视角:华叔 Nuwa、Feynman、Jobs、Musk、Naval、Paul Graham、Taleb。
@@ -194,7 +194,7 @@
### PUA / 反 PUA 类 Skills ### PUA / 反 PUA 类 Skills
- 仓库:[tanweai/pua](https://github.com/tanweai/pua) - 仓库:[tanweai/pua](https://www.xinmi.cloud/tanweai/pua)
- 方向:高能动性、强反馈、反操控或尖锐教练风格的 Agent 行为。 - 方向:高能动性、强反馈、反操控或尖锐教练风格的 Agent 行为。
- 适合:动机强化、批判反馈、反操控和刻意强风格交互。 - 适合:动机强化、批判反馈、反操控和刻意强风格交互。
- 代表 Skills`pua``pua-en``pua-ja``pua-loop``mama``p7``p9``p10``pro``shot``yes` - 代表 Skills`pua``pua-en``pua-ja``pua-loop``mama``p7``p9``p10``pro``shot``yes`
@@ -202,7 +202,7 @@
### Ex Skill ### Ex Skill
- 仓库:[therealXiaomanChu/ex-skill](https://github.com/therealXiaomanChu/ex-skill) - 仓库:[therealXiaomanChu/ex-skill](https://www.xinmi.cloud/therealXiaomanChu/ex-skill)
- 方向:把某个前任/人格风格蒸馏成 AI Skill。 - 方向:把某个前任/人格风格蒸馏成 AI Skill。
- 适合:Persona 实验、情绪化角色扮演和特定语气模拟。 - 适合:Persona 实验、情绪化角色扮演和特定语气模拟。
- 代表 Skill`create-ex` - 代表 Skill`create-ex`
@@ -212,17 +212,17 @@
如果你只想先装一批实用的,可以从这些开始: 如果你只想先装一批实用的,可以从这些开始:
- [Anthropic 官方 Skills](https://github.com/anthropics/skills/tree/main/skills):参考实现和通用能力。 - [Anthropic 官方 Skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills):参考实现和通用能力。
- [Matt Pocock Skills](https://github.com/mattpocock/skills):工程流程。 - [Matt Pocock Skills](https://www.xinmi.cloud/mattpocock/skills):工程流程。
- [宝玉 Skills](https://github.com/JimLiu/baoyu-skills):图片、媒体和发布。 - [宝玉 Skills](https://www.xinmi.cloud/JimLiu/baoyu-skills):图片、媒体和发布。
- [华叔 Design](https://github.com/alchaincyf/huashu-design):高保真 HTML 设计。 - [华叔 Design](https://www.xinmi.cloud/alchaincyf/huashu-design):高保真 HTML 设计。
- [归藏 PPT Skill](https://github.com/op7418/guizang-ppt-skill) 或 [HTML PPT Skill](https://github.com/lewislulu/html-ppt-skill):浏览器演示稿。 - [归藏 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://github.com/alchaincyf/huashu-md-html)Markdown/HTML 文档转换。 - [华叔 Markdown To HTML](https://www.xinmi.cloud/alchaincyf/huashu-md-html)Markdown/HTML 文档转换。
- [Web Access](https://github.com/eze-is/web-access):网页研究。 - [Web Access](https://www.xinmi.cloud/eze-is/web-access):网页研究。
- [OpenCLI](https://github.com/jackwener/opencli):已登录浏览器自动化和可复用网站 CLI 适配器。 - [OpenCLI](https://www.xinmi.cloud/jackwener/opencli):已登录浏览器自动化和可复用网站 CLI 适配器。
- [Fireworks Tech Graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph):技术图表。 - [Fireworks Tech Graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph):技术图表。
- [SlowMist Agent Security](https://github.com/slowmist/slowmist-agent-security):社区 Skill 安全审计。 - [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>
<div class="version-info"> <div class="version-info">
<div class="version-links"> <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> <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>
<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> <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> </a>
</div> </div>
+1 -1
View File
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job ausgelost',
// Anderungsprotokoll // Anderungsprotokoll
changelog: { 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_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_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_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', 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',
+1 -1
View File
@@ -1483,7 +1483,7 @@ export default {
// Changelog // Changelog
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_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_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_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', 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 // Registro de cambios
changelog: { 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_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_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_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', 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 // Journal des modifications
changelog: { 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_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_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_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', 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: { changelog: {
new_0_6_7_1: 'Desktop アプリは既定で port 8748 を使用し、LAN アクセスとローカルブラウザからの直接アクセスに対応しました', 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_2: 'MCP ツールは bridge の tool discovery 修正、MCP 管理ライフサイクル修正、管理画面のモデル別 tool visibility によりさらに整備されました',
new_0_6_7_3: 'メッセージ一覧は empty state の中央揃え、scroll jitter、History 読み込み中のライブチャット混入を修正し、セッション別スクロール位置保持と 1.5 秒のフェードインに対応しました', 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 作成スキップで安定しました', 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: { changelog: {
new_0_6_7_1: 'Desktop 앱은 기본적으로 8748 포트를 사용하며 LAN 접근과 로컬 브라우저 직접 열기를 지원합니다', 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_2: 'MCP 도구는 bridge tool discovery 수정, MCP 관리 라이프사이클 수정, 관리자 화면의 모델별 tool visibility 제어로 더 완성되었습니다',
new_0_6_7_3: '메시지 목록은 빈 상태 중앙 정렬, 스크롤 튐, History 로딩 중 라이브 채팅 메시지 노출을 수정하고 세션별 스크롤 위치 보존과 1.5초 페이드인을 지원합니다', new_0_6_7_3: '메시지 목록은 빈 상태 중앙 정렬, 스크롤 튐, History 로딩 중 라이브 채팅 메시지 노출을 수정하고 세션별 스크롤 위치 보존과 1.5초 페이드인을 지원합니다',
new_0_6_7_4: 'Bridge 와 runtime 은 text/tool-call 순서 보존, Profile runtime status loading 수정, Node/npm 감지 개선, 운영 데이터 디렉터리 생성 생략으로 더 안정적입니다', 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 // Registro de alteracoes
changelog: { 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_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_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_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', 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: { changelog: {
new_0_6_7_1: '桌面版預設使用 8748 連接埠,支援區域網路內存取,也可以直接用本機瀏覽器開啟 Web UI', 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_2: 'MCP 工具鏈繼續完善:修復 bridge 工具發現與 MCP 管理生命週期,並在管理頁支援按模型控制工具可見性',
new_0_6_7_3: '訊息列表體驗優化:修復空狀態置中、捲動抖動、歷史會話載入串訊息,並在切換會話時保留捲動位置與 1.5 秒淡入效果', new_0_6_7_3: '訊息列表體驗優化:修復空狀態置中、捲動抖動、歷史會話載入串訊息,並在切換會話時保留捲動位置與 1.5 秒淡入效果',
new_0_6_7_4: 'Bridge 與執行狀態更穩定:保持文字和 tool-call 順序、修復 Profile runtime 狀態載入、改進 Node/npm 偵測,並避免正式環境自動建立資料目錄', new_0_6_7_4: 'Bridge 與執行狀態更穩定:保持文字和 tool-call 順序、修復 Profile runtime 狀態載入、改進 Node/npm 偵測,並避免正式環境自動建立資料目錄',
+1 -1
View File
@@ -1485,7 +1485,7 @@ export default {
// 更新日志 // 更新日志
changelog: { changelog: {
new_0_6_7_1: '桌面版默认使用 8748 端口,支持局域网内访问,也可以直接用本机浏览器打开 Web UI', 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_2: 'MCP 工具链继续完善:修复 bridge 工具发现与 MCP 管理生命周期,并在管理页支持按模型控制工具可见性',
new_0_6_7_3: '消息列表体验优化:修复空状态居中、滚动抖动、历史会话加载串消息,并在切换会话时保留滚动位置与 1.5 秒淡入效果', new_0_6_7_3: '消息列表体验优化:修复空状态居中、滚动抖动、历史会话加载串消息,并在切换会话时保留滚动位置与 1.5 秒淡入效果',
new_0_6_7_4: 'Bridge 与运行态更稳定:保持文本和 tool-call 顺序、修复 Profile runtime 状态加载、改进 Node/npm 检测,并避免生产环境自动创建数据目录', new_0_6_7_4: 'Bridge 与运行态更稳定:保持文本和 tool-call 顺序、修复 Profile runtime 状态加载、改进 Node/npm 检测,并避免生产环境自动创建数据目录',
+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 Download the latest macOS, Windows, or Linux installer for your CPU
architecture from the project 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 The desktop app bundles the Web UI runtime and launches it locally from the
native shell app. 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/ 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: python-build-standalone release mirror:
```sh ```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" - "!**/node_modules/.bin"
- "!**/{.DS_Store,.git,.gitignore,.eslintrc*,.prettierrc*,*.map,tsconfig.json}" - "!**/{.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. # This package lives at packages/desktop, so ../.. is the hermes-web-ui repo root.
extraResources: extraResources:
- from: "build" - from: "build"
@@ -32,6 +33,7 @@ extraResources:
- "icon.ico" - "icon.ico"
- "trayTemplate.png" - "trayTemplate.png"
- "trayWindows.png" - "trayWindows.png"
- "runtime-release.json"
- from: "../.." - from: "../.."
to: "webui" to: "webui"
filter: filter:
@@ -44,10 +46,6 @@ extraResources:
- "!packages/desktop/**" - "!packages/desktop/**"
- "!**/{.git,.github,docs,tests,playwright.config.ts,README*,scripts,*.map}" - "!**/{.git,.github,docs,tests,playwright.config.ts,README*,scripts,*.map}"
- "!node_modules/**/*.md" - "!node_modules/**/*.md"
- from: "resources/python/${os}-${arch}"
to: "python"
filter:
- "**/*"
asarUnpack: asarUnpack:
- "**/*.node" - "**/*.node"
+58 -58
View File
@@ -7,7 +7,7 @@
"": { "": {
"name": "hermes-studio", "name": "hermes-studio",
"version": "0.6.8", "version": "0.6.8",
"license": "MIT", "license": "BSL-1.1",
"dependencies": { "dependencies": {
"electron-updater": "^6.3.9" "electron-updater": "^6.3.9"
}, },
@@ -169,7 +169,7 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/gjtorikian/" "url": "https://www.xinmi.cloud/sponsors/gjtorikian/"
} }
}, },
"node_modules/@electron/rebuild": { "node_modules/@electron/rebuild": {
@@ -265,7 +265,7 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/@gar/promisify": { "node_modules/@gar/promisify": {
@@ -303,7 +303,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "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": { "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
@@ -316,7 +316,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "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": { "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
@@ -341,7 +341,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/@isaacs/cliui/node_modules/strip-ansi": { "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
@@ -357,7 +357,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "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": { "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
@@ -375,7 +375,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "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": { "node_modules/@malept/cross-spawn-promise": {
@@ -386,7 +386,7 @@
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
"url": "https://github.com/sponsors/malept" "url": "https://www.xinmi.cloud/sponsors/malept"
}, },
{ {
"type": "tidelift", "type": "tidelift",
@@ -483,7 +483,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1" "url": "https://www.xinmi.cloud/sindresorhus/is?sponsor=1"
} }
}, },
"node_modules/@szmarczak/http-timer": { "node_modules/@szmarczak/http-timer": {
@@ -692,7 +692,7 @@
}, },
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/epoberezkin" "url": "https://www.xinmi.cloud/sponsors/epoberezkin"
} }
}, },
"node_modules/ajv-keywords": { "node_modules/ajv-keywords": {
@@ -728,7 +728,7 @@
"node": ">=8" "node": ">=8"
}, },
"funding": { "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": { "node_modules/app-builder-bin": {
@@ -967,7 +967,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/feross" "url": "https://www.xinmi.cloud/sponsors/feross"
}, },
{ {
"type": "patreon", "type": "patreon",
@@ -1030,7 +1030,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/feross" "url": "https://www.xinmi.cloud/sponsors/feross"
}, },
{ {
"type": "patreon", "type": "patreon",
@@ -1168,7 +1168,7 @@
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/cacache/node_modules/lru-cache": { "node_modules/cacache/node_modules/lru-cache": {
@@ -1251,7 +1251,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://www.xinmi.cloud/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chownr": { "node_modules/chownr": {
@@ -1279,7 +1279,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/sibiraj-s" "url": "https://www.xinmi.cloud/sponsors/sibiraj-s"
} }
], ],
"license": "MIT", "license": "MIT",
@@ -1320,7 +1320,7 @@
"node": ">=6" "node": ">=6"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/cli-truncate": { "node_modules/cli-truncate": {
@@ -1338,7 +1338,7 @@
"node": ">=8" "node": ">=8"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/cliui": { "node_modules/cliui": {
@@ -1376,7 +1376,7 @@
"mimic-response": "^1.0.0" "mimic-response": "^1.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/color-convert": { "node_modules/color-convert": {
@@ -1513,7 +1513,7 @@
"glob": "dist/esm/bin.mjs" "glob": "dist/esm/bin.mjs"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/config-file-ts/node_modules/minimatch": { "node_modules/config-file-ts/node_modules/minimatch": {
@@ -1529,7 +1529,7 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/config-file-ts/node_modules/minipass": { "node_modules/config-file-ts/node_modules/minipass": {
@@ -1641,7 +1641,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/decompress-response/node_modules/mimic-response": { "node_modules/decompress-response/node_modules/mimic-response": {
@@ -1654,7 +1654,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/defaults": { "node_modules/defaults": {
@@ -1667,7 +1667,7 @@
"clone": "^1.0.2" "clone": "^1.0.2"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/defer-to-connect": { "node_modules/defer-to-connect": {
@@ -2015,7 +2015,7 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0" "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/err-code": { "node_modules/err-code": {
@@ -2201,7 +2201,7 @@
"node": ">=14" "node": ">=14"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/foreground-child/node_modules/signal-exit": { "node_modules/foreground-child/node_modules/signal-exit": {
@@ -2214,7 +2214,7 @@
"node": ">=14" "node": ">=14"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
@@ -2283,7 +2283,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/gauge": { "node_modules/gauge": {
@@ -2339,7 +2339,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/get-proto": { "node_modules/get-proto": {
@@ -2369,7 +2369,7 @@
"node": ">=8" "node": ">=8"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/glob": { "node_modules/glob": {
@@ -2391,7 +2391,7 @@
"node": "*" "node": "*"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/glob/node_modules/balanced-match": { "node_modules/glob/node_modules/balanced-match": {
@@ -2435,7 +2435,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/got": { "node_modules/got": {
@@ -2461,7 +2461,7 @@
"node": ">=10.19.0" "node": ">=10.19.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1" "url": "https://www.xinmi.cloud/sindresorhus/got?sponsor=1"
} }
}, },
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
@@ -2490,7 +2490,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": { "node_modules/has-tostringtag": {
@@ -2506,7 +2506,7 @@
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/has-unicode": { "node_modules/has-unicode": {
@@ -2640,7 +2640,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/feross" "url": "https://www.xinmi.cloud/sponsors/feross"
}, },
{ {
"type": "patreon", "type": "patreon",
@@ -2759,7 +2759,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/isarray": { "node_modules/isarray": {
@@ -2780,7 +2780,7 @@
"node": ">= 18.0.0" "node": ">= 18.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/gjtorikian/" "url": "https://www.xinmi.cloud/sponsors/gjtorikian/"
} }
}, },
"node_modules/isexe": { "node_modules/isexe": {
@@ -2800,7 +2800,7 @@
"@isaacs/cliui": "^8.0.2" "@isaacs/cliui": "^8.0.2"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
}, },
"optionalDependencies": { "optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0" "@pkgjs/parseargs": "^0.11.0"
@@ -3015,7 +3015,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/lowercase-keys": { "node_modules/lowercase-keys": {
@@ -3200,7 +3200,7 @@
"node": "18 || 20 || >=22" "node": "18 || 20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
@@ -3210,7 +3210,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://www.xinmi.cloud/sponsors/ljharb"
} }
}, },
"node_modules/minipass": { "node_modules/minipass": {
@@ -3443,7 +3443,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/npmlog": { "node_modules/npmlog": {
@@ -3486,7 +3486,7 @@
"node": ">=6" "node": ">=6"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/ora": { "node_modules/ora": {
@@ -3510,7 +3510,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/p-cancelable": { "node_modules/p-cancelable": {
@@ -3536,7 +3536,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/p-map": { "node_modules/p-map": {
@@ -3552,7 +3552,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/package-json-from-dist": { "node_modules/package-json-from-dist": {
@@ -3596,7 +3596,7 @@
"node": ">=16 || 14 >=14.18" "node": ">=16 || 14 >=14.18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/path-scurry/node_modules/lru-cache": { "node_modules/path-scurry/node_modules/lru-cache": {
@@ -3628,7 +3628,7 @@
}, },
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/jet2jet" "url": "https://www.xinmi.cloud/sponsors/jet2jet"
} }
}, },
"node_modules/pend": { "node_modules/pend": {
@@ -3730,7 +3730,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/read-binary-file-arch": { "node_modules/read-binary-file-arch": {
@@ -3830,7 +3830,7 @@
}, },
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/jet2jet" "url": "https://www.xinmi.cloud/sponsors/jet2jet"
} }
}, },
"node_modules/resolve-alpn": { "node_modules/resolve-alpn": {
@@ -3850,7 +3850,7 @@
"lowercase-keys": "^2.0.0" "lowercase-keys": "^2.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/restore-cursor": { "node_modules/restore-cursor": {
@@ -3891,7 +3891,7 @@
"rimraf": "bin.js" "rimraf": "bin.js"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://www.xinmi.cloud/sponsors/isaacs"
} }
}, },
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
@@ -3902,7 +3902,7 @@
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/feross" "url": "https://www.xinmi.cloud/sponsors/feross"
}, },
{ {
"type": "patreon", "type": "patreon",
@@ -4464,7 +4464,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "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/wrap-ansi-cjs": {
@@ -4483,7 +4483,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/wrappy": { "node_modules/wrappy": {
@@ -4570,7 +4570,7 @@
"node": ">=10" "node": ">=10"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://www.xinmi.cloud/sponsors/sindresorhus"
} }
}, },
"node_modules/zip-stream": { "node_modules/zip-stream": {
+10 -4
View File
@@ -2,21 +2,27 @@
"name": "hermes-studio", "name": "hermes-studio",
"version": "0.6.8", "version": "0.6.8",
"description": "Hermes Studio desktop distribution with bundled Python runtime and hermes-agent", "description": "Hermes Studio desktop distribution with bundled Python runtime and hermes-agent",
"homepage": "https://ekkolearnai.com", "homepage": "https://www.xinmi.cloud",
"author": { "author": {
"name": "Hermes Studio Contributors", "name": "Hermes Studio Contributors",
"email": "noreply@hermes-studio.local" "email": "noreply@hermes-studio.local"
}, },
"license": "MIT", "license": "BSL-1.1",
"private": true, "private": true,
"main": "dist/main/index.js", "main": "dist/main/index.js",
"scripts": { "scripts": {
"build:main": "tsc -p tsconfig.json", "build:main": "tsc -p tsconfig.json",
"build": "npm run build:main", "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", "fetch:python": "node scripts/fetch-python.mjs",
"install:hermes": "node scripts/install-hermes.mjs", "install:hermes": "node scripts/install-hermes.mjs",
"patch:hermes": "node scripts/apply-hermes-patches.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", "prune:python": "node scripts/prune-python.mjs",
"dev": "npm run build:main && electron .", "dev": "npm run build:main && electron .",
"dist": "npm run build && electron-builder", "dist": "npm run build && electron-builder",
@@ -33,4 +39,4 @@
"dependencies": { "dependencies": {
"electron-updater": "^6.3.9" "electron-updater": "^6.3.9"
} }
} }
@@ -33,6 +33,7 @@ const sitePkgs = process.env.HERMES_AGENT_SITE_PACKAGES ?? (
) )
const dtPath = join(sitePkgs, 'gateway', 'platforms', 'dingtalk.py') const dtPath = join(sitePkgs, 'gateway', 'platforms', 'dingtalk.py')
const browserToolPath = join(sitePkgs, 'tools', 'browser_tool.py')
const sitecustomizePath = join(sitePkgs, 'sitecustomize.py') const sitecustomizePath = join(sitePkgs, 'sitecustomize.py')
if (!existsSync(dtPath)) { if (!existsSync(dtPath)) {
console.error(`dingtalk.py not found at ${dtPath} — is hermes-agent installed?`) console.error(`dingtalk.py not found at ${dtPath} — is hermes-agent installed?`)
@@ -59,6 +60,21 @@ function patch(id, marker, find, replace) {
applied++ 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}`) console.log(`Patching ${dtPath}`)
// NOTE: the former `dt-pre-start` patch was retired — hermes-agent now ships // NOTE: the former `dt-pre-start` patch was retired — hermes-agent now ships
@@ -179,6 +195,63 @@ if (src !== before) {
writeFileSync(dtPath, src) 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 brotlicffiCompatMarker = '# patch:brotlicffi-error-compat'
const brotlicffiCompat = ` const brotlicffiCompat = `
${brotlicffiCompatMarker} ${brotlicffiCompatMarker}
@@ -194,15 +267,76 @@ except Exception:
pass pass
` `
const sitecustomize = existsSync(sitecustomizePath) ? readFileSync(sitecustomizePath, 'utf-8') : '' const desktopHiddenSubprocessMarker = '# patch:desktop-hidden-subprocess-defaults'
if (sitecustomize.includes(brotlicffiCompatMarker)) { const desktopHiddenSubprocessDefaults = `
console.log(' · brotlicffi-error-compat (already applied)') ${desktopHiddenSubprocessMarker}
skipped++ try:
} else { import os as _hermes_os
const nextSitecustomize = `${sitecustomize.replace(/\s*$/, '')}\n${brotlicffiCompat.trim()}\n` 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(marker)) {
console.log(` · ${id} (already applied)`)
skipped++
return
}
const nextSitecustomize = `${sitecustomize.replace(/\s*$/, '')}\n${body.trim()}\n`
writeFileSync(sitecustomizePath, nextSitecustomize) writeFileSync(sitecustomizePath, nextSitecustomize)
console.log(' ✓ brotlicffi-error-compat') console.log(`${id}`)
applied++ applied++
} }
appendSitecustomizePatch('brotlicffi-error-compat', brotlicffiCompatMarker, brotlicffiCompat)
appendSitecustomizePatch('desktop-hidden-subprocess-defaults', desktopHiddenSubprocessMarker, desktopHiddenSubprocessDefaults)
console.log(`Done. Applied ${applied}, skipped ${skipped}.`) 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}`)
+371 -38
View File
@@ -1,22 +1,69 @@
#!/usr/bin/env node #!/usr/bin/env node
// Install hermes-agent into the bundled Python at resources/python/<os>-<arch>/. // Install hermes-agent into the bundled Python at resources/python/<os>-<arch>/.
// Prefers `uv` (10-100x faster, more deterministic) and falls back to pip. // Prefers `uv` (10-100x faster, more deterministic) and falls back to pip.
import { existsSync } from 'node:fs' import {
import { resolve, dirname } from 'node:path' 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 { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process' 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 __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dirname, '..') const ROOT = resolve(__dirname, '..')
const TARGET_OS = process.env.TARGET_OS || osPlatform() const TARGET_OS = process.env.TARGET_OS || osPlatform()
const TARGET_ARCH = process.env.TARGET_ARCH || osArch() const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
const HERMES_VERSION = process.env.HERMES_VERSION || '0.15.2' const HERMES_VERSION = hermesVersion()
const HERMES_PACKAGE = process.env.HERMES_PACKAGE || `hermes-agent[mcp]==${HERMES_VERSION}` // 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 OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
const PY_DIR = resolve(ROOT, 'resources', 'python', `${OS_LABEL}-${TARGET_ARCH}`) 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' const pyBin = TARGET_OS === 'win32'
? resolve(PY_DIR, 'python.exe') ? resolve(PY_DIR, 'python.exe')
@@ -32,34 +79,309 @@ function hasUv() {
return r.status === 0 return r.status === 0
} }
let r function splitPackageList(value) {
if (hasUv()) { return value
console.log(`→ Installing ${HERMES_PACKAGE} via uv`) .split(/[,\s]+/)
r = spawnSync('uv', [ .map(part => part.trim())
'pip', 'install', .filter(Boolean)
'--python', pyBin,
HERMES_PACKAGE,
], { stdio: 'inherit' })
} else {
console.log(`→ Installing ${HERMES_PACKAGE} via pip`)
r = spawnSync(pyBin, [
'-m', 'pip', 'install',
HERMES_PACKAGE,
'--no-warn-script-location',
'--disable-pip-version-check',
], { stdio: 'inherit' })
} }
if (r.status !== 0) process.exit(r.status ?? 1)
r = spawnSync(pyBin, [ function run(command, args, options = {}) {
'-c', const result = spawnSync(command, args, { stdio: 'inherit', ...options })
'import mcp; import tools.mcp_tool as t; assert t._MCP_AVAILABLE', if (result.status !== 0) process.exit(result.status ?? 1)
], { stdio: 'inherit' }) return result
if (r.status !== 0) {
console.error('MCP Python SDK sanity check failed')
process.exit(r.status ?? 1)
} }
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 ${label} via uv: ${packages.join(' ')}`)
run('uv', [
'pip', 'install',
'--python', pyBin,
...packages,
], { env })
} else {
console.log(`→ Installing ${label} via pip: ${packages.join(' ')}`)
run(pyBin, [
'-m', 'pip', 'install',
...packages,
'--no-warn-script-location',
'--disable-pip-version-check',
], { env })
}
}
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' const hermesBin = TARGET_OS === 'win32'
? resolve(PY_DIR, 'Scripts', 'hermes.exe') ? resolve(PY_DIR, 'Scripts', 'hermes.exe')
: resolve(PY_DIR, 'bin', 'hermes') : resolve(PY_DIR, 'bin', 'hermes')
@@ -76,14 +398,11 @@ if (!existsSync(hermesBin)) {
// it at the venv root with a *relative* symlink so the venv stays portable when copied // 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 // into the packaged .app/.exe (an absolute symlink would break the moment the bundle
// is moved to /Applications/...). // is moved to /Applications/...).
const { readdirSync, symlinkSync, copyFileSync, unlinkSync, lstatSync } = await import('node:fs')
function siteRunAgentRelative() { function siteRunAgentRelative() {
if (TARGET_OS === 'win32') { if (TARGET_OS === 'win32') {
return ['Lib', 'site-packages', 'run_agent.py'].join('\\') return ['Lib', 'site-packages', 'run_agent.py'].join('\\')
} }
const libDir = resolve(PY_DIR, 'lib') return `${sitePackagesDir().slice(PY_DIR.length + 1)}/run_agent.py`
const py = readdirSync(libDir).find(n => /^python\d+\.\d+$/.test(n))
return ['lib', py, 'site-packages', 'run_agent.py'].join('/')
} }
{ {
const relSrc = siteRunAgentRelative() const relSrc = siteRunAgentRelative()
@@ -102,7 +421,6 @@ function siteRunAgentRelative() {
// Relocate: replace the pip-generated launcher (which embeds an absolute // Relocate: replace the pip-generated launcher (which embeds an absolute
// shebang to the build-time Python path) with a relative wrapper so the // shebang to the build-time Python path) with a relative wrapper so the
// bundled venv works after being moved into the .app/.exe payload. // bundled venv works after being moved into the .app/.exe payload.
const { writeFileSync, chmodSync } = await import('node:fs')
if (TARGET_OS === 'win32') { if (TARGET_OS === 'win32') {
// Windows: pip generates a .exe launcher that embeds a relative shebang // Windows: pip generates a .exe launcher that embeds a relative shebang
// already. Add a .cmd wrapper that prefers the colocated python.exe. // 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)`) console.log(`✓ hermes installed at ${hermesBin} (relocatable launcher)`)
r = spawnSync(hermesCheckCommand, hermesCheckArgs, { stdio: 'inherit' }) run(hermesCheckCommand, hermesCheckArgs)
if (r.status !== 0) {
console.error('hermes --version failed') if (!SKIP_BROWSER_RUNTIME) {
process.exit(r.status ?? 1) 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)
})
})
}
+112 -8
View File
@@ -4,6 +4,9 @@ import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
import { desktopIcon, desktopTrayTemplateIcon, desktopWindowsTrayIcon, hermesBinExists, hermesBin } from './paths' import { desktopIcon, desktopTrayTemplateIcon, desktopWindowsTrayIcon, hermesBinExists, hermesBin } from './paths'
import { checkForDesktopUpdates, initAutoUpdater } from './updater' import { checkForDesktopUpdates, initAutoUpdater } from './updater'
import { t } from './desktop-i18n' 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 PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
const START_HIDDEN = process.argv.includes('--hidden') const START_HIDDEN = process.argv.includes('--hidden')
@@ -13,6 +16,7 @@ let mainWindow: BrowserWindow | null = null
let serverUrl: string | null = null let serverUrl: string | null = null
let tray: Tray | null = null let tray: Tray | null = null
let isQuitting = false let isQuitting = false
let isBootstrapping = false
function showMainWindow() { function showMainWindow() {
if (!mainWindow) { if (!mainWindow) {
@@ -166,25 +170,91 @@ function splashHtml(): string {
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Hermes Studio</title> const html = `<!doctype html><html><head><meta charset="utf-8"><title>Hermes Studio</title>
<style> <style>
html,body{margin:0;height:100%;background:#1a1a1a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;} 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} .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}} @keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
.row{display:flex;gap:8px} .row{display:flex;gap:8px}
.row .dot:nth-child(2){animation-delay:.2s}.row .dot:nth-child(3){animation-delay:.4s} .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} h1{font-weight:500;margin:0;font-size:18px}
</style></head><body><div class="wrap"> </style></head><body><div class="wrap">
<h1>Hermes Studio</h1> <h1>Hermes Studio</h1>
<div class="row"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div> <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>` </div></body></html>`
return 'data:text/html;charset=utf-8,' + encodeURIComponent(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() { 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()) { if (!hermesBinExists()) {
console.error(`hermes binary missing at ${hermesBin()}`) 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 { try {
@@ -201,15 +271,28 @@ async function bootstrap() {
</body></html>`, </body></html>`,
)) ))
} }
} finally {
isBootstrapping = false
} }
} }
ipcMain.handle('hermes-desktop:get-token', () => getToken()) 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()
return
}
const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
app.quit()
} else {
app.on('second-instance', (_event, argv) => { app.on('second-instance', (_event, argv) => {
if (argv.includes('--quit')) { if (argv.includes('--quit')) {
quitApp() quitApp()
@@ -229,6 +312,15 @@ if (!gotLock) {
// visual clutter. macOS keeps a menu (system requirement) but Electron's // visual clutter. macOS keeps a menu (system requirement) but Electron's
// default is fine there. // default is fine there.
if (process.platform !== 'darwin') Menu.setApplicationMenu(null) 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() createTray()
createWindow() createWindow()
bootstrap() bootstrap()
@@ -262,3 +354,15 @@ if (!gotLock) {
app.exit(0) 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 { app } from 'electron'
import { existsSync } from 'node:fs' import { existsSync, readdirSync } from 'node:fs'
import { join, resolve } from 'node:path' import { join, resolve } from 'node:path'
import { homedir, platform, arch } from 'node:os' import { homedir, platform, arch } from 'node:os'
@@ -23,17 +23,120 @@ export function webuiServerEntry(): string {
return join(webuiDir(), 'dist', 'server', 'index.js') 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> // dev: packages/desktop/resources/python/<os>-<arch>
// prod: <resources>/python // prod: <resources>/python when present, otherwise downloaded runtime cache.
export function pythonDir(): string { export function pythonDir(): string {
if (app.isPackaged) return resolve(process.resourcesPath, 'python') if (app.isPackaged) {
return resolve(app.getAppPath(), 'resources', 'python', `${osLabel}-${archLabel}`) 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 { export function hermesBin(): string {
const dir = pythonDir() return isWin ? join(pythonBinDir(), 'hermes.exe') : join(pythonBinDir(), 'hermes')
return isWin ? join(dir, 'Scripts', 'hermes.exe') : join(dir, 'bin', 'hermes')
} }
export function hermesBinExists(): boolean { export function hermesBinExists(): boolean {
@@ -63,12 +166,23 @@ export function hermesHome(): string {
const override = process.env.HERMES_HOME?.trim() const override = process.env.HERMES_HOME?.trim()
if (override) return resolve(override) if (override) return resolve(override)
const defaultHome = resolve(homedir(), '.hermes')
if (isWin) { if (isWin) {
const localAppData = process.env.LOCALAPPDATA?.trim() || process.env.APPDATA?.trim() const candidates = [
if (localAppData) return resolve(localAppData, 'hermes') 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 { 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 checking = false
let updateDownloaded = false let updateDownloaded = false
const LATEST_RELEASE_URL = 'https://api.github.com/repos/EKKOLearnAI/hermes-web-ui/releases/latest' const LATEST_RELEASE_URL = 'https://api.www.xinmi.cloud/repos/EKKOLearnAI/hermes-web-ui/releases/latest'
const CLOUDFLARE_DOWNLOAD_BASE_URL = 'https://download.ekkolearnai.com' const CLOUDFLARE_DOWNLOAD_BASE_URL = 'https://download.www.xinmi.cloud'
interface GitHubRelease { interface GitHubRelease {
tag_name?: string tag_name?: string
+104 -5
View File
@@ -6,10 +6,25 @@ import { dirname, delimiter, join } from 'node:path'
import { randomBytes } from 'node:crypto' import { randomBytes } from 'node:crypto'
import { promisify } from 'node:util' import { promisify } from 'node:util'
import { app } from 'electron' 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_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) const execFileAsync = promisify(execFile)
let serverProc: ChildProcess | null = null let serverProc: ChildProcess | null = null
@@ -47,6 +62,60 @@ function readyTimeoutMs(): number {
return envPositiveInt('HERMES_DESKTOP_READY_TIMEOUT_MS') || DEFAULT_READY_TIMEOUT_MS 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 { function ensureToken(): string {
if (cachedToken) return cachedToken if (cachedToken) return cachedToken
const file = tokenFile() const file = tokenFile()
@@ -231,17 +300,28 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
const bundledPython = isWin const bundledPython = isWin
? join(pythonDir(), 'python.exe') ? join(pythonDir(), 'python.exe')
: join(pythonDir(), 'bin', 'python3') : 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 bridgePort = await getFreeTcpPort()
const workerPortBase = await getFreeTcpPortInRange(20000, 59000) const workerPortBase = await getFreeTcpPortInRange(20000, 59000)
const loginShellPath = await getLoginShellPath() const loginShellPath = await getLoginShellPath()
const nvmNodeBinPaths = getNvmNodeBinPaths() const nvmNodeBinPaths = getNvmNodeBinPaths()
const runtimePath = mergePathEntries( const runtimePath = mergePathEntries(
dirname(hermesBin()), dirname(hermesBin()),
bundledAgentBrowserBin,
bundledNodeBin,
bundledGitPath,
loginShellPath, loginShellPath,
nvmNodeBinPaths, nvmNodeBinPaths,
process.env.PATH, process.env.PATH,
process.env.Path,
COMMON_USER_BIN_DIRS.join(delimiter), 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. // Run via Electron's "run as Node" mode — Electron binary doubles as Node.
const env: NodeJS.ProcessEnv = { const env: NodeJS.ProcessEnv = {
@@ -256,11 +336,21 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
HERMES_AGENT_BRIDGE_PYTHON: bundledPython, HERMES_AGENT_BRIDGE_PYTHON: bundledPython,
HERMES_AGENT_CLI_PYTHON: bundledPython, HERMES_AGENT_CLI_PYTHON: bundledPython,
HERMES_AGENT_ROOT: pythonDir(), 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/...` // Force TCP loopback for the agent bridge. The default `ipc:///tmp/...`
// unix socket is rejected on macOS in some EDR/sandbox setups (silent // 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 // SIGKILL of the bridge child within ~150ms). TCP on 127.0.0.1 works
// identically and avoids the issue cross-platform. // identically and avoids the issue cross-platform.
HERMES_AGENT_BRIDGE_ENDPOINT: `tcp://127.0.0.1:${bridgePort}`, 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 // Force TCP for worker endpoints too (upstream #1106). Same EDR/sandbox
// reason as above — default ipc:// unix sockets in /tmp get killed. // reason as above — default ipc:// unix sockets in /tmp get killed.
HERMES_AGENT_BRIDGE_WORKER_TRANSPORT: 'tcp', 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. // HERMES_HOME/.env or by configuring per-platform allowlists.
GATEWAY_ALLOW_ALL_USERS: process.env.GATEWAY_ALLOW_ALL_USERS ?? 'true', GATEWAY_ALLOW_ALL_USERS: process.env.GATEWAY_ALLOW_ALL_USERS ?? 'true',
// Keep the bundled Hermes Agent, bridge, gateway, and Web UI path helpers // Keep the bundled Hermes Agent, bridge, gateway, and Web UI path helpers
// on the same data directory. Native Windows uses %LOCALAPPDATA%\hermes; // on the same data directory. Native Windows uses an existing
// macOS/Linux keep the standard ~/.hermes layout. // %LOCALAPPDATA%\hermes or %APPDATA%\hermes; otherwise all platforms keep
// the standard ~/.hermes layout.
HERMES_HOME: agentHome, HERMES_HOME: agentHome,
HERMES_WEB_UI_HOME: home, HERMES_WEB_UI_HOME: home,
HERMES_WEBUI_STATE_DIR: home, HERMES_WEBUI_STATE_DIR: home,
@@ -295,10 +386,14 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
windowsHide: true, windowsHide: true,
}) })
const bridgeStartup = createAgentBridgeStartupTracker()
serverProc.stdout?.on('data', (chunk: Buffer) => { serverProc.stdout?.on('data', (chunk: Buffer) => {
bridgeStartup.observe(chunk)
process.stdout.write(`[webui] ${chunk}`) process.stdout.write(`[webui] ${chunk}`)
}) })
serverProc.stderr?.on('data', (chunk: Buffer) => { serverProc.stderr?.on('data', (chunk: Buffer) => {
bridgeStartup.observe(chunk)
process.stderr.write(`[webui] ${chunk}`) process.stderr.write(`[webui] ${chunk}`)
}) })
serverProc.on('exit', (code, signal) => { 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) return getServerUrl(port)
} }
+1
View File
@@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('hermesDesktop', { contextBridge.exposeInMainWorld('hermesDesktop', {
getToken: (): Promise<string> => ipcRenderer.invoke('hermes-desktop:get-token'), getToken: (): Promise<string> => ipcRenderer.invoke('hermes-desktop:get-token'),
retryBootstrap: (): Promise<void> => ipcRenderer.invoke('hermes-desktop:retry-bootstrap'),
platform: process.platform, platform: process.platform,
isDesktop: true, isDesktop: true,
}) })
@@ -23,6 +23,7 @@ function appendPinoContext(message: string, obj: any): string {
parts.push(`profile=${obj.profile}`) parts.push(`profile=${obj.profile}`)
} }
if (obj.request?.action) parts.push(`action=${obj.request.action}`) 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.sessionId) parts.push(`session=${obj.sessionId}`)
if (obj.runId) parts.push(`run=${obj.runId}`) if (obj.runId) parts.push(`run=${obj.runId}`)
if (obj.status) parts.push(`status=${obj.status}`) if (obj.status) parts.push(`status=${obj.status}`)
+3 -3
View File
@@ -107,7 +107,7 @@ function normalizeGithubRepoUrl(raw: string): string {
return raw return raw
.trim() .trim()
.replace(/^git\+/, '') .replace(/^git\+/, '')
.replace(/^git@github\.com:/, 'https://github.com/') .replace(/^git@github\.com:/, 'https://www.xinmi.cloud/')
.replace(/\.git$/, '') .replace(/\.git$/, '')
} }
@@ -127,7 +127,7 @@ function getPreviewRepoApiUrl(): string {
const baseUrl = getPreviewRepoBaseUrl() const baseUrl = getPreviewRepoBaseUrl()
const match = baseUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/) const match = baseUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/)
if (!match) throw new Error(`Preview zip fallback only supports GitHub repositories: ${baseUrl}`) 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 } { function getPreviewGithubRepoParts(): { owner: string; repo: string } {
@@ -902,7 +902,7 @@ async function downloadGithubZip(ref: string, targetDir: string, type: 'tag' | '
const { owner, repo } = getPreviewGithubRepoParts() const { owner, repo } = getPreviewGithubRepoParts()
const refKind = type === 'branch' ? 'heads' : 'tags' const refKind = type === 'branch' ? 'heads' : 'tags'
const archiveKind = process.platform === 'win32' ? 'zip' : 'tar.gz' 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}`) appendPreviewActionLog(`download archive: ${url}`)
const res = await fetch(url, { const res = await fetch(url, {
headers: { 'User-Agent': 'hermes-web-ui-preview' }, 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 DEFAULT_AGENT_BRIDGE_RESTART_DELAY_MS = 1000
const MAX_AGENT_BRIDGE_RESTART_DELAY_MS = 30000 const MAX_AGENT_BRIDGE_RESTART_DELAY_MS = 30000
const OPENROUTER_WEB_UI_ATTRIBUTION_ENV = { 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_TITLE: 'Hermes Web UI',
HERMES_OPENROUTER_APP_CATEGORIES: 'cli-agent,personal-agent', HERMES_OPENROUTER_APP_CATEGORIES: 'cli-agent,personal-agent',
} as const } as const
@@ -3,16 +3,16 @@
* *
* Mirrors the upstream hermes-agent implementation * Mirrors the upstream hermes-agent implementation
* (`hermes_cli/copilot_auth.py:155-275`): * (`hermes_cli/copilot_auth.py:155-275`):
* - POST https://github.com/login/device/code → device_code, user_code, verification_uri * - POST https://www.xinmi.cloud/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/oauth/access_token → access_token (after user approves)
* - Polling rules per RFC 8628: authorization_pending, slow_down, expired_token, access_denied * - Polling rules per RFC 8628: authorization_pending, slow_down, expired_token, access_denied
* *
* Client ID `Ov23li8tweQw6odWQebz` is reused from upstream hermes-agent for now; * 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. * 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_DEVICE_CODE_URL = 'https://www.xinmi.cloud/login/device/code'
const GITHUB_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token' const GITHUB_ACCESS_TOKEN_URL = 'https://www.xinmi.cloud/login/oauth/access_token'
export const COPILOT_OAUTH_CLIENT_ID = 'Ov23li8tweQw6odWQebz' export const COPILOT_OAUTH_CLIENT_ID = 'Ov23li8tweQw6odWQebz'
export const COPILOT_OAUTH_SCOPE = 'read:user' export const COPILOT_OAUTH_SCOPE = 'read:user'
const FETCH_TIMEOUT_MS = 15_000 const FETCH_TIMEOUT_MS = 15_000
@@ -6,7 +6,7 @@ import { join } from 'path'
const execFileAsync = promisify(execFile) 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 COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models'
const EDITOR_VERSION = 'vscode/1.104.1' const EDITOR_VERSION = 'vscode/1.104.1'
const PLUGIN_VERSION = 'copilot-chat/0.20.0' const PLUGIN_VERSION = 'copilot-chat/0.20.0'
@@ -2,11 +2,12 @@
* Hermes - * Hermes -
* *
* Hermes * Hermes
* - Windows : %LOCALAPPDATA%\hermes * - Windows : %LOCALAPPDATA%\hermes when it exists
* - Linux/macOS/WSL2: ~/.hermes * - Linux/macOS/WSL2: ~/.hermes
* - 用户自定义: HERMES_HOME * - 用户自定义: HERMES_HOME
*/ */
import { existsSync } from 'fs'
import { basename, dirname, isAbsolute, relative, resolve, join } from 'path' import { basename, dirname, isAbsolute, relative, resolve, join } from 'path'
import { homedir } from 'os' import { homedir } from 'os'
@@ -15,7 +16,7 @@ import { homedir } from 'os'
* *
* *
* 1. HERMES_HOME * 1. HERMES_HOME
* 2. Windows: %LOCALAPPDATA%\hermes * 2. Windows: existing %LOCALAPPDATA%\hermes or %APPDATA%\hermes
* 3. : ~/.hermesLinux/macOS/WSL2 * 3. : ~/.hermesLinux/macOS/WSL2
* *
* @returns Hermes * @returns Hermes
@@ -26,16 +27,25 @@ export function detectHermesHome(): string {
return resolve(process.env.HERMES_HOME) return resolve(process.env.HERMES_HOME)
} }
// 2. Windows:直接使用 %LOCALAPPDATA%\hermes const defaultHome = resolve(homedir(), '.hermes')
// 2. Windows:优先使用存在的原生安装数据目录;不存在时回退到 ~/.hermes。
if (process.platform === 'win32') { if (process.platform === 'win32') {
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA const candidates = [
if (localAppData) { process.env.LOCALAPPDATA,
return join(localAppData, 'hermes') 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 // 3. Linux/macOS~/.hermes
return resolve(homedir(), '.hermes') return defaultHome
} }
/** /**
@@ -137,7 +137,7 @@ onMounted(() => {
</button> </button>
<a <a
class="btn-outline" class="btn-outline"
href="https://github.com/EKKOLearnAI/hermes-web-ui" href="https://www.xinmi.cloud/root/Hermes-ui"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
> >
@@ -15,10 +15,10 @@ const activeTab = ref<'desktop' | 'npm' | 'docker' | 'source'>('desktop')
const releaseVersion = __APP_VERSION__.replace(/^v/, '') const releaseVersion = __APP_VERSION__.replace(/^v/, '')
const releaseTag = `v${releaseVersion}` 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 releaseUrl = `${releaseBaseUrl}/tag/${releaseTag}`
const githubDownloadUrl = `${releaseBaseUrl}/download/${releaseTag}` const githubDownloadUrl = `${releaseBaseUrl}/download/${releaseTag}`
const cloudflareDownloadUrl = `https://download.ekkolearnai.com/${releaseTag}` const cloudflareDownloadUrl = `https://download.www.xinmi.cloud/${releaseTag}`
const desktopDownloads = computed(() => const desktopDownloads = computed(() =>
(tm('install.desktop.downloads') as DesktopDownload[]).map((item) => { (tm('install.desktop.downloads') as DesktopDownload[]).map((item) => {
const assetName = `Hermes.Studio-${releaseVersion}-${item.assetSuffix}` const assetName = `Hermes.Studio-${releaseVersion}-${item.assetSuffix}`
@@ -17,7 +17,7 @@ const chartSrc = computed(() => {
onMounted(async () => { onMounted(async () => {
try { 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() const data = await res.json()
stars.value = data.stargazers_count stars.value = data.stargazers_count
} catch {} } catch {}
@@ -32,7 +32,7 @@ onMounted(async () => {
<div class="star-badges reveal reveal-delay-1"> <div class="star-badges reveal reveal-delay-1">
<a <a
class="star-btn" class="star-btn"
href="https://github.com/EKKOLearnAI/hermes-web-ui" href="https://www.xinmi.cloud/root/Hermes-ui"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
> >
@@ -19,7 +19,7 @@ const { t } = useI18n()
<p class="footer-meta">{{ t('footer.license') }}</p> <p class="footer-meta">{{ t('footer.license') }}</p>
<a <a
class="footer-github" class="footer-github"
href="https://github.com/EKKOLearnAI/hermes-web-ui" href="https://www.xinmi.cloud/root/Hermes-ui"
target="_blank" target="_blank"
rel="noopener" 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" @click.prevent="navigateTo('docs.getting-started')">{{ t('nav.docs') }}</a>
<a <a
class="nav-link" class="nav-link"
href="https://github.com/EKKOLearnAI/hermes-web-ui" href="https://www.xinmi.cloud/root/Hermes-ui"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
> >
@@ -84,7 +84,7 @@ function goHome() {
<div class="mobile-menu-inner" @click.stop> <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('landing')">{{ t('nav.home') }}</a>
<a class="mobile-link" @click.prevent="navigateTo('docs.getting-started')">{{ t('nav.docs') }}</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"> <div class="mobile-actions">
<button class="mobile-action-btn" @click="switchLocale"> <button class="mobile-action-btn" @click="switchLocale">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="action-icon"> <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: { source: {
title: 'From 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', cmd2: 'cd hermes-web-ui && npm install && npm run dev',
}, },
prereq: 'Requires Node.js >= 23', prereq: 'Requires Node.js >= 23',
+1 -1
View File
@@ -156,7 +156,7 @@ export default {
}, },
source: { source: {
title: '源码安装', 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', cmd2: 'cd hermes-web-ui && npm install && npm run dev',
}, },
prereq: '需要 Node.js >= 23', 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 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 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 }}')) { if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) {
fail('desktop-release.yml must upload matrix-specific 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') 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) { if (failures.length > 0) {
console.error('Harness check failed:') console.error('Harness check failed:')
for (const failure of failures) { for (const failure of failures) {
+4 -4
View File
@@ -45,7 +45,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
mockApi.startCopilotLogin.mockResolvedValue({ mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-1', session_id: 'sess-1',
user_code: 'ABCD-1234', user_code: 'ABCD-1234',
verification_url: 'https://github.com/login/device', verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900, expires_in: 900,
interval: 5, interval: 5,
}) })
@@ -62,7 +62,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
mockApi.startCopilotLogin.mockResolvedValue({ mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-2', session_id: 'sess-2',
user_code: 'WXYZ-9999', user_code: 'WXYZ-9999',
verification_url: 'https://github.com/login/device', verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900, expires_in: 900,
interval: 5, interval: 5,
}) })
@@ -87,7 +87,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
mockApi.startCopilotLogin.mockResolvedValue({ mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-3', session_id: 'sess-3',
user_code: 'EXPI-RED!', user_code: 'EXPI-RED!',
verification_url: 'https://github.com/login/device', verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900, expires_in: 900,
interval: 5, interval: 5,
}) })
@@ -116,7 +116,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
mockApi.startCopilotLogin.mockResolvedValue({ mockApi.startCopilotLogin.mockResolvedValue({
session_id: 'sess-4', session_id: 'sess-4',
user_code: 'NOPE', user_code: 'NOPE',
verification_url: 'https://github.com/login/device', verification_url: 'https://www.xinmi.cloud/login/device',
expires_in: 900, expires_in: 900,
interval: 5, 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 { buildAgentBridgeProcessEnv } = await import('../../packages/server/src/services/hermes/agent-bridge/manager')
const env = buildAgentBridgeProcessEnv('ipc:///tmp/test.sock', '/tmp/hermes-home', '/tmp/hermes-agent') 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_TITLE).toBe('Hermes Web UI')
expect(env.HERMES_OPENROUTER_APP_CATEGORIES).toBe('cli-agent,personal-agent') 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') 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 () => { it('waits briefly for a restarting bridge socket before failing', async () => {
const endpoint = process.platform === 'win32' const endpoint = process.platform === 'win32'
? `tcp://127.0.0.1:${32000 + (process.pid % 10000)}` ? `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({ const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
device_code: 'DC-1', device_code: 'DC-1',
user_code: 'USER-1234', user_code: 'USER-1234',
verification_uri: 'https://github.com/login/device', verification_uri: 'https://www.xinmi.cloud/login/device',
expires_in: 900, expires_in: 900,
interval: 5, interval: 5,
})) }))
const data = await startDeviceFlow(fetchSpy as any) const data = await startDeviceFlow(fetchSpy as any)
expect(data.device_code).toBe('DC-1') expect(data.device_code).toBe('DC-1')
expect(data.user_code).toBe('USER-1234') 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.expires_in).toBe(900)
expect(data.interval).toBe(5) expect(data.interval).toBe(5)
const [url, init] = fetchSpy.mock.calls[0] 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') expect(init.method).toBe('POST')
const body = String(init.body) const body = String(init.body)
expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`) expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`)
@@ -57,7 +57,7 @@ describe('startDeviceFlow', () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
device_code: 'DC-2', device_code: 'DC-2',
user_code: 'AAAA', 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) const data = await startDeviceFlow(fetchSpy as any)
expect(data.expires_in).toBe(900) expect(data.expires_in).toBe(900)
@@ -130,7 +130,7 @@ describe('pollDeviceFlow', () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ access_token: 'gho_x' })) const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ access_token: 'gho_x' }))
await pollDeviceFlow('DEVICE-CODE-XYZ', fetchSpy as any) await pollDeviceFlow('DEVICE-CODE-XYZ', fetchSpy as any)
const [url, init] = fetchSpy.mock.calls[0] 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) const body = String(init.body)
expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`) expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`)
expect(body).toContain('device_code=DEVICE-CODE-XYZ') expect(body).toContain('device_code=DEVICE-CODE-XYZ')
+2 -2
View File
@@ -82,7 +82,7 @@ describe('resolveCopilotOAuthToken', () => {
mockReadFile.mockImplementation(async (p: string) => { mockReadFile.mockImplementation(async (p: string) => {
if (p.includes('apps.json')) { if (p.includes('apps.json')) {
return JSON.stringify({ 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') throw new Error('ENOENT')
@@ -93,7 +93,7 @@ describe('resolveCopilotOAuthToken', () => {
it('apps.json 中的 ghp_ token 也应跳过', async () => { it('apps.json 中的 ghp_ token 也应跳过', async () => {
mockReadFile.mockImplementation(async (p: string) => { mockReadFile.mockImplementation(async (p: string) => {
if (p.includes('apps.json')) { 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') 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({ const readFileSync = overrides.readFileSync ?? vi.fn(() => JSON.stringify({
name: 'hermes-web-ui', name: 'hermes-web-ui',
version: '0.0.0', 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() 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 () => { 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) => { const execFile = vi.fn((_command: string, _args: string[], _options: any, callback: any) => {
callback(null, [ callback(null, [
'abc123\trefs/tags/v0.6.6', 'abc123\trefs/tags/v0.6.6',
@@ -233,14 +233,14 @@ describe('update controller', () => {
}) })
expect(mocks.execFile).toHaveBeenCalledWith( expect(mocks.execFile).toHaveBeenCalledWith(
'git', '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.objectContaining({ timeout: 8000 }),
expect.any(Function), expect.any(Function),
) )
}) })
it('falls back to GitHub API when async git tag loading fails', async () => { 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) => { const execFile = vi.fn((_command: string, _args: string[], _options: any, callback: any) => {
callback(new Error('git timeout'), '', '') callback(new Error('git timeout'), '', '')
}) })
@@ -267,7 +267,7 @@ describe('update controller', () => {
], ],
}) })
expect(fetchMock).toHaveBeenCalledWith( 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({ expect.objectContaining({
headers: { 'User-Agent': 'hermes-web-ui-preview' }, headers: { 'User-Agent': 'hermes-web-ui-preview' },
signal: expect.any(AbortSignal), signal: expect.any(AbortSignal),