Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ These are **not bundled with `hermes-agent`**. The core repo ships only the plug
| [`plugin-llm-async-example`](./plugin-llm-async-example) | `ctx.llm.acomplete()` + `asyncio.gather()` | Async LLM lane — concurrent forward + sentiment + back-translation pass for `/translate` |
| [`example-dashboard`](./example-dashboard) | `dashboard/manifest.json` | Bare-minimum dashboard plugin — a tab, a slot injection, a backend route |
| [`strike-freedom-cockpit`](./strike-freedom-cockpit) | dashboard theme + slot plugin | Complete custom-skin reskin — palette, layout variant, asset slots, sidebar HUD |
| [`theme-forge`](./theme-forge) | desktop plugin SDK + dashboard backend | Full theme editor — wallpapers, extended palette, bold, markdown accent colors, matrix rain — applied through Hermes' native skin path |

## Installing an example as a user plugin

Expand All @@ -26,6 +27,11 @@ cp -r hermes-example-plugins/plugin-llm-async-example ~/.hermes/plugins/
cp -r hermes-example-plugins/example-dashboard ~/.hermes/plugins/
cp -r hermes-example-plugins/strike-freedom-cockpit ~/.hermes/plugins/

# theme-forge is a *desktop* plugin: the JS goes in desktop-plugins/, only the
# backend (dashboard/) goes in plugins/. See theme-forge/README.md.
cp -r hermes-example-plugins/theme-forge ~/.hermes/desktop-plugins/
mkdir -p ~/.hermes/plugins/theme-forge && cp -r hermes-example-plugins/theme-forge/dashboard ~/.hermes/plugins/theme-forge/

# enable any with a slash command surface
hermes plugins enable plugin-llm-example
hermes plugins enable plugin-llm-async-example
Expand Down
21 changes: 21 additions & 0 deletions theme-forge/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Theme Forge contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
126 changes: 126 additions & 0 deletions theme-forge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Theme Forge — tema seu Hermes Desktop

**Plugin para o Hermes Desktop que faz o que o app não faz nativamente: temas
com imagem de fundo ou vídeo animado, paleta estendida, bold, tamanho de fonte,
cores de destaque no markdown e fundos animados (matrix rain, scanlines).**

- ✅ Sem build, sem tocar no código do app — um arquivo ESM + um backend Python opcional
- ✅ Aplicação em **1 clique** ("Aplicar") via o mecanismo nativo de skins do Hermes
- ✅ Edição ao vivo: as mudanças repintam na hora com o tema ativo
- ✅ 13 temas prontos, incluindo uma linha inspirada em filmes

## Temas incluídos

| Tema | Visual | Extras |
|---|---|---|
| **Forge Matrix** | Preto + verde fósforo | 🌧️ **Chuva digital animada** |
| **Forge Hacker** | Verde limão / roxo / amarelo, terminal | 📺 Scanlines CRT |
| **Forge Terminator** | HUD vermelho sobre gunmetal | 📺 Scanlines CRT |
| **Forge Spider-Man** | Preto + vermelho aranha | — |
| **Forge Wolverine** | Adamantium amarelo-mostarda + vermelho | — |
| **Forge Deadpool** | Vermelho mercenário sobre preto | — |
| **Forge Iron Man** | Vermelho arc-reactor + dourado | — |
| **Forge Batman** | Preto Gotham + amarelo bat-sinal | — |
| **Forge Dune** | Areia de Arrakis + âmbar de especiaria | — |
| **Forge Sith** | Preto imperial + sabre vermelho | — |
| Forge Cyber / Glass / Paper | Presets clássicos do Hermes | — |
| **Forge Custom** | Seu tema editável (persiste) | Tudo |

## O que ele faz que o Hermes não faz

| Recurso | Por que não existe nativamente |
|---|---|
| **Fundo com imagem ou vídeo animado** + overlay/blur | O modelo `DesktopTheme` só tem cores sólidas |
| **Paleta estendida** — `--ui-red/green/blue/purple/yellow/cyan/orange/warm` | Essas cores são fixas no `styles.css` do app |
| **Bold no texto** (3 níveis) | Sem controle de peso tipográfico por tema |
| **Tamanho da fonte da conversa** (11–18px) | Token existe mas sem UI |
| **Cores de destaque** (títulos, links, código no chat) | Não configurável |
| **Matrix rain animada** / scanlines CRT | Impossível no modelo de tema |
| **Aplicar tema com 1 clique** | O desktop não expõe `setTheme` a plugins |

## Instalação

### 1. Plugin desktop (obrigatório)

Copie a pasta para o diretório de plugins do Hermes Desktop:

```bash
mkdir -p ~/.hermes/desktop-plugins
cp -R theme-forge ~/.hermes/desktop-plugins/
```

(com perfil nomeado: `~/.hermes/profiles/<nome>/desktop-plugins/`)

### 2. Backend Python (recomendado — habilita o botão "Aplicar")

```bash
mkdir -p ~/.hermes/plugins
cp -R theme-forge ~/.hermes/plugins/ # usa dashboard/manifest.json + plugin_api.py
hermes plugins enable theme-forge 2>/dev/null || true
# garanta plugins.enabled no config.yaml:
hermes config set plugins.enabled '["theme-forge"]'
hermes gateway restart
```

> O backend monta `POST /api/plugins/theme-forge/activate` — grava o tema como
> skin Hermes real (`~/.hermes/skins/<nome>.yaml`) e ativa em ~1s em **todas as
> superfícies** (CLI, TUI, desktop). Sem ele, use ⌘K → Themes para ativar
> manualmente (os temas do forge aparecem na grade).

### 3. Ative o plugin no app

No Hermes Desktop: **⌘K → Reload desktop plugins**. A entrada **Theme Forge**
aparece na sidebar (ao lado de Capabilities/Messaging/Artifacts) e o painel
editor na barra lateral direita.

## Uso

1. **Aplicar um tema**: no painel, escolha o tema no seletor e clique **Aplicar**
— ou selecione em **⌘K → Themes** (a grade nativa).
2. **Editar**: abas **Cores** (núcleo + paleta estendida), **Fundo** (imagem ou
vídeo animado — URL ou "Procurar no Mac…", overlay 0–90%, blur 0–12px),
**Texto** (fonte, tamanho, bold, cores de destaque para títulos/links/código).
3. **Persistência**: tudo é salvo automaticamente; o Custom persiste entre
sessões. O tema ativo repinta ao vivo enquanto você edita. **O tema também
sobrevive a updates do Hermes**: no boot, o plugin re-afirma o skin
configurado (re-escreve `display.skin` → o watcher do gateway re-emite
`skin.changed` → o desktop re-aplica o tema mesmo quando o update reseta o
armazenamento local do app).

## Arquitetura

- `plugin.js` — contribui 13 temas (`THEMES_AREA`), painel + página
(`ROUTES_AREA` + `SIDEBAR_NAV_AREA`) e um **CSS injection engine**: um
`MutationObserver` no `<html>` detecta o tema ativo (`data-hermes-theme`) e
injeta `<style>` com cores/bold/tamanho; o fundo (imagem **ou** vídeo
`<video autoplay muted loop>`) é uma **camada de mídia** num container
`position:fixed; z-index:-1` (blur só no fundo, não na UI), com overlay, rain
e scanlines empilhados acima (z-index 0–3). Re-registrar um tema bumpa
`$registryVersion` → o app repinta ao vivo.
- `dashboard/plugin_api.py` — endpoint `POST /activate`: valida tokens hex
(whitelist), grava `skins/<nome>.yaml` e seta `display.skin`. O watcher do
gateway emite `skin.changed` e todas as superfícies repintam. O formato do
skin é o canônico do Hermes (`apps/shared/src/skin.ts`).

## Limitações conhecidas

- Imagem local vira **data URI** (limite ~2.5MB) e vídeo local ~3.5MB — acima
disso o fundo funciona na sessão mas não persiste (quota do armazenamento);
o plugin avisa. Para fundos grandes/definitivos, cole uma URL.
- O "Aplicar" usa a paleta do skin (conversão terminal-first); os **extras**
(imagem, bold, cores, rain) continuam vindo do plugin quando o tema está ativo.
- A ativação é global (skin do Hermes) — vale para CLI/TUI/desktop.

## Desenvolvimento / publicação

- Hot-reload: o app observa o arquivo; salve e aguarde ~5s (ou ⌘K → Reload
desktop plugins). Diagnóstico: `console.error` aparece em
`~/.hermes/logs/desktop.log`.
- Este plugin é distribuído como exemplo oficial —
[`NousResearch/hermes-example-plugins`](https://github.com/NousResearch/hermes-example-plugins).
Não há marketplace de desktop plugins ainda; temas de filme são inspirações
de paleta, sem associação oficial com os estúdios.

## Licença

MIT — veja `LICENSE`.
4 changes: 4 additions & 0 deletions theme-forge/dashboard/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "theme-forge",
"api": "plugin_api.py"
}
121 changes: 121 additions & 0 deletions theme-forge/dashboard/plugin_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Theme Forge backend — applies forge themes as live Hermes skins.

Rides the canonical skin path: writes ``$HERMES_HOME/skins/<name>.yaml`` and
sets ``display.skin`` so the gateway's skin watcher broadcasts ``skin.changed``
— the same live path the ``/skin`` command uses. Every Hermes surface (CLI,
TUI, desktop) repaints within ~1s, no restart needed.

Mounted at ``POST /api/plugins/theme-forge/activate`` by the desktop SDK's
plugin backend (``ctx.rest``). Only accepts a whitelisted subset of the
canonical skin tokens, validated as hex colors — never free-form YAML.
"""

from __future__ import annotations

import argparse
import re
from pathlib import Path

import yaml
from fastapi import APIRouter

from hermes_constants import get_hermes_home

router = APIRouter()

_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
_HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")

# Whitelisted canonical skin color tokens (see apps/shared/src/skin.ts).
_ALLOWED_TOKENS = {
"background",
"ui_text",
"ui_accent",
"ui_primary",
"ui_border",
"ui_error",
"banner_dim",
"banner_title",
"banner_text",
"status_bar_bg",
"completion_menu_bg",
"ui_ok",
"ui_warn",
}


def _skins_dir() -> Path:
return get_hermes_home() / "skins"


@router.post("/activate")
async def activate(body: dict):
name = str((body or {}).get("name") or "").strip()
if not _NAME_RE.match(name):
return {"ok": False, "error": "invalid skin name"}

raw_colors = (body or {}).get("colors") or {}
if not isinstance(raw_colors, dict):
return {"ok": False, "error": "colors must be an object"}

colors = {
key: value
for key, value in raw_colors.items()
if key in _ALLOWED_TOKENS and _HEX_RE.match(str(value))
}
if not colors:
return {"ok": False, "error": "no valid colors provided"}

label = str((body or {}).get("label") or name)
description = str((body or {}).get("description") or f"Forge theme {name}")

data = {
"name": name,
"description": description,
"colors": colors,
}

path = _skins_dir() / f"{name}.yaml"
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
except OSError as exc: # pragma: no cover - filesystem edge
return {"ok": False, "error": f"write failed: {exc}"}

try:
from hermes_cli.config import config_command

config_command(
argparse.Namespace(config_command="set", key="display.skin", value=name, force=True)
)
except Exception as exc: # pragma: no cover - surface backend failure to the UI
return {"ok": False, "error": f"config set failed: {exc}"}

return {"ok": True, "name": name, "label": label, "skin_path": str(path)}


@router.post("/reassert")
async def reassert(body: dict):
"""Re-assert the active forge skin after an app/backend restart.

The desktop applies a skin only when the gateway broadcasts ``skin.changed``
(it fires when ``display.skin`` or the skin YAML mtime changes). On boot the
desktop does NOT re-apply the configured skin, so after an update the theme
falls back to the app's local storage. Re-writing the same value bumps the
config mtime, which makes the gateway watcher broadcast again — every
surface re-applies the theme with no manual step.
"""
name = str((body or {}).get("name") or "").strip()
if not _NAME_RE.match(name):
return {"ok": False, "error": "invalid skin name"}
if not (_skins_dir() / f"{name}.yaml").exists():
return {"ok": False, "error": f"skin {name} not found — apply it from the editor first"}
try:
from hermes_cli.config import config_command

config_command(
argparse.Namespace(config_command="set", key="display.skin", value=name, force=True)
)
except Exception as exc: # pragma: no cover - surface backend failure to the UI
return {"ok": False, "error": f"config set failed: {exc}"}
return {"ok": True, "name": name, "reasserted": True}
Loading