대시보드 확장
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
anchor alias
대시보드 확장
Hermes 웹 대시보드 (hermes dashboard)는 코드베이스를 포크하지 않고도 리스킨 및 확장이 가능하도록 설계되었습니다. 세 가지 계층이 노출됩니다:
- 테마 — 대시보드의 팔레트, 타이포그래피, 레이아웃 및 구성 요소별 크롬을 재페인트하는 YAML 파일입니다.
~/.hermes/dashboard-themes/에 파일을 넣으면 테마 스위처에 나타납니다. - UI 플러그인 — 탭을 등록하거나, 내장 페이지를 교체하거나, 페이지 범위 슬롯을 통해 하나를 확장하거나, 명명된 셸 슬롯에 컴포넌트를 주입하는
manifest.json+ 자바스크립트 번들이 포함된 디렉토리. - 백엔드 플러그인 — 플러그인 디렉토리 안에 있는 Python 파일로 FastAPI
router를 노출합니다; 라우트는/api/plugins/<name>/아래에 마운트되며 플러그인의 UI에서 호출됩니다.
세 가지 모두 런타임에 바로 적용됩니다: 저장소를 복제할 필요도, npm run build도, 대시보드 소스를 패치할 필요도 없습니다. 이 페이지가 세 가지 모두에 대한 정식 참조입니다.
대시보드만 사용하고 싶다면 웹 대시보드를 참조하세요. 터미널 CLI(웹 대시보드가 아님)를 리스킨하려면 스킨 & 테마를 참조하세요 — CLI 스킨 시스템은 대시보드 테마와 관련이 없습니다.
테마와 플러그인은 독립적이지만 시너지 효과를 냅니다. 테마는 단독으로 존재할 수 있습니다(YAML 파일 하나만으로). 플러그인도 단독으로 존재할 수 있습니다(탭 하나만으로). 함께 사용하면 커스텀 HUD가 포함된 완전한 비주얼 리스킨을 만들 수 있습니다 — 번들로 제공되는 strike-freedom-cockpit 데모가 바로 그 예입니다. 결합된 테마 + 플러그인 데모를 참조하세요.
목차
테마
테마는 ~/.hermes/dashboard-themes/에 저장된 YAML 파일입니다. 파일 이름은 중요하지 않으며(시스템에서 사용하는 것은 테마의 name: 필드입니다), 일반적인 관례는 <name>.yaml입니다. 모든 필드는 선택 사항이며, 누락된 키는 내장 default 테마로 대체되므로 테마는 하나의 색상만 있어도 됩니다.
빠른 시작 — 첫 번째 테마
mkdir -p ~/.hermes/dashboard-themes
````yaml
# ~/.hermes/dashboard-themes/neon.yaml
name: neon
label: Neon
description: Pure magenta on black
palette:
background: "#000000"
midground: "#ff00ff"
대시보드를 새로 고침하세요. 헤더의 팔레트 아이콘을 클릭하고 Neon을 선택하세요. 배경은 검은색으로 바뀌고, 텍스트와 강조 색상은 마젠타 색상이 되며, 모든 파생 색상(카드, 테두리, 무음, 링 등)은 CSS에서 color-mix()를 통해 그 두 가지 색상 트리플렛으로부터 다시 계산됩니다.
그게 전체 온보딩 과정입니다: 한 개의 파일, 두 가지 색상. 그 아래의 모든 것은 선택적인 개선입니다.
팔레트, 타이포그래피, 레이아웃
이 세 블록은 주제의 핵심입니다. 각각 독립적입니다 — 하나를 덮어쓰고 나머지는 그대로 두세요.
팔레트 (3층)
팔레트는 세 개의 색상 레이어와 따뜻한 빛의 비네트 색상, 그리고 노이즈-그레인 배수로 구성된 삼중 구조입니다. 대시보드의 디자인 시스템 캐스케이드는 CSS color-mix()를 통해 이 삼중 구조에서 모든 shadcn 호환 토큰(카드, 팝오버, 뮤트, 경계선, 기본, 파괴, 링 등)을 도출합니다. 세 가지 색상을 재정의하면 전체 UI에 영향을 미칩니다.
| 열쇠 | 설명 |
|---|---|
palette.background | 가장 깊은 캔버스 색상 — 일반적으로 거의 검은색에 가까움. 페이지 배경과 카드 채우기를 구동함. |
palette.midground | 주요 텍스트와 강조. 대부분의 UI 크롬은 이것을 읽습니다(전경 텍스트, 버튼 윤곽, 포커스 링). |
palette.foreground | 최상단 강조. 기본 테마는 이것을 알파 0(보이지 않음)인 흰색으로 설정합니다. 상단에 밝은 강조를 원하는 테마는 알파 값을 높일 수 있습니다. |
palette.warmGlow | rgba(...) 는 <Backdrop /> 에 의해 비네트 색상으로 사용됩니다. |
palette.noiseOpacity | 곡물 오버레이에 0–1.2 배수 적용. 낮게 설정 = 부드러움, 높게 설정 = 거침. |
각 레이어는 {hex: "#RRGGBB", alpha: 0.0–1.0} 또는 일반 16진수 문자열을 허용합니다(알파 값은 기본적으로 1.0입니다).
palette:
background:
hex: "#05091a"
alpha: 1.0
midground: "#d8f0ff" # bare hex, alpha = 1.0
foreground:
hex: "#ffffff"
alpha: 0 # invisible top layer
warmGlow: "rgba(255, 199, 55, 0.24)"
noiseOpacity: 0.7
타이포그래피
| 열쇠 | 타입 | 설명 |
|---|---|---|
fontSans | 문자열 | 본문 글자에 대한 CSS 글꼴 패밀리 스택 (html, body에 적용됨). |
fontMono | 문자열 | 코드 블록용 CSS 글꼴 패밀리 스택, <code>, .font-mono 유틸리티. |
fontDisplay | 문자열 | 선택적 헤딩/디스플레이 스택. fontSans로 대체됩니다. |
fontUrl | 문자열 | 선택적 외부 스타일시트 URL. 테마 전환 시 <head>에 <link rel="stylesheet">으로 주입됩니다. 동일한 URL은 두 번 주입되지 않습니다. Google Fonts, Bunny Fonts, 자체 호스팅 @font-face 시트 등 링크 가능한 모든 것과 작동합니다. |
baseSize | 문자열 | 루트 글꼴 크기 — rem 스케일을 제어합니다. 예: "14px", "16px". |
lineHeight | 문자열 | 기본 줄간격. 예: "1.5", "1.65". |
letterSpacing | 문자열 | 기본 글자 간격. 예: "0", "0.01em", "-0.01em". |
typography:
fontSans: '"Orbitron", "Eurostile", "Impact", sans-serif'
fontMono: '"Share Tech Mono", ui-monospace, monospace'
fontDisplay: '"Orbitron", "Eurostile", sans-serif'
fontUrl: "https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&family=Share+Tech+Mono&display=swap"
baseSize: "14px"
lineHeight: "1.5"
letterSpacing: "0.04em"
레이아웃
| 열쇠 | 값 | 설명 |
|---|---|---|
radius | 모든 CSS 길이 ("0", "0.25rem", "0.5rem", "1rem",...) | 모서리 반경 토큰. --radius에 매핑되며 --radius-sm/md/lg/xl로 이어집니다 — 모든 둥근 요소가 함께 이동합니다. |
density | compact " | comfortable " |
layout:
radius: "0"
density: compact
레이아웃 변형
layoutVariant 는 전체 쉘 레이아웃을 선택합니다. 없을 경우 기본값은 "standard" 입니다.
| 변형 | 행동 |
|---|---|
standard | 단일 열, 최대 너비 1600px (기본값). |
cockpit | 왼쪽 사이드바 레일(260px) + 주요 콘텐츠. sidebar 슬롯을 통해 플러그인으로 채워짐 — Shell 슬롯 참고. 플러그인이 없으면 레일에 자리 표시자가 표시됩니다. |
tiled | 페이지가 전체 뷰포트 너비를 사용할 수 있도록 최대 너비 제한을 제거합니다. |
layoutVariant: cockpit
현재 변형은 document.documentElement.dataset.layoutVariant로 노출되므로, customCSS의 원시 CSS가 :root[data-layout-variant="cockpit"]...를 통해 이를 타겟팅할 수 있습니다.
테마 자산 (CSS 변수로서의 이미지)
테마가 있는 선박 아트워크 URL. 각 이름이 지정된 슬롯은 내장 셸과 모든 플러그인이 읽을 수 있는 CSS 변수(--theme-asset-<name>)가 됩니다. bg 슬롯은 자동으로 배경에 연결되며, 다른 슬롯은 플러그인 대상으로 사용됩니다.
assets:
bg: "https://example.com/hero-bg.jpg" # auto-wired into <Backdrop />
hero: "/my-images/strike-freedom.png" # for plugin sidebars
crest: "/my-images/crest.svg" # for header-left plugins
logo: "/my-images/logo.png"
sidebar: "/my-images/rail.png"
header: "/my-images/header-art.png"
custom:
scanLines: "/my-images/scanlines.png" # → --theme-asset-custom-scanLines
값을 허용:
- 일반 URL —
url(...)로 자동으로 감쌉니다. - 사전 포장된
url(...),linear-gradient(...),radial-gradient(...)표현 — 그대로 사용됩니다. "none"— 명시적 옵트아웃.
모든 자산은 또한 --theme-asset-<name>-raw (언랩된 URL)로도 내보내지며, 이는 플러그인이 background-image 대신 <img src>으로 전달해야 할 경우를 대비한 것입니다.
플러그인은 일반 CSS나 JS로 이것들을 읽습니다:
// In a plugin slot
const hero = getComputedStyle(document.documentElement).getPropertyValue("--theme-asset-hero").trim();
컴포넌트 크롬 오버라이드
componentStyles는 CSS 선택자를 작성하지 않고 개별 셸 구성 요소의 스타일을 다시 지정합니다. 각 버킷의 항목은 셸의 공유 구성 요소가 읽는 CSS 변수(--component-<bucket>-<kebab-property>)가 됩니다. 따라서 card: override는 모든 <Card>에 적용되며, header:는 앱 바에 적용됩니다.
componentStyles:
card:
clipPath: "polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px)"
background: "linear-gradient(180deg, rgba(10, 22, 52, 0.85), rgba(5, 9, 26, 0.92))"
boxShadow: "inset 0 0 0 1px rgba(64, 200, 255, 0.28)"
header:
background: "linear-gradient(180deg, rgba(16, 32, 72, 0.95), rgba(5, 9, 26, 0.9))"
tab:
clipPath: "polygon(6px 0, 100% 0, calc(100% - 6px) 100%, 0 100%)"
sidebar: {}
backdrop: {}
footer: {}
progress: {}
badge: {}
page: {}
지원되는 버킷: card, header, footer, sidebar, tab, progress, badge, backdrop, page.
속성 이름은 camelCase(clipPath)를 사용하고 kebab(clip-path)로 출력됩니다. 값은 일반 CSS 문자열입니다 — CSS가 허용하는 모든 것(clip-path, border-image, background, box-shadow, animation,...).
색상 덮어쓰기
대부분의 테마는 이것이 필요하지 않습니다 — 3계층 팔레트가 모든 shadcn 토큰을 도출합니다. 도출 과정에서 나오지 않는 특정 강조색이 필요할 때 colorOverrides을 사용하세요 (파스텔 테마를 위한 더 부드러운 파괴적 빨강, 브랜드를 위한 특정 성공 녹색 등).
colorOverrides:
primary: "#ffce3a"
primaryForeground: "#05091a"
accent: "#3fd3ff"
ring: "#3fd3ff"
destructive: "#ff3a5e"
border: "rgba(64, 200, 255, 0.28)"
지원되는 키: card, cardForeground, popover, popoverForeground, primary, primaryForeground, secondary, secondaryForeground, muted, mutedForeground, accent, accentForeground, destructive, destructiveForeground, success, warning, border, input, ring.
각 키는 --color-<kebab> CSS 변수에 1:1로 매핑됩니다 (예: primaryForeground → --color-primary-foreground). 여기에서 설정된 모든 키는 활성 테마에 대해서만 팔레트 계층보다 우선합니다 — 다른 테마로 전환하면 재정의가 삭제됩니다.
원시 customCSS
선택자 수준의 크롬으로 인해 componentStyles이(가) 표현할 수 없는 경우 — 의사 요소, 애니메이션, 미디어 쿼리, 테마 범위 override — 원시 CSS를 customCSS에 넣으세요:
customCSS: |
/* Scanline overlay — only visible when cockpit variant is active. */:root[data-layout-variant="cockpit"] body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 100;
background: repeating-linear-gradient(to bottom,
transparent 0px, transparent 2px,
rgba(64, 200, 255, 0.035) 3px, rgba(64, 200, 255, 0.035) 4px);
mix-blend-mode: screen;
}
CSS는 테마 적용 시 단일 범위 <style data-hermes-theme-css> 태그로 주입되며 테마 전환 시 정리됩니다. 테마당 32 KiB로 제한됩니다.
내장 테마
각 내장 항목은 자체 팔레트, 타이포그래피 및 레이아웃을 제공하며 — 전환하면 색상뿐만 아니라 다른 눈에 띄는 변화도 발생합니다.
| 주제 | 팔레트 | 타이포그래피 | 레이아웃 |
|---|---|---|---|
헤르메스 틸 (default) | 다크 티얼 + 크림 | 시스템 스택, 15픽셀 | 0.5rem 반경, 편안한 |
에르메스 틸 (대형) (default-large) | 기본값과 동일 | 시스템 스택, 18px, 줄 높이 1.65 | 0.5rem 반경, 넓은 |
자정 (midnight) | 짙은 청자주 | 인터 + JetBrains 모노, 14px | 0.75rem 반경, 편안한 |
엠버 (ember) | 따뜻한 진홍색 + 청동 | Spectral (세리프) + IBM Plex Mono, 15px | 0.25rem 반경, 편안한 |
모노 (mono) | 그레이스케일 | IBM Plex Sans + IBM Plex Mono, 13픽셀 | 0 반경, 컴팩트 |
사이버펑크 (cyberpunk) | 검은색 위의 네온 그린 | Share Tech Mono 어디서나, 14px | 0 반경, 컴팩트 |
로제 (rose) | 핑크 + 아이보리 | Fraunces (세리프) + DM Mono, 16px | 1rem 반지름, 넓은 |
Google Fonts를 참조하는 테마( Hermes Teal 제외)는 필요에 따라 스타일시트를 로드합니다 — 처음으로 해당 테마로 전환할 때 <link> 태그가 <head>에 삽입됩니다.
전체 테마 YAML 참조
모든 노브를 한 파일에 — 필요 없는 것은 복사하고 잘라내세요:
# ~/.hermes/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Deep sea blues with coral accents
# 3-layer palette (accepts {hex, alpha} or bare hex)
palette:
background:
hex: "#0a1628"
alpha: 1.0
midground:
hex: "#a8d0ff"
alpha: 1.0
foreground:
hex: "#ffffff"
alpha: 0.0
warmGlow: "rgba(255, 107, 107, 0.35)"
noiseOpacity: 0.7
typography:
fontSans: "Poppins, system-ui, sans-serif"
fontMono: "Fira Code, ui-monospace, monospace"
fontDisplay: "Poppins, system-ui, sans-serif" # optional
fontUrl: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap"
baseSize: "15px"
lineHeight: "1.6"
letterSpacing: "-0.003em"
layout:
radius: "0.75rem"
density: comfortable
layoutVariant: standard # standard | cockpit | tiled
assets:
bg: "https://example.com/ocean-bg.jpg"
hero: "/my-images/kraken.png"
crest: "/my-images/anchor.svg"
logo: "/my-images/logo.png"
custom:
pattern: "/my-images/waves.svg"
componentStyles:
card:
boxShadow: "inset 0 0 0 1px rgba(168, 208, 255, 0.18)"
header:
background: "linear-gradient(180deg, rgba(10, 22, 40, 0.95), rgba(5, 9, 26, 0.9))"
colorOverrides:
destructive: "#ff6b6b"
ring: "#ff6b6b"
customCSS: |
/* Any additional selector-level tweaks */
파일을 만든 후 대시보드를 새로 고치세요. 헤더 바에서 실시간으로 테마를 전환하려면 팔레트 아이콘을 클릭하세요. 선택은 config.yaml 아래의 dashboard.theme에 유지되며 새로 고침 시 복원됩니다.
플러그인
대시보드 플러그인은 manifest.json, 미리 빌드된 JS 번들, 선택적으로 CSS 파일과 FastAPI 경로가 포함된 Python 파일이 있는 디렉토리입니다. 플러그인은 ~/.hermes/plugins/<name>/에 있는 다른 Hermes 플러그인 옆에 위치하며 — 대시보드 확장은 해당 플러그인 디렉토리 안의 dashboard/ 하위 폴더이므로, 하나의 플러그인으로 단일 설치에서 CLI/게이트웨이와 대시보드 둘 다 확장할 수 있습니다.
플러그인은 React나 UI 컴포넌트를 번들로 포함하지 않습니다. 이들은 window....``에 노출된 Plugin SDK를 사용합니다. 이는 플러그인 번들을 작게 유지하고(보통 몇 KB 정도) 버전 충돌을 방지합니다.
빠른 시작 — 첫 번째 플러그인
디렉토리 구조를 생성하세요:
mkdir -p ~/.hermes/plugins/my-plugin/dashboard/dist
매니페스트를 작성하세요:
// ~/.hermes/plugins/my-plugin/dashboard/manifest.json
{
"name": "my-plugin",
"label": "My Plugin",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills"
},
"entry": "dist/index.js"
}
JS 번들 작성하기 (일반 IIFE — 빌드 단계 불필요):
// ~/.hermes/plugins/my-plugin/dashboard/dist/index.js
(function () {
"use strict";
const SDK = window.`...`;
const { React } = SDK;
const { Card, CardHeader, CardTitle, CardContent } = SDK.components;
function MyPage() {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "My Plugin"),
),
React.createElement(CardContent, null,
React.createElement("p", { className: "text-sm text-muted-foreground" },
"Hello from my custom dashboard tab.",
),
),
);
}
window.`...`.register("my-plugin", MyPage);
})();
대시보드를 새로 고침하세요 — 탭은 내비게이션 바에서 기술 다음에 나타납니다.
JSX를 선호한다면, React를 외부로 하고 IIFE 출력으로 하는 어떤 번들러(esbuild, Vite, rollup)든 사용하세요. 유일한 필수 조건은 최종 파일이 <script>를 통해 로드할 수 있는 단일 JS 파일이어야 한다는 것입니다. React는 절대 번들되지 않으며 SDK.React에서 가져옵니다.
디렉토리 구조
~/.hermes/plugins/my-plugin/
├── plugin.yaml # optional — existing CLI/gateway plugin manifest
├── __init__.py # optional — existing CLI/gateway hooks
└── dashboard/ # dashboard extension
├── manifest.json # required — tab config, icon, entry point
├── dist/
│ ├── index.js # required — pre-built JS bundle (IIFE)
│ └── style.css # optional — custom CSS
└── plugin_api.py # optional — backend API routes (FastAPI)
단일 플러그인 디렉토리는 세 가지 직교 확장 기능을 가질 수 있습니다:
plugin.yaml+__init__.py— CLI/게이트웨이 플러그인 (플러그인 페이지 보기).dashboard/manifest.json+dashboard/dist/index.js— 대시보드 UI 플러그인.dashboard/plugin_api.py— 대시보드 백엔드 경로.
그 중 어느 것도 필수는 아닙니다; 필요한 레이어만 포함하세요.
선적 명세서 참조
{
"name": "my-plugin",
"label": "My Plugin",
"description": "What this plugin does",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills",
"재정의": "/",
"hidden": false
},
"slots": ["sidebar", "header-left"],
"entry": "dist/index.js",
"css": "dist/style.css",
"api": "plugin_api.py"
}
| 필드 | 필수 | 설명 |
|---|---|---|
name | 예 | 고유한 플러그인 식별자. 소문자 사용, 하이픈 허용. URL 및 등록에 사용됨. |
label | 예 | 탭 네비게이션에 표시되는 이름. |
description | No | 간단한 설명(대시보드 관리 화면에 표시됨). |
icon | No | Lucide 아이콘 이름. 기본값은 Puzzle입니다. 알 수 없는 이름은 Puzzle로 대체됩니다. |
version | No | 세머버 문자열. 기본값은 0.0.0입니다. |
tab.path | 예 | 탭의 URL 경로 (예: /my-plugin) |
tab.position | No | 탭을 삽입할 위치. "end" (기본값), "after:<path>", 또는 "before:<path>" — 콜론 다음의 값은 대상 탭의 경로 세그먼트입니다 (선행 슬래시 없음). 예시: "after:skills", "before:config". |
tab.override | No | 새 탭을 추가하는 대신 해당 페이지를 대체하려면 내장 경로 ("/", "/sessions", "/config",...)로 설정하세요. 자세한 내용은 내장 페이지 대체를 참조하세요. |
tab.hidden | No | true일 경우, 컴포넌트와 모든 슬롯을 등록하지만 nav에 탭을 추가하지 않습니다. 슬롯 전용 플러그인에서 사용됩니다. 자세한 내용은 슬롯 전용 플러그인을 참조하십시오. |
slots | No | 이 플러그인이 채우는 명명된 셸 슬롯입니다. 문서용 보조 — 실제 등록은 JS 번들을 통해 registerSlot()에서 이루어집니다. 여기에 슬롯을 나열하면 검색 환경이 더 유익해집니다. |
entry | 예 | dashboard/에 상대적인 JS 번들 경로입니다. 기본값은 dist/index.js입니다. |
css | No | CSS 파일을 <link> 태그로 주입할 경로. |
api | No | FastAPI 경로가 있는 Python 파일의 경로. /api/plugins/<name>/에 마운트됨. |
사용 가능한 아이콘
플러그인은 Lucide 아이콘 이름을 사용합니다. 대시보드는 이름으로 이를 매핑하며 — 알 수 없는 이름은 조용히 Puzzle로 대체됩니다.
현재 매핑됨: Activity, BarChart3, Clock, Code, Database, Eye, FileText, Globe, Heart, KeyRound, MessageSquare, Package, Puzzle, Settings, Shield, Sparkles, Star, Terminal, Wrench, Zap.
다른 아이콘이 필요하신가요? web/src/App.tsx의 ICON_MAP에 PR을 열어주세요 — 순수한 추가 변경입니다.
플러그인 SDK
모든 플러그인에 필요한 것은 window....``에 있습니다. 플러그인은 절대로 React를 직접 가져와서는 안 됩니다.
const SDK = window.`...`;
// React + hooks
SDK.React // the React instance
SDK.hooks.useState
SDK.hooks.useEffect
SDK.hooks.useCallback
SDK.hooks.useMemo
SDK.hooks.useRef
SDK.hooks.use컨텍스트
SDK.hooks.create컨텍스트
// UI components (shadcn/ui primitives)
SDK.components.Card
SDK.components.CardHeader
SDK.components.CardTitle
SDK.components.CardContent
SDK.components.Badge
SDK.components.Button
SDK.components.Input
SDK.components.Label
SDK.components.Select
SDK.components.SelectOption
SDK.components.Separator
SDK.components.Tabs
SDK.components.TabsList
SDK.components.TabsTrigger
SDK.components.PluginSlot // render a named slot (useful for nested plugin UIs)
// Hermes API client + raw fetcher
SDK.api // typed client — getStatus, getSessions, getConfig,...
SDK.fetchJSON // raw fetch for custom endpoints (plugin-registered routes)
// Utilities
SDK.utils.cn // Tailwind class merger (clsx + twMerge)
SDK.utils.timeAgo // "5m ago" from unix timestamp
SDK.utils.isoTimeAgo // "5m ago" from ISO string
// Hooks
SDK.useI18n // i18n hook for multi-language plugins
플러그인의 백엔드 호출 중
SDK.fetchJSON("/api/plugins/my-plugin/data").then((data) => console.log(data)).catch((err) => console.error("API call failed:", err));
``fetchJSON`은 세션 인증 토큰을 주입하고, 오류를 발생한 예외로 표시하며, JSON을 자동으로 파싱합니다.
#### 내장 Hermes 엔드포인트 호출 중 \{#palette-typography-layout}
```javascript
// Agent status
SDK.api.getStatus().then((s) => console.log("Version:", s.version));
// Recent sessions
SDK.api.getSessions(10).then((resp) => console.log(resp.sessions.length));
전체 목록은 웹 대시보드 → REST API를 참조하세요.
쉘 슬롯
슬롯은 플러그인이 전체 탭을 차지하지 않고도 앱 셸의 지정된 위치 — 조종석 사이드바, 헤더, 푸터, 오버레이 레이어 — 에 컴포넌트를 주입할 수 있게 해줍니다. 여러 플러그인이 동일한 슬롯을 채울 수 있으며, 등록 순서대로 쌓여서 렌더링됩니다.
플러그인 번들 내부에서 등록하세요:
window.`...`.registerSlot("my-plugin", "sidebar", MySidebar);
window.`...`.registerSlot("my-plugin", "header-left", MyCrest);
슬롯 카탈로그
쉘 전체 슬롯 (앱 크롬 어디에서나 렌더링 가능):
| 성 | 위치 |
|---|---|
backdrop | <Backdrop /> 레이어 스택 안, 노이즈 레이어 위에. |
header-left | 상단 바에서 에르메스 브랜드 앞에. |
header-right | 상단 바의 테마/언어 전환기 전에. |
header-banner | 네비게이션 아래 전체 폭 스트립. |
sidebar | 조종석 사이드바 레일 — layoutVariant === "cockpit" 일 때만 렌더링됩니다. |
pre-main | 경로 출구 위(<main> 내부). |
post-main | 라우트 아웃렛 아래 (<main> 내부). |
footer-left | 바닥글 셀 내용 (기본값을 대체합니다). |
footer-right | 바닥글 셀 내용 (기본값을 대체합니다). |
overlay | 모든 것 위에 고정된 위치의 레이어. 크롬(스캔라인, 비네트)에 유용하며 customCSS 혼자서는 할 수 없는 기능. |
페이지 범위 슬롯 (지정된 내장 페이지에서만 렌더링 — 전체 경로를 덮어쓰지 않고 기존 페이지에 위젯, 카드 또는 툴바를 삽입할 때 사용):
| 성 | 그것이 렌더링되는 곳 |
|---|---|
sessions:top / sessions:bottom | /sessions 페이지의 상단 / 하단. |
analytics:top / analytics:bottom | /analytics 페이지의 상단 / 하단. |
logs:top / logs:bottom | /logs의 상단(필터 툴바 위) / 하단(로그 뷰어 아래). |
cron:top / cron:bottom | /cron 페이지의 상단 / 하단. |
skills:top / skills:bottom | /skills 페이지의 상단 / 하단. |
config:top / config:bottom | /config 페이지의 상단 / 하단. |
env:top / env:bottom | /env (키) 페이지의 상단 / 하단. |
docs:top / docs:bottom | iframe 위 / /docs 아래. |
chat:top / chat:bottom | /chat의 상단/하단 (임베디드 채팅이 활성화된 경우에만 작동). |
예시 — 세션 페이지 상단에 배너 카드를 추가합니다:
function PinnedSessionsBanner() {
return React.createElement(Card, null,
React.createElement(CardContent, { className: "py-2 text-xs" },
"Pinned note injected by my-plugin"),
);
}
window.`...`.registerSlot("my-plugin", "sessions:top", PinnedSessionsBanner);
플러그인이 기존 페이지만 확장하고 자체 사이드바 탭이 필요하지 않은 경우, 페이지 범위 슬롯을 tab.hidden: true와 결합하세요.
셸은 위의 슬롯에 대해 <PluginSlot name="..." />만 렌더링합니다. 추가 이름은 중첩 플러그인 UI에 대해 레지스트리에서 허용됩니다 — 플러그인은 SDK.components.PluginSlot을 통해 자체 슬롯을 노출할 수 있습니다.
재등록 및 HMR
같은 (plugin, slot) 쌍이 두 번 등록되면, 나중 호출이 이전 호출을 대체합니다 — 이는 React HMR이 플러그인 재마운트가 작동하는 방식을 기대하는 것과 일치합니다.
내장 페이지 교체 (tab.override)
tab.override를 내장 경로로 설정하면 플러그인의 구성 요소가 새 탭을 추가하는 대신 해당 페이지를 대체합니다. 테마에서 사용자 지정 홈 페이지(/)를 원하지만 대시보드의 나머지 부분은 그대로 유지하고 싶을 때 유용합니다.
{
"name": "my-home",
"label": "Home",
"tab": {
"path": "/my-home",
"override": "/",
"position": "end"
},
"entry": "dist/index.js"
}
``override`이 설정된 상태에서:
- 원래 페이지 구성 요소 `/`가 라우터에서 제거되었습니다.
- 귀하의 플러그인은 대신 `/`에서 렌더링됩니다.
- `tab.path`에 대한 내비게이션 탭이 추가되지 않습니다(재정의가 핵심입니다).
주어진 경로를 재정의할 수 있는 플러그인은 하나뿐입니다. 두 개의 플러그인이 동일한 재정을 주장하면, 먼저 나온 플러그인이 적용되고 두 번째는 개발자 모드 경고와 함께 무시됩니다.
기존 페이지를 인수하지 않고 카드나 도구 모음을 추가하기만 하면, 대신 [페이지 범위 슬롯](#augmenting-built-in-pages-page-scoped-slots)을 사용하세요.
### 내장 페이지 확장(페이지 범위 슬롯) \{#augmenting-built-in-pages-page-scoped-slots}
`tab.override`을 통한 전체 교체는 부담이 됩니다 — 이제 귀하의 플러그인은 해당 페이지 전체를 소유하게 되며, 여기에 우리가 제공하는 모든 미래 업데이트도 포함됩니다. 대부분의 경우, 기존 페이지에 배너, 카드 또는 툴바를 추가하고자 할 뿐입니다. 그것이 바로 **페이지 범위 슬롯(page-scoped slots)**의 목적입니다.
모든 내장 페이지는 컨텐츠 영역의 상단과 하단에 렌더링되는 `<page>:top` 및 `<page>:bottom` 슬롯을 노출합니다. 플러그인은 `registerSlot()`를 호출하여 슬롯 하나를 채웁니다 — 내장 페이지는 정상적으로 작동하며, 귀하의 컴포넌트는 그 옆에 함께 렌더링됩니다.
사용 가능한 슬롯: `sessions:*`, `analytics:*`, `logs:*`, `cron:*`, `skills:*`, `config:*`, `env:*`, `docs:*`, `chat:*` (각각 `:top` 및 `:bottom` 포함). 전체 카탈로그는 [Shell Slots → 슬롯 카탈로그](#slot-catalogue)에서 확인하세요.
최소 예제 — 배너를 세션 페이지 상단에 고정:
```json
// ~/.hermes/plugins/session-notes/dashboard/manifest.json
{
"name": "session-notes",
"label": "Session Notes",
"tab": { "path": "/session-notes", "hidden": true },
"slots": ["sessions:top"],
"entry": "dist/index.js"
}
````javascript
// ~/.hermes/plugins/session-notes/dashboard/dist/index.js
(function () {
const SDK = window.`...`;
const { React } = SDK;
const { Card, CardContent } = SDK.components;
function Banner() {
return React.createElement(Card, null,
React.createElement(CardContent, { className: "py-2 text-xs" },
"Remember to label important sessions before archiving."),
);
}
// Placeholder for the hidden tab.
window.`...`.register("session-notes", function () { return null; });
// The real work.
window.`...`.registerSlot("session-notes", "sessions:top", Banner);
})();
핵심 요점:
tab.hidden: true는 플러그인을 사이드바에서 제외합니다 — 독립적인 페이지가 없습니다.slots매니페스트 필드는 문서용일 뿐입니다. 실제 바인딩은 JS 번들에서registerSlot()를 통해 이루어집니다.- 여러 플러그인이 동일한 페이지 범위 슬롯을 요청할 수 있습니다. 이들은 등록 순서대로 쌓여 렌더링됩니다.
- 플러그인이 등록되지 않았을 때 제로 풋프린트: 내장 페이지가 이전과 정확히 동일하게 렌더링됩니다.
레퍼런스 플러그인 (example-dashboard in hermes-example-plugins) 은 sessions:top 에 배너를 주입하는 라이브 데모를 제공합니다 — 전체 과정을 보기 위해 설치하세요.
슬롯 전용 플러그인 (tab.hidden)
플러그인이 tab.hidden: true 할 때, 해당 플러그인은 자신의 컴포넌트(직접 URL 방문용)와 모든 슬롯을 등록하지만, 탐색에 탭을 추가하지는 않습니다. 슬롯에만 삽입되기 위해 존재하는 플러그인에서 사용됩니다 — 헤더 문장, 사이드바 HUD, 오버레이 등.
{
"name": "header-crest",
"label": "Header Crest",
"tab": {
"path": "/header-crest",
"position": "end",
"hidden": true
},
"slots": ["header-left"],
"entry": "dist/index.js"
}
번들은 여전히 자리 표시자 컴포넌트와 함께 register()(누군가가 URL을 직접 입력할 경우를 대비한 좋은 관행) 그리고 실제 작업을 수행하기 위해 registerSlot().
백엔드 API 경로
플러그인은 매니페스트에서 api를 설정하여 FastAPI 라우트를 등록할 수 있습니다. 파일을 생성하고 router를 내보내세요:
# ~/.hermes/plugins/my-plugin/dashboard/plugin_api.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/data")
async def get_data():
return {"items": ["one", "two", "three"]}
@router.post("/action")
async def do_action(body: dict):
return {"ok": True, "received": body}
경로는 /api/plugins/<name>/ 아래에 장착되므로, 위의 내용은 다음과 같이 됩니다:
GET /api/plugins/my-plugin/dataPOST /api/plugins/my-plugin/action
플러그인 API 경로는 대시보드 서버가 기본적으로 로컬호스트에 바인딩되므로 세션 토큰 인증을 우회합니다. 신뢰할 수 없는 플러그인을 실행하는 경우 --host 0.0.0.0로 대시보드를 공개 인터페이스에 노출하지 마세요 — 플러그인 경로도 접근 가능해집니다.
Hermes 내부 접근
백엔드 경로는 대시보드 프로세스 내부에서 실행되므로, hermes-agent 코드베이스에서 직접 가져올 수 있습니다:
from fastapi import APIRouter
from hermes_state import SessionDB
from hermes_cli.config import load_config
router = APIRouter()
@router.get("/session-count")
async def session_count():
db = SessionDB()
try:
count = len(db.list_sessions(limit=9999))
return {"count": count}
finally:
db.close()
@router.get("/config-snapshot")
async def config_snapshot():
cfg = load_config()
return {"model": cfg.get("model", {})}
플러그인별 맞춤 CSS
플러그인이 Tailwind 클래스와 인라인 style= 외에 스타일이 필요하다면, CSS 파일을 추가하고 매니페스트에서 참조하세요:
{
"css": "dist/style.css"
}
파일은 플러그인 로드 시 <link> 태그로 주입됩니다. 대시보드 스타일과의 충돌을 피하기 위해 특정 클래스 이름을 사용하고, 테마 인식을 유지하려면 대시보드의 CSS 변수를 참조하세요:
/* dist/style.css */.my-plugin-chart {
border: 1px solid var(--color-border);
background: var(--color-card);
color: var(--color-card-foreground);
padding: 1rem;
}.my-plugin-chart:hover {
border-color: var(--color-ring);
}
대시보드는 모든 shadcn 토큰을 --color-* 및 테마 추가 항목(--theme-asset-*, --component-<bucket>-*, --radius, --spacing-mul)으로 노출합니다. 그것들을 참조하면 플러그인이 활성 테마에 맞게 자동으로 리스킨됩니다.
플러그인 검색 및 다시 로드
대시보드는 dashboard/manifest.json를 위해 세 개의 디렉토리를 스캔합니다:
| 우선순위 | 디렉토리 | 소스 레이블 |
|---|---|---|
| 1 (충돌에서 승리) | ~/.hermes/plugins/<name>/dashboard/ | user |
| 2 | <repo>/plugins/memory/<name>/dashboard/ | bundled |
| 2 | <repo>/plugins/<name>/dashboard/ | bundled |
| 3 | ./.hermes/plugins/<name>/dashboard/ | project — HERMES_ENABLE_PROJECT_PLUGINS이 설정되어 있을 때만 |
탐색 결과는 대시보드 프로세스별로 캐시됩니다. 새 플러그인을 추가한 후에는 다음 중 하나를 수행하세요:
# Force a rescan without restart
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan
…또는 hermes dashboard를 재시작하세요.
플러그인 로드 수명 주기
- 대시보드가 로드됩니다.
main.tsx은window....에서 SDK를, `window.`...에서 레지스트리를 노출합니다. App.tsx가usePlugins()를 호출 →GET /api/dashboard/plugins를 가져옵니다.- 각 매니페스트마다: CSS
<link>가 주입됩니다(선언된 경우), 그 다음에<script>태그가 JS 번들을 로드합니다. - 플러그인의 IIFE가 실행되어
window.....register(name, Component)를 호출하고, 선택적으로 각 슬롯에 대해.registerSlot(name, slot, Component). - 대시보드는 등록된 컴포넌트를 매니페스트에 따라 해결하고, 탭을 네비게이션에 추가하며(
hidden가 아닌 경우), 컴포넌트를 라우트로 마운트합니다.
플러그인은 스크립트가 로드된 후 최대 2초 동안 register()를 호출할 수 있습니다. 그 이후에는 대시보드가 더 이상 기다리지 않고 초기 렌더링을 완료합니다. 나중에 플러그인이 등록되더라도 여전히 나타납니다 — 네비게이션은 반응형입니다.
플러그인의 스크립트 로드에 실패할 경우(404, 구문 오류, IIFE 실행 중 예외 발생), 대시보드는 브라우저 콘솔에 경고를 기록하고 해당 플러그인 없이 계속 실행됩니다.
결합된 테마 + 플러그인 데모
strike-freedom-cockpit 플러그인(동반 저장소 hermes-example-plugins)은 완전한 리스킨 데모입니다. 이것은 테마 YAML과 슬롯 전용 플러그인을 결합하여 대시보드를 포크하지 않고 조종석 스타일의 HUD를 생성합니다.
무엇을 보여주는가:
- 팔레트, 타이포그래피,
fontUrl,layoutVariant: cockpit,assets,componentStyles(모서리가 잘린 카드, 그래디언트 배경),colorOverrides, 그리고customCSS(스캔라인 오버레이)를 사용하는 전체 테마. - 세 개의 슬롯에 등록되는 슬롯 전용 플러그인 (
tab.hidden: true):sidebar—SDK.api.getStatus()에 의해 구동되는 실시간 원격 측정 막대가 있는 MS-STATUS 패널.header-left— 활성 테마에서--theme-asset-crest라고 적힌 파벌 문장.footer-right— 기본 조직 라인을 대체하는 맞춤 태그라인.
- 이 플러그인은 CSS 변수를 통해 테마에서 제공하는 아트워크를 읽으므로, 테마를 바꾸면 플러그인 코드 수정 없이도 히어로/크레스트가 변경됩니다.
설치:
git clone https://github.com/NousResearch/hermes-example-plugins.git
# Theme
cp hermes-example-plugins/strike-freedom-cockpit/theme/strike-freedom.yaml \
~/.hermes/dashboard-themes/
# Plugin
cp -r hermes-example-plugins/strike-freedom-cockpit ~/.hermes/plugins/
대시보드를 열고 테마 전환기에서 Strike Freedom을 선택하세요. 조종석 사이드바가 나타나고, 문장이 헤더에 표시되며, 태그라인이 푸터를 대체합니다. Hermes Teal로 다시 전환하면 플러그인은 설치된 상태지만 보이지 않습니다 (sidebar 슬롯은 cockpit 레이아웃 변형에서만 렌더링됩니다).
플러그인 소스(strike-freedom-cockpit/dashboard/dist/index.js를 동반 리포지토리에서) 읽어 CSS 변수를 읽는 방법, 슬롯 지원이 없는 이전 대시보드에 대한 보호 방법, 하나의 번들에서 세 개의 슬롯을 등록하는 방법을 확인하세요.
API 참조
테마 엔드포인트
| 엔드포인트 | 방법 | 설명 |
|---|---|---|
/api/dashboard/themes | 받기 | 사용 가능한 테마 목록 + 활성 이름. 내장 테마는 {name, label, description}을 반환합니다; 사용자 테마에는 전체 정규화된 테마 객체가 포함된 definition 필드도 포함됩니다. |
/api/dashboard/theme | PUT | 활성 테마 설정. 본문: {"name": "midnight"}. dashboard.theme 아래 config.yaml에 유지됩니다. |
플러그인 엔드포인트
| 엔드포인트 | 방법 | 설명 |
|---|---|---|
/api/dashboard/plugins | 받다 | 발견된 플러그인 목록 (매니페스트 포함, 내부 필드 제외). |
/api/dashboard/plugins/rescan | 받다 | 재시작 없이 플러그인 디렉토리를 강제로 다시 스캔합니다. |
/dashboard-plugins/<name>/<path> | 받다 | 플러그인의 dashboard/ 디렉토리에서 정적 자산을 제공하세요. 경로 순회는 차단됩니다. |
/api/plugins/<name>/* | * | 플러그인에 등록된 백엔드 경로. |
window에서의 SDK
| 글로벌 | 유형 | 제공자 |
|---|---|---|
window....`` | 객체 | registry.ts — 리액트, 훅, UI 구성 요소, API 클라이언트, 유틸리티. |
window.....register(name, Component) | 함수 | 플러그인의 주요 구성 요소를 등록하세요. |
window.....registerSlot(name, slot, Component) | 함수 | 지정된 셸 슬롯에 등록하세요. |
문제 해결
내 테마가 선택기에서 나타나지 않습니다.
파일이 ~/.hermes/dashboard-themes/에 있으며 .yaml 또는 .yml로 끝나는지 확인하세요. 페이지를 새로 고치십시오. curl http://127.0.0.1:9119/api/dashboard/themes를 실행하세요 — 테마가 응답에 있어야 합니다. YAML에 구문 분석 오류가 있는 경우, 대시보드가 errors.log의 ~/.hermes/logs/ 아래에 로그를 기록합니다.
내 플러그인의 탭이 나타나지 않습니다.
- 매니페스트가
~/.hermes/plugins/<name>/dashboard/manifest.json에 있는지 확인하세요 (dashboard/하위 디렉토리를 참고하세요). curl http://127.0.0.1:9119/api/dashboard/plugins/rescan재발견을 강제로 수행합니다.- 브라우저 개발자 도구 열기 → 네트워크 —
manifest.json,index.js및 404 없이 로드된 모든 CSS 확인. - 브라우저 개발자 도구 → 콘솔을 열고 IIFE 또는
window....is undefined동안의 오류를 확인하세요 (SDK가 초기화되지 않았음을 나타내며, 보통 이전의 React 렌더 크래시 때문입니다). - 번들 호출
window.....register(...)가manifest.json:name와 같은 이름을 사용하는지 확인하세요.
슬롯에 등록된 컴포넌트는 렌더링되지 않습니다.
sidebar 슬롯은 활성 테마에 layoutVariant: cockpit가 있을 때만 렌더링됩니다. 다른 슬롯은 항상 렌더링됩니다. 히트가 없는 슬롯에 등록하려면, 플러그인 번들이 실행되었는지 확인하기 위해 registerSlot 안에 console.log를 추가하세요.
플러그인 백엔드 경로가 404를 반환합니다.
- 매니페스트가
"api": "plugin_api.py"를 확인하고,dashboard/내부의 기존 파일을 가리키고 있는지 확인하세요. - 재시작
hermes dashboard— 플러그인 API 경로는 시작 시 한 번만 마운트되며, 재스캔 시에는 되지 않습니다. plugin_api.py가 모듈 수준의router = APIRouter()을 내보내는지 확인하세요. 다른 내보내기 이름은 선택되지 않습니다.Failed to load plugin <name> API routes용~/.hermes/logs/errors.log꼬리 — 가져오기 오류가 그곳에 기록됩니다.
테마 변경 시 내 색상 재정의가 사라집니다.
colorOverrides는 활성 테마에 적용되며 테마를 전환하면 지워집니다 — 이것은 의도된 동작입니다. 지속되는 오버라이드를 원하면 라이브 스위처가 아닌 테마의 YAML에 넣으세요.
테마 customCSS가 잘립니다.
customCSS 블록은 테마당 32KiB로 제한됩니다. 큰 스타일시트는 여러 테마로 나누거나 전체 스타일시트를 css 필드를 통해 주입하는 플러그인으로 전환하세요(크기 제한 없음).
나는 PyPI에 플러그인을 배포하고 싶다.
대시보드 플러그인은 pip 엔트리 포인트가 아니라 디렉토리 구조로 설치됩니다. 현재 가장 깔끔한 배포 경로는 사용자가 ~/.hermes/plugins/에 클론하는 git 저장소입니다. 대시보드 플러그인을 위한 pip 기반 설치 프로그램은 현재 연결되어 있지 않습니다.