본문으로 건너뛰기

대시보드 확장

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)는 코드베이스를 포크하지 않고도 리스킨 및 확장이 가능하도록 설계되었습니다. 세 가지 계층이 노출됩니다:

  1. 테마 — 대시보드의 팔레트, 타이포그래피, 레이아웃 및 구성 요소별 크롬을 재페인트하는 YAML 파일입니다. ~/.hermes/dashboard-themes/에 파일을 넣으면 테마 스위처에 나타납니다.
  2. UI 플러그인 — 탭을 등록하거나, 내장 페이지를 교체하거나, 페이지 범위 슬롯을 통해 하나를 확장하거나, 명명된 셸 슬롯에 컴포넌트를 주입하는 manifest.json + 자바스크립트 번들이 포함된 디렉토리.
  3. 백엔드 플러그인 — 플러그인 디렉토리 안에 있는 Python 파일로 FastAPI router를 노출합니다; 라우트는 /api/plugins/<name>/ 아래에 마운트되며 플러그인의 UI에서 호출됩니다.

세 가지 모두 런타임에 바로 적용됩니다: 저장소를 복제할 필요도, npm run build도, 대시보드 소스를 패치할 필요도 없습니다. 이 페이지가 세 가지 모두에 대한 정식 참조입니다.

대시보드만 사용하고 싶다면 웹 대시보드를 참조하세요. 터미널 CLI(웹 대시보드가 아님)를 리스킨하려면 스킨 & 테마를 참조하세요 — CLI 스킨 시스템은 대시보드 테마와 관련이 없습니다.

How the pieces compose

테마와 플러그인은 독립적이지만 시너지 효과를 냅니다. 테마는 단독으로 존재할 수 있습니다(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.warmGlowrgba(...)<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로 이어집니다 — 모든 둥근 요소가 함께 이동합니다.
densitycompact "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-&lt;name&gt;-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-&lt;bucket&gt;-&lt;kebab-property&gt;)가 됩니다. 따라서 card: override는 모든 &lt;Card&gt;에 적용되며, 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: &#123;&#125;
backdrop: &#123;&#125;
footer: &#123;&#125;
progress: &#123;&#125;
badge: &#123;&#125;
page: &#123;&#125;

지원되는 버킷: 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-&lt;kebab&gt; 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 &#123;
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;
&#125;

CSS는 테마 적용 시 단일 범위 &lt;style data-hermes-theme-css&gt; 태그로 주입되며 테마 전환 시 정리됩니다. 테마당 32 KiB로 제한됩니다.

내장 테마

각 내장 항목은 자체 팔레트, 타이포그래피 및 레이아웃을 제공하며 — 전환하면 색상뿐만 아니라 다른 눈에 띄는 변화도 발생합니다.

주제팔레트타이포그래피레이아웃
헤르메스 틸 (default)다크 티얼 + 크림시스템 스택, 15픽셀0.5rem 반경, 편안한
에르메스 틸 (대형) (default-large)기본값과 동일시스템 스택, 18px, 줄 높이 1.650.5rem 반경, 넓은
자정 (midnight)짙은 청자주인터 + JetBrains 모노, 14px0.75rem 반경, 편안한
엠버 (ember)따뜻한 진홍색 + 청동Spectral (세리프) + IBM Plex Mono, 15px0.25rem 반경, 편안한
모노 (mono)그레이스케일IBM Plex Sans + IBM Plex Mono, 13픽셀0 반경, 컴팩트
사이버펑크 (cyberpunk)검은색 위의 네온 그린Share Tech Mono 어디서나, 14px0 반경, 컴팩트
로제 (rose)핑크 + 아이보리Fraunces (세리프) + DM Mono, 16px1rem 반지름, 넓은

Google Fonts를 참조하는 테마( Hermes Teal 제외)는 필요에 따라 스타일시트를 로드합니다 — 처음으로 해당 테마로 전환할 때 &lt;link&gt; 태그가 &lt;head&gt;에 삽입됩니다.

전체 테마 YAML 참조

모든 노브를 한 파일에 — 필요 없는 것은 복사하고 잘라내세요:

# ~/.hermes/dashboard-themes/ocean.yaml
name: ocean
label: Ocean Deep
description: Deep sea blues with coral accents

# 3-layer palette (accepts &#123;hex, alpha&#125; 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/&lt;name&gt;/에 있는 다른 Hermes 플러그인 옆에 위치하며 — 대시보드 확장은 해당 플러그인 디렉토리 안의 dashboard/ 하위 폴더이므로, 하나의 플러그인으로 단일 설치에서 CLI/게이트웨이와 대시보드 둘 다 확장할 수 있습니다.

플러그인은 React나 UI 컴포넌트를 번들로 포함하지 않습니다. 이들은 window....``에 노출된 Plugin SDK를 사용합니다. 이는 플러그인 번들을 작게 유지하고(보통 몇 KB 정도) 버전 충돌을 방지합니다.

빠른 시작 — 첫 번째 플러그인

디렉토리 구조를 생성하세요:

mkdir -p ~/.hermes/plugins/my-plugin/dashboard/dist

매니페스트를 작성하세요:

// ~/.hermes/plugins/my-plugin/dashboard/manifest.json
&#123;
"name": "my-plugin",
"label": "My Plugin",
"icon": "Sparkles",
"version": "1.0.0",
"tab": &#123;
"path": "/my-plugin",
"position": "after:skills"
&#125;,
"entry": "dist/index.js"
&#125;

JS 번들 작성하기 (일반 IIFE — 빌드 단계 불필요):

// ~/.hermes/plugins/my-plugin/dashboard/dist/index.js
(function () &#123;
"use strict";

const SDK = window.`...`;
const &#123; React &#125; = SDK;
const &#123; Card, CardHeader, CardTitle, CardContent &#125; = SDK.components;

function MyPage() &#123;
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "My Plugin"),
),
React.createElement(CardContent, null,
React.createElement("p", &#123; className: "text-sm text-muted-foreground" &#125;,
"Hello from my custom dashboard tab.",
),
),
);
&#125;

window.`...`.register("my-plugin", MyPage);
&#125;)();

대시보드를 새로 고침하세요 — 탭은 내비게이션 바에서 기술 다음에 나타납니다.

Skip React.createElement

JSX를 선호한다면, React를 외부로 하고 IIFE 출력으로 하는 어떤 번들러(esbuild, Vite, rollup)든 사용하세요. 유일한 필수 조건은 최종 파일이 &lt;script&gt;를 통해 로드할 수 있는 단일 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 — 대시보드 백엔드 경로.

그 중 어느 것도 필수는 아닙니다; 필요한 레이어만 포함하세요.

선적 명세서 참조

&#123;
"name": "my-plugin",
"label": "My Plugin",
"description": "What this plugin does",
"icon": "Sparkles",
"version": "1.0.0",
"tab": &#123;
"path": "/my-plugin",
"position": "after:skills",
"재정의": "/",
"hidden": false
&#125;,
"slots": ["sidebar", "header-left"],
"entry": "dist/index.js",
"css": "dist/style.css",
"api": "plugin_api.py"
&#125;
필드필수설명
name고유한 플러그인 식별자. 소문자 사용, 하이픈 허용. URL 및 등록에 사용됨.
label탭 네비게이션에 표시되는 이름.
descriptionNo간단한 설명(대시보드 관리 화면에 표시됨).
iconNoLucide 아이콘 이름. 기본값은 Puzzle입니다. 알 수 없는 이름은 Puzzle로 대체됩니다.
versionNo세머버 문자열. 기본값은 0.0.0입니다.
tab.path탭의 URL 경로 (예: /my-plugin)
tab.positionNo탭을 삽입할 위치. "end" (기본값), "after:&lt;path&gt;", 또는 "before:&lt;path&gt;" — 콜론 다음의 값은 대상 탭의 경로 세그먼트입니다 (선행 슬래시 없음). 예시: "after:skills", "before:config".
tab.overrideNo새 탭을 추가하는 대신 해당 페이지를 대체하려면 내장 경로 ("/", "/sessions", "/config",...)로 설정하세요. 자세한 내용은 내장 페이지 대체를 참조하세요.
tab.hiddenNotrue일 경우, 컴포넌트와 모든 슬롯을 등록하지만 nav에 탭을 추가하지 않습니다. 슬롯 전용 플러그인에서 사용됩니다. 자세한 내용은 슬롯 전용 플러그인을 참조하십시오.
slotsNo이 플러그인이 채우는 명명된 셸 슬롯입니다. 문서용 보조 — 실제 등록은 JS 번들을 통해 registerSlot()에서 이루어집니다. 여기에 슬롯을 나열하면 검색 환경이 더 유익해집니다.
entrydashboard/에 상대적인 JS 번들 경로입니다. 기본값은 dist/index.js입니다.
cssNoCSS 파일을 &lt;link&gt; 태그로 주입할 경로.
apiNoFastAPI 경로가 있는 Python 파일의 경로. /api/plugins/&lt;name&gt;/에 마운트됨.

사용 가능한 아이콘

플러그인은 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.tsxICON_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&lt;Backdrop /&gt; 레이어 스택 안, 노이즈 레이어 위에.
header-left상단 바에서 에르메스 브랜드 앞에.
header-right상단 바의 테마/언어 전환기 전에.
header-banner네비게이션 아래 전체 폭 스트립.
sidebar조종석 사이드바 레일 — layoutVariant === "cockpit" 일 때만 렌더링됩니다.
pre-main경로 출구 위(&lt;main&gt; 내부).
post-main라우트 아웃렛 아래 (&lt;main&gt; 내부).
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:bottomiframe 위 / /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와 결합하세요.

셸은 위의 슬롯에 대해 &lt;PluginSlot name="..." /&gt;만 렌더링합니다. 추가 이름은 중첩 플러그인 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)**의 목적입니다.

모든 내장 페이지는 컨텐츠 영역의 상단과 하단에 렌더링되는 `&lt;page&gt;:top` 및 `&lt;page&gt;: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
&#123;
"name": "session-notes",
"label": "Session Notes",
"tab": &#123; "path": "/session-notes", "hidden": true &#125;,
"slots": ["sessions:top"],
"entry": "dist/index.js"
&#125;
````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/&lt;name&gt;/ 아래에 장착되므로, 위의 내용은 다음과 같이 됩니다:

  • GET /api/plugins/my-plugin/data
  • POST /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"
}

파일은 플러그인 로드 시 &lt;link&gt; 태그로 주입됩니다. 대시보드 스타일과의 충돌을 피하기 위해 특정 클래스 이름을 사용하고, 테마 인식을 유지하려면 대시보드의 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-&lt;bucket&gt;-*, --radius, --spacing-mul)으로 노출합니다. 그것들을 참조하면 플러그인이 활성 테마에 맞게 자동으로 리스킨됩니다.

플러그인 검색 및 다시 로드

대시보드는 dashboard/manifest.json를 위해 세 개의 디렉토리를 스캔합니다:

우선순위디렉토리소스 레이블
1 (충돌에서 승리)~/.hermes/plugins/&lt;name&gt;/dashboard/user
2&lt;repo&gt;/plugins/memory/&lt;name&gt;/dashboard/bundled
2&lt;repo&gt;/plugins/&lt;name&gt;/dashboard/bundled
3./.hermes/plugins/&lt;name&gt;/dashboard/projectHERMES_ENABLE_PROJECT_PLUGINS이 설정되어 있을 때만

탐색 결과는 대시보드 프로세스별로 캐시됩니다. 새 플러그인을 추가한 후에는 다음 중 하나를 수행하세요:

# Force a rescan without restart
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan

…또는 hermes dashboard를 재시작하세요.

플러그인 로드 수명 주기

  1. 대시보드가 로드됩니다. main.tsxwindow....에서 SDK를, `window.`...에서 레지스트리를 노출합니다.
  2. App.tsxusePlugins()를 호출 → GET /api/dashboard/plugins를 가져옵니다.
  3. 각 매니페스트마다: CSS &lt;link&gt;가 주입됩니다(선언된 경우), 그 다음에 &lt;script&gt; 태그가 JS 번들을 로드합니다.
  4. 플러그인의 IIFE가 실행되어 window.....register(name, Component)를 호출하고, 선택적으로 각 슬롯에 대해 .registerSlot(name, slot, Component).
  5. 대시보드는 등록된 컴포넌트를 매니페스트에 따라 해결하고, 탭을 네비게이션에 추가하며(hidden가 아닌 경우), 컴포넌트를 라우트로 마운트합니다.

플러그인은 스크립트가 로드된 후 최대 2초 동안 register()를 호출할 수 있습니다. 그 이후에는 대시보드가 더 이상 기다리지 않고 초기 렌더링을 완료합니다. 나중에 플러그인이 등록되더라도 여전히 나타납니다 — 네비게이션은 반응형입니다.

플러그인의 스크립트 로드에 실패할 경우(404, 구문 오류, IIFE 실행 중 예외 발생), 대시보드는 브라우저 콘솔에 경고를 기록하고 해당 플러그인 없이 계속 실행됩니다.


결합된 테마 + 플러그인 데모

strike-freedom-cockpit 플러그인(동반 저장소 hermes-example-plugins)은 완전한 리스킨 데모입니다. 이것은 테마 YAML과 슬롯 전용 플러그인을 결합하여 대시보드를 포크하지 않고 조종석 스타일의 HUD를 생성합니다.

무엇을 보여주는가:

  • 팔레트, 타이포그래피, fontUrl, layoutVariant: cockpit, assets, componentStyles (모서리가 잘린 카드, 그래디언트 배경), colorOverrides, 그리고 customCSS (스캔라인 오버레이)를 사용하는 전체 테마.
  • 세 개의 슬롯에 등록되는 슬롯 전용 플러그인 (tab.hidden: true):
    • sidebarSDK.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/themePUT활성 테마 설정. 본문: {"name": "midnight"}. dashboard.theme 아래 config.yaml에 유지됩니다.

플러그인 엔드포인트

엔드포인트방법설명
/api/dashboard/plugins받다발견된 플러그인 목록 (매니페스트 포함, 내부 필드 제외).
/api/dashboard/plugins/rescan받다재시작 없이 플러그인 디렉토리를 강제로 다시 스캔합니다.
/dashboard-plugins/&lt;name&gt;/&lt;path&gt;받다플러그인의 dashboard/ 디렉토리에서 정적 자산을 제공하세요. 경로 순회는 차단됩니다.
/api/plugins/&lt;name&gt;/**플러그인에 등록된 백엔드 경로.

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/ 아래에 로그를 기록합니다.

내 플러그인의 탭이 나타나지 않습니다.

  1. 매니페스트가 ~/.hermes/plugins/&lt;name&gt;/dashboard/manifest.json에 있는지 확인하세요 (dashboard/ 하위 디렉토리를 참고하세요).
  2. curl http://127.0.0.1:9119/api/dashboard/plugins/rescan 재발견을 강제로 수행합니다.
  3. 브라우저 개발자 도구 열기 → 네트워크 — manifest.json, index.js 및 404 없이 로드된 모든 CSS 확인.
  4. 브라우저 개발자 도구 → 콘솔을 열고 IIFE 또는 window.... is undefined 동안의 오류를 확인하세요 (SDK가 초기화되지 않았음을 나타내며, 보통 이전의 React 렌더 크래시 때문입니다).
  5. 번들 호출 window.....register(...)manifest.json:name같은 이름을 사용하는지 확인하세요.

슬롯에 등록된 컴포넌트는 렌더링되지 않습니다. sidebar 슬롯은 활성 테마에 layoutVariant: cockpit가 있을 때만 렌더링됩니다. 다른 슬롯은 항상 렌더링됩니다. 히트가 없는 슬롯에 등록하려면, 플러그인 번들이 실행되었는지 확인하기 위해 registerSlot 안에 console.log를 추가하세요.

플러그인 백엔드 경로가 404를 반환합니다.

  1. 매니페스트가 "api": "plugin_api.py"를 확인하고, dashboard/ 내부의 기존 파일을 가리키고 있는지 확인하세요.
  2. 재시작 hermes dashboard — 플러그인 API 경로는 시작 시 한 번만 마운트되며, 재스캔 시에는 되지 않습니다.
  3. plugin_api.py가 모듈 수준의 router = APIRouter()을 내보내는지 확인하세요. 다른 내보내기 이름은 선택되지 않습니다.
  4. Failed to load plugin &lt;name&gt; API routes~/.hermes/logs/errors.log 꼬리 — 가져오기 오류가 그곳에 기록됩니다.

테마 변경 시 내 색상 재정의가 사라집니다. colorOverrides는 활성 테마에 적용되며 테마를 전환하면 지워집니다 — 이것은 의도된 동작입니다. 지속되는 오버라이드를 원하면 라이브 스위처가 아닌 테마의 YAML에 넣으세요.

테마 customCSS가 잘립니다. customCSS 블록은 테마당 32KiB로 제한됩니다. 큰 스타일시트는 여러 테마로 나누거나 전체 스타일시트를 css 필드를 통해 주입하는 플러그인으로 전환하세요(크기 제한 없음).

나는 PyPI에 플러그인을 배포하고 싶다. 대시보드 플러그인은 pip 엔트리 포인트가 아니라 디렉토리 구조로 설치됩니다. 현재 가장 깔끔한 배포 경로는 사용자가 ~/.hermes/plugins/에 클론하는 git 저장소입니다. 대시보드 플러그인을 위한 pip 기반 설치 프로그램은 현재 연결되어 있지 않습니다.