Initial Commit

This commit is contained in:
Zachariah K. Sharma
2026-06-05 23:22:08 -06:00
commit b78b25a5a5
12 changed files with 5063 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
.DS_Store
.env
*.log
*.tgz
*.zip
coverage/
node_modules/
state/
test/
+4
View File
@@ -0,0 +1,4 @@
HERMES_SETUP_UI_PORT=7843
HERMES_HOME_HOST=./state/hermes
CODEX_HOME_HOST=/Users/zachsharma/.codex
CLAUDE_HOME_HOST=/Users/zachsharma/.claude
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
.env
*.log
*.tgz
*.zip
coverage/
node_modules/
state/
+23
View File
@@ -0,0 +1,23 @@
FROM node:20-bookworm-slim
WORKDIR /app
COPY index.html app.js style.css server.cjs README.md ./
ENV NODE_ENV=production \
HERMES_SETUP_UI_HOST=0.0.0.0 \
HERMES_SETUP_UI_PORT=7843 \
HOME=/home/hermes \
HERMES_HOME=/home/hermes/.hermes \
HERMES_EXE=/home/hermes/.hermes/hermes-agent/venv/bin/hermes \
NO_COLOR=1 \
HERMES_NO_TUI=1
RUN useradd --uid 1000 --create-home --home-dir /home/hermes hermes \
&& chown -R hermes:hermes /app /home/hermes
USER hermes
EXPOSE 7843
CMD ["node", "/app/server.cjs"]
+63
View File
@@ -0,0 +1,63 @@
# Hermes Control Plane
A local, single-page operations console for Hermes Agent. The app is a small zero-dependency Node.js server that serves the UI and shells out to a Hermes CLI installation.
## What It Does
- Lists OAuth/API-key provider pools.
- Displays account identity/email data when it can be derived from Codex, Claude, Gemini, or Hermes JWT-backed auth state.
- Edits Hermes model/fallback configuration.
- Provides panes for skills, plugins, bundles, MCP, cron, sessions, hooks, memory, kanban, webhooks, profiles, storage, config, tools, insights, security, and system status.
## Run With Docker Compose
```bash
cp .env.example .env
docker compose up --build -d
docker compose logs -f
```
Open:
```text
http://127.0.0.1:7843
```
## Runtime Requirements
The container is Linux. It can only execute a Linux-compatible Hermes install at:
```text
/home/hermes/.hermes/hermes-agent/venv/bin/hermes
```
If you are on macOS, your local `~/.hermes` venv contains a macOS Python binary and cannot run inside this Linux container. Use a Linux Hermes home, or run the compose bundle on the Linux Hermes host/LXC.
The default `.env.example` mounts:
- Hermes state from `./state/hermes`
- Codex auth from `/Users/zachsharma/.codex`
- Claude auth from `/Users/zachsharma/.claude`
## Local Node Run
If Hermes is installed directly on the same OS as Node:
```bash
node server.cjs
```
The default local URL is:
```text
http://127.0.0.1:7843
```
## Useful Commands
```bash
npm run check
npm test
docker compose config
docker compose down
```
+1167
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
services:
hermes-control-plane:
build:
context: .
dockerfile: Dockerfile
image: hermes-control-plane:local
container_name: hermes-control-plane
restart: unless-stopped
ports:
- "${HERMES_SETUP_UI_PORT:-7843}:7843"
environment:
HOME: /home/hermes
HERMES_HOME: /home/hermes/.hermes
HERMES_EXE: /home/hermes/.hermes/hermes-agent/venv/bin/hermes
HERMES_SETUP_UI_HOST: 0.0.0.0
HERMES_SETUP_UI_PORT: 7843
NO_COLOR: "1"
HERMES_NO_TUI: "1"
volumes:
- ${HERMES_HOME_HOST:-./state/hermes}:/home/hermes/.hermes
- ${CODEX_HOME_HOST:-./state/codex}:/home/hermes/.codex:ro
- ${CLAUDE_HOME_HOST:-./state/claude}:/home/hermes/.claude:ro
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:7843/api/paths').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
+855
View File
@@ -0,0 +1,855 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hermes · Operations Console</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Stencil+Display:wght@500;700;800&family=JetBrains+Mono:wght@400;500;700&family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- printer registration crosshairs in the four page corners -->
<span class="reg reg-tl" aria-hidden="true"></span>
<span class="reg reg-tr" aria-hidden="true"></span>
<span class="reg reg-bl" aria-hidden="true"></span>
<span class="reg reg-br" aria-hidden="true"></span>
<!-- ─── Masthead / Index ──────────────────────────────────────────────── -->
<aside class="nav">
<header class="masthead">
<div class="masthead-title">HERMES</div>
<div class="masthead-sub">OPERATIONS&nbsp;CONSOLE</div>
<div class="masthead-meta">
<span>REV.</span><span id="masthead-version">———</span>
</div>
<div class="masthead-stamp" aria-hidden="true">
<span class="stamp-line"></span>
<span>LOCAL · LOOPBACK · :7843</span>
</div>
</header>
<nav class="dial">
<button class="dial-item" data-route="providers" data-active>
<span class="dial-num">01</span><span class="dial-name">Providers</span>
</button>
<button class="dial-item" data-route="routing">
<span class="dial-num">02</span><span class="dial-name">Routing</span>
</button>
<button class="dial-item" data-route="skills">
<span class="dial-num">03</span><span class="dial-name">Skills</span>
</button>
<button class="dial-item" data-route="plugins">
<span class="dial-num">04</span><span class="dial-name">Plugins</span>
</button>
<button class="dial-item" data-route="bundles">
<span class="dial-num">05</span><span class="dial-name">Bundles</span>
</button>
<button class="dial-item" data-route="curator">
<span class="dial-num">06</span><span class="dial-name">Curator</span>
</button>
<button class="dial-item" data-route="mcp">
<span class="dial-num">07</span><span class="dial-name">MCP</span>
</button>
<button class="dial-item" data-route="cron">
<span class="dial-num">08</span><span class="dial-name">Cron</span>
</button>
<button class="dial-item" data-route="sessions">
<span class="dial-num">09</span><span class="dial-name">Sessions</span>
</button>
<button class="dial-item" data-route="hooks">
<span class="dial-num">10</span><span class="dial-name">Hooks</span>
</button>
<button class="dial-item" data-route="memory">
<span class="dial-num">11</span><span class="dial-name">Memory</span>
</button>
<button class="dial-item" data-route="kanban">
<span class="dial-num">12</span><span class="dial-name">Kanban</span>
</button>
<button class="dial-item" data-route="webhooks">
<span class="dial-num">13</span><span class="dial-name">Webhooks</span>
</button>
<button class="dial-item" data-route="profiles">
<span class="dial-num">14</span><span class="dial-name">Profiles</span>
</button>
<button class="dial-item" data-route="storage">
<span class="dial-num">15</span><span class="dial-name">Storage</span>
</button>
<button class="dial-item" data-route="config">
<span class="dial-num">16</span><span class="dial-name">Config</span>
</button>
<div class="dial-divider"><span>VIEW · READ ONLY</span></div>
<button class="dial-item" data-route="tools">
<span class="dial-num">17</span><span class="dial-name">Tools</span>
</button>
<button class="dial-item" data-route="insights">
<span class="dial-num">18</span><span class="dial-name">Insights</span>
</button>
<button class="dial-item" data-route="security">
<span class="dial-num">19</span><span class="dial-name">Security</span>
</button>
<button class="dial-item" data-route="system">
<span class="dial-num">20</span><span class="dial-name">System</span>
</button>
</nav>
<footer class="nav-foot">
<div class="kv"><span class="kv-k">home</span><span class="kv-v" id="foot-home">·</span></div>
<div class="kv"><span class="kv-k">conf</span><span class="kv-v">config.yaml</span></div>
<div class="kv"><span class="kv-k">port</span><span class="kv-v">:7843</span></div>
</footer>
</aside>
<!-- ─── Main ─────────────────────────────────────────────────────────── -->
<main class="main">
<!-- §01 PROVIDERS ──────────────────────────────────────────────────── -->
<section class="pane" data-pane="providers" data-active>
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;01</span>
<span class="folio-tag">AUTH&nbsp;·&nbsp;POOLS&nbsp;·&nbsp;FAILOVER</span>
</div>
<h1 class="folio-title">Providers</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<div class="grid grid-4" id="provider-grid"></div>
<section class="block" data-block="01.E">
<header class="block-head">
<span class="block-id">§ 01.E</span>
<span class="block-name">DeepSeek · API key</span>
<span class="block-meta">last resort · written to .env</span>
</header>
<div class="key-row">
<input id="deepseek-key" type="password" placeholder="sk-XXXXXXXXXXXXXXXXXX" autocomplete="off" spellcheck="false" />
<button class="btn-primary" data-action="save-deepseek">Save</button>
</div>
<p class="status" id="deepseek-status"></p>
</section>
</section>
<!-- §02 ROUTING ───────────────────────────────────────────────────── -->
<section class="pane" data-pane="routing">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;02</span>
<span class="folio-tag">PRIMARY&nbsp;·&nbsp;FALLBACK&nbsp;CHAIN</span>
</div>
<h1 class="folio-title">Routing</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="02.A">
<header class="block-head">
<span class="block-id">§ 02.A</span>
<span class="block-name">Primary route</span>
<span class="block-meta">provider · model</span>
</header>
<div class="primary-row">
<select id="primary-provider"></select>
<input id="primary-model" placeholder="claude-opus-4.6" />
<button class="btn-primary" data-action="set-primary">Commit</button>
</div>
</section>
<section class="block" data-block="02.B">
<header class="block-head">
<span class="block-id">§ 02.B</span>
<span class="block-name">Fallback chain</span>
<span class="block-meta">ordered · tried top-to-bottom on failure</span>
<span class="block-actions">
<button class="btn-mini" data-action="add-fallback-row">+ Hop</button>
</span>
</header>
<ol class="chain-edit" id="chain-edit"></ol>
<div class="row gap">
<button class="btn-primary" data-action="save-chain">Save chain</button>
<button class="btn" data-action="reset-chain">Discard</button>
<span class="status" id="chain-status"></span>
</div>
</section>
</section>
<!-- §17 TOOLS · view-only ───────────────────────────────────────── -->
<section class="pane" data-pane="tools">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;17</span>
<span class="folio-tag">PER-PLATFORM&nbsp;TOOLSETS</span>
</div>
<h1 class="folio-title">Tools</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<pre class="raw" id="tools-raw">loading…</pre>
</section>
<!-- §03 SKILLS ────────────────────────────────────────────────────── -->
<section class="pane" data-pane="skills">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;03</span>
<span class="folio-tag">FILESYSTEM&nbsp;·&nbsp;HUB&nbsp;MARKETPLACE</span>
</div>
<h1 class="folio-title">Skills</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<div class="tabs" data-tab-group="skills">
<button class="tab" data-tab="installed" data-active>Installed&nbsp;<span class="tab-count" id="skills-count">·</span></button>
<button class="tab" data-tab="discover">Discover · Hermes Hub</button>
</div>
<div class="tab-body" data-tab-pane="skills:installed" data-active>
<div class="row gap" style="margin-bottom:14px">
<input id="skills-filter" placeholder="filter by name, tag, category…" style="flex:1" />
<select id="skills-cat-filter" style="width:200px"><option value="">all categories</option></select>
</div>
<div class="cards-grid" id="skills-grid"></div>
</div>
<div class="tab-body" data-tab-pane="skills:discover">
<div class="row gap" style="margin-bottom:14px">
<input id="skills-search" placeholder="search the hub (5000+ skills)…" style="flex:1" />
<select id="skills-source">
<option value="all">all sources</option>
<option value="official">official ★</option>
<option value="skills-sh">skills.sh</option>
<option value="clawhub">clawhub</option>
<option value="lobehub">lobehub</option>
<option value="github">github</option>
<option value="well-known">well-known</option>
<option value="browse-sh">browse.sh</option>
</select>
<button class="btn-primary" data-action="skills-discover">Search</button>
</div>
<div class="cards-grid" id="skills-discover-grid">
<div class="empty-pane">Pick a registry source and search. Results stream from <code>hermes skills search&nbsp;--json</code>.</div>
</div>
</div>
</section>
<!-- §07 MCP ───────────────────────────────────────────────────────── -->
<section class="pane" data-pane="mcp">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;07</span>
<span class="folio-tag">MODEL&nbsp;CONTEXT&nbsp;PROTOCOL</span>
</div>
<h1 class="folio-title">MCP Servers</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="05.A">
<header class="block-head">
<span class="block-id">§ 09.A</span>
<span class="block-name">Connected servers</span>
</header>
<ul class="list" id="mcp-list"></ul>
</section>
<section class="block" data-block="05.B">
<header class="block-head">
<span class="block-id">§ 07.B</span>
<span class="block-name">Add server</span>
<span class="block-meta">URL or stdio command</span>
</header>
<div class="mcp-form">
<input id="mcp-name" placeholder="name (e.g. github)" />
<input id="mcp-url" placeholder="https://… (url-mode)" />
<input id="mcp-command" placeholder="command (stdio-mode)" />
<input id="mcp-args" placeholder="args (space-separated)" />
<button class="btn-primary" data-action="add-mcp">Add</button>
</div>
<pre class="log" id="mcp-log" hidden></pre>
</section>
</section>
<!-- §06 CRON ──────────────────────────────────────────────────────── -->
<section class="pane" data-pane="cron">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;08</span>
<span class="folio-tag">SCHEDULED&nbsp;TASKS</span>
</div>
<h1 class="folio-title">Cron</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<ul class="list" id="cron-list"></ul>
</section>
<!-- §07 SESSIONS ──────────────────────────────────────────────────── -->
<section class="pane" data-pane="sessions">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;09</span>
<span class="folio-tag">TRAJECTORY&nbsp;STORE</span>
</div>
<h1 class="folio-title">Sessions</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="07.A">
<header class="block-head">
<span class="block-id">§ 09.A</span>
<span class="block-name">Maintenance</span>
</header>
<div class="row gap">
<span class="status" id="sessions-count">·</span>
<span class="grow"></span>
<label class="field-inline">
<span>older than</span>
<input id="prune-days" type="number" min="1" value="30" />
<span>days</span>
</label>
<button class="btn-primary" data-action="prune-sessions">Prune</button>
</div>
</section>
<pre class="raw" id="sessions-raw">loading…</pre>
</section>
<!-- §08 HOOKS ─────────────────────────────────────────────────────── -->
<section class="pane" data-pane="hooks">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;10</span>
<span class="folio-tag">SHELL-SCRIPT&nbsp;HOOKS</span>
</div>
<h1 class="folio-title">Hooks</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
<button class="btn-ghost" data-action="hooks-doctor">▷ Doctor</button>
</div>
</header>
<section class="block" data-block="08.A">
<header class="block-head">
<span class="block-id">§ 10.A</span>
<span class="block-name">Configured hooks</span>
<span class="block-meta">events · matcher · timeout · consent</span>
</header>
<pre class="raw" id="hooks-raw">loading…</pre>
</section>
<section class="block" data-block="08.B">
<header class="block-head">
<span class="block-id">§ 10.B</span>
<span class="block-name">Test event</span>
<span class="block-meta">fire all hooks matching event against synthetic payload</span>
</header>
<div class="row gap">
<select id="hook-event">
<option>pre_tool_call</option>
<option>post_tool_call</option>
<option>pre_llm_call</option>
<option>post_llm_call</option>
<option>pre_api_request</option>
<option>post_api_request</option>
<option>on_session_start</option>
<option>on_session_end</option>
<option>on_session_reset</option>
<option>subagent_stop</option>
</select>
<button class="btn-primary" data-action="hooks-test">Fire test</button>
</div>
<pre class="log" id="hooks-log" hidden></pre>
</section>
</section>
<!-- §04 PLUGINS ───────────────────────────────────────────────────── -->
<section class="pane" data-pane="plugins">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;04</span>
<span class="folio-tag">MANIFESTS&nbsp;·&nbsp;GITHUB&nbsp;MARKETPLACE</span>
</div>
<h1 class="folio-title">Plugins</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<div class="tabs" data-tab-group="plugins">
<button class="tab" data-tab="installed" data-active>Installed&nbsp;<span class="tab-count" id="plugins-count">·</span></button>
<button class="tab" data-tab="discover">Discover · GitHub</button>
</div>
<div class="tab-body" data-tab-pane="plugins:installed" data-active>
<div class="row gap" style="margin-bottom:14px">
<input id="plugins-filter" placeholder="filter by name, hook, source…" style="flex:1" />
<select id="plugins-status-filter" style="width:180px">
<option value="">all statuses</option>
<option value="enabled">enabled</option>
<option value="not enabled">not enabled</option>
<option value="disabled">disabled</option>
</select>
</div>
<div class="cards-grid" id="plugins-grid"></div>
</div>
<div class="tab-body" data-tab-pane="plugins:discover">
<div class="row gap" style="margin-bottom:14px">
<input id="plugins-search" placeholder="search GitHub for Hermes plugins…" style="flex:1" />
<button class="btn-primary" data-action="plugins-discover">Search</button>
</div>
<div class="row gap" style="margin-bottom:14px">
<input id="plugin-install-target" placeholder="install directly · owner/repo or https git URL" style="flex:1" />
<button class="btn" data-action="plugin-install-manual">Install</button>
</div>
<div class="cards-grid" id="plugins-discover-grid">
<div class="empty-pane">Press Search to fetch repos tagged <code>topic:hermes-plugin</code> on GitHub.</div>
</div>
<pre class="log" id="plugins-log" hidden></pre>
</div>
</section>
<!-- §05 BUNDLES ───────────────────────────────────────────────────── -->
<section class="pane" data-pane="bundles">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;05</span>
<span class="folio-tag">SKILL&nbsp;BUNDLES&nbsp;·&nbsp;SLASH&nbsp;COMMANDS</span>
</div>
<h1 class="folio-title">Bundles</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="05.A">
<header class="block-head">
<span class="block-id">§ 05.A</span>
<span class="block-name">Your bundles</span>
<span class="block-meta">create custom slash-commands grouping multiple skills</span>
</header>
<div id="user-bundles" class="cards-grid"></div>
<div class="row gap" style="margin-top:14px">
<input id="bundle-name" placeholder="bundle name" style="width:240px" />
<input id="bundle-skills" placeholder="skills · comma-separated · e.g. github-pr-workflow, claude-code" style="flex:1" />
<button class="btn-primary" data-action="bundle-create">Create</button>
</div>
<pre class="log" id="bundles-log" hidden></pre>
</section>
<section class="block" data-block="05.B">
<header class="block-head">
<span class="block-id">§ 05.B</span>
<span class="block-name">Bundled official skills</span>
<span class="block-meta">pre-shipped with this Hermes install · <span id="bundled-count">·</span></span>
</header>
<div class="row gap" style="margin-bottom:14px">
<input id="bundles-filter" placeholder="filter by name or category…" style="flex:1" />
<select id="bundles-cat-filter" style="width:200px"><option value="">all categories</option></select>
</div>
<div class="cards-grid" id="bundled-grid"></div>
</section>
</section>
<!-- §06 CURATOR ───────────────────────────────────────────────────── -->
<section class="pane" data-pane="curator">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;06</span>
<span class="folio-tag">SKILL&nbsp;MAINTENANCE&nbsp;DAEMON</span>
</div>
<h1 class="folio-title">Curator</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="curator-run">▷ Run now</button>
<button class="btn-ghost" data-action="curator-pause">⏸ Pause</button>
<button class="btn-ghost" data-action="curator-resume">▷ Resume</button>
<button class="btn-ghost" data-action="curator-prune">▷ Prune</button>
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<div class="stat-grid" id="curator-stats"></div>
<section class="block" data-block="06.A">
<header class="block-head">
<span class="block-id">§ 06.A</span>
<span class="block-name">Schedule</span>
</header>
<dl class="kv-list" id="curator-meta"></dl>
</section>
<section class="block" data-block="06.B">
<header class="block-head">
<span class="block-id">§ 06.B</span>
<span class="block-name">Least recently used skills</span>
<span class="block-meta">candidates for archival</span>
</header>
<table class="table" id="curator-recent">
<thead><tr><th>Skill</th><th>Activity</th><th>Uses</th><th>Last activity</th></tr></thead>
<tbody></tbody>
</table>
</section>
<section class="block" data-block="06.C">
<header class="block-head">
<span class="block-id">§ 06.C</span>
<span class="block-name">Archived</span>
<span class="block-meta">recoverable · never auto-deleted</span>
</header>
<ul class="list" id="curator-archived"></ul>
</section>
</section>
<!-- §10 MEMORY ────────────────────────────────────────────────────── -->
<section class="pane" data-pane="memory">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;11</span>
<span class="folio-tag">MEMORY.MD&nbsp;·&nbsp;USER.MD&nbsp;·&nbsp;EXTERNAL</span>
</div>
<h1 class="folio-title">Memory</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="10.A">
<header class="block-head">
<span class="block-id">§ 11.A</span>
<span class="block-name">External provider</span>
<span class="block-actions">
<button class="btn-mini" data-action="memory-off">Disable external</button>
</span>
</header>
<pre class="raw" id="memory-status-raw">loading…</pre>
</section>
<section class="block" data-block="10.B">
<header class="block-head">
<span class="block-id">§ 11.B</span>
<span class="block-name">MEMORY.md</span>
<span class="block-meta">agent notes · environment facts · conventions</span>
<span class="block-actions">
<button class="btn-mini" data-action="save-memory-md" data-file="MEMORY.md">Save</button>
</span>
</header>
<textarea id="memory-md" class="editor" spellcheck="false" placeholder="(empty)"></textarea>
</section>
<section class="block" data-block="10.C">
<header class="block-head">
<span class="block-id">§ 11.C</span>
<span class="block-name">USER.md</span>
<span class="block-meta">user profile · preferences · communication style</span>
<span class="block-actions">
<button class="btn-mini" data-action="save-memory-md" data-file="USER.md">Save</button>
</span>
</header>
<textarea id="user-md" class="editor" spellcheck="false" placeholder="(empty)"></textarea>
</section>
</section>
<!-- §11 KANBAN ────────────────────────────────────────────────────── -->
<section class="pane" data-pane="kanban">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;12</span>
<span class="folio-tag">MULTI-PROFILE&nbsp;TASK&nbsp;BOARD</span>
</div>
<h1 class="folio-title">Kanban</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="11.A">
<header class="block-head">
<span class="block-id">§ 12.A</span>
<span class="block-name">Boards</span>
</header>
<pre class="raw" id="kanban-boards-raw">loading…</pre>
</section>
<section class="block" data-block="11.B">
<header class="block-head">
<span class="block-id">§ 12.B</span>
<span class="block-name">Tasks</span>
</header>
<pre class="raw" id="kanban-tasks-raw">loading…</pre>
</section>
<section class="block" data-block="11.C">
<header class="block-head">
<span class="block-id">§ 12.C</span>
<span class="block-name">Create task</span>
</header>
<div class="row gap">
<input id="kanban-board" placeholder="board slug (optional)" style="width:200px" />
<input id="kanban-title" placeholder="task title" />
<button class="btn-primary" data-action="kanban-create">Create</button>
</div>
<pre class="log" id="kanban-log" hidden></pre>
</section>
</section>
<!-- §12 WEBHOOKS ──────────────────────────────────────────────────── -->
<section class="pane" data-pane="webhooks">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;13</span>
<span class="folio-tag">EVENT&nbsp;SUBSCRIPTIONS</span>
</div>
<h1 class="folio-title">Webhooks</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="12.A">
<header class="block-head">
<span class="block-id">§ 13.A</span>
<span class="block-name">Active subscriptions</span>
</header>
<pre class="raw" id="webhooks-raw">loading…</pre>
</section>
<section class="block" data-block="12.B">
<header class="block-head">
<span class="block-id">§ 13.B</span>
<span class="block-name">Add subscription</span>
</header>
<div class="row gap">
<input id="webhook-url" placeholder="https://target.example.com/hook" />
<input id="webhook-event" placeholder="event filter (optional)" style="width:240px" />
<button class="btn-primary" data-action="webhook-add">Add</button>
</div>
<div class="row gap" style="margin-top:8px">
<input id="webhook-test-target" placeholder="id or URL to test" />
<button class="btn" data-action="webhook-test">Send test POST</button>
</div>
<pre class="log" id="webhooks-log" hidden></pre>
</section>
</section>
<!-- §13 PROFILES ──────────────────────────────────────────────────── -->
<section class="pane" data-pane="profiles">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;14</span>
<span class="folio-tag">PROFILES&nbsp;·&nbsp;PAIRING</span>
</div>
<h1 class="folio-title">Profiles</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="13.A">
<header class="block-head">
<span class="block-id">§ 14.A</span>
<span class="block-name">Profiles</span>
</header>
<pre class="raw" id="profiles-raw">loading…</pre>
<div class="row gap" style="margin-top:14px">
<input id="profile-name" placeholder="profile name" style="width:240px" />
<button class="btn" data-action="profile-create">Create</button>
<button class="btn" data-action="profile-use">Switch to</button>
<button class="btn-danger" data-action="profile-delete">Delete</button>
</div>
<pre class="log" id="profiles-log" hidden></pre>
</section>
<section class="block" data-block="13.B">
<header class="block-head">
<span class="block-id">§ 14.B</span>
<span class="block-name">Pairing codes</span>
<span class="block-meta">DM user authorization</span>
<span class="block-actions">
<button class="btn-mini" data-action="pairing-clear">Clear pending</button>
</span>
</header>
<pre class="raw" id="pairing-raw">loading…</pre>
<div class="row gap" style="margin-top:14px">
<input id="pairing-subject" placeholder="code (to approve) or user (to revoke)" />
<button class="btn" data-action="pairing-approve">Approve</button>
<button class="btn-danger" data-action="pairing-revoke">Revoke</button>
</div>
</section>
</section>
<!-- §14 CONFIG ────────────────────────────────────────────────────── -->
<section class="pane" data-pane="config">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;16</span>
<span class="folio-tag">CONFIG.YAML&nbsp;·&nbsp;VIEW&nbsp;·&nbsp;CHECK</span>
</div>
<h1 class="folio-title">Config</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
<button class="btn-ghost" data-action="config-check">▷ Check</button>
<button class="btn-ghost" data-action="config-migrate">▷ Migrate</button>
</div>
</header>
<section class="block" data-block="14.A">
<header class="block-head">
<span class="block-id">§ 16.A</span>
<span class="block-name">Path</span>
</header>
<div class="kv" id="config-path-row" style="font-family:var(--ff-mono);font-size:11px"></div>
</section>
<section class="block" data-block="14.B">
<header class="block-head">
<span class="block-id">§ 16.B</span>
<span class="block-name">Resolved configuration</span>
<span class="block-meta">merged: file + env + defaults</span>
</header>
<pre class="raw" id="config-raw">loading…</pre>
</section>
<section class="block" data-block="14.C">
<header class="block-head">
<span class="block-id">§ 16.C</span>
<span class="block-name">Set key</span>
<span class="block-meta">dotted path · e.g. compression.threshold</span>
</header>
<div class="row gap">
<input id="config-key" placeholder="e.g. agent.reasoning_effort" />
<input id="config-value" placeholder="value" />
<button class="btn-primary" data-action="config-set">Set</button>
</div>
<pre class="log" id="config-log" hidden></pre>
</section>
</section>
<!-- §15 SECURITY ──────────────────────────────────────────────────── -->
<section class="pane" data-pane="security">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;19</span>
<span class="folio-tag">SUPPLY-CHAIN&nbsp;AUDIT</span>
</div>
<h1 class="folio-title">Security</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="security-run">▷ Run audit</button>
</div>
</header>
<pre class="raw" id="security-raw">Press “▷ Run audit”. Scans venv + plugin deps + pinned MCP servers against OSV.dev.</pre>
</section>
<!-- §16 STORAGE ───────────────────────────────────────────────────── -->
<section class="pane" data-pane="storage">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;15</span>
<span class="folio-tag">BACKUP&nbsp;·&nbsp;CHECKPOINTS</span>
</div>
<h1 class="folio-title">Storage</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="refresh">↻ Reload</button>
</div>
</header>
<section class="block" data-block="16.A">
<header class="block-head">
<span class="block-id">§ 15.A</span>
<span class="block-name">Backup archive</span>
<span class="block-meta">zip of config + skills + sessions + memories</span>
</header>
<div class="row gap">
<input id="backup-label" placeholder="optional label" style="width:280px" />
<button class="btn-primary" data-action="backup-full">Full backup</button>
<button class="btn" data-action="backup-quick">Quick snapshot</button>
</div>
<pre class="log" id="backup-log" hidden></pre>
</section>
<section class="block" data-block="16.B">
<header class="block-head">
<span class="block-id">§ 15.B</span>
<span class="block-name">Checkpoints store</span>
<span class="block-meta">shadow git · rollback snapshots</span>
<span class="block-actions">
<button class="btn-mini" data-action="checkpoints-prune">Prune</button>
<button class="btn-danger" data-action="checkpoints-clear">Clear all</button>
</span>
</header>
<pre class="raw" id="checkpoints-raw">loading…</pre>
</section>
</section>
<!-- §17 INSIGHTS ──────────────────────────────────────────────────── -->
<section class="pane" data-pane="insights">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;18</span>
<span class="folio-tag">USAGE&nbsp;·&nbsp;TOKENS&nbsp;·&nbsp;COSTS</span>
</div>
<h1 class="folio-title">Insights</h1>
<div class="folio-actions">
<label class="field-inline" style="padding-bottom:0">
<span>window</span>
<input id="insights-days" type="number" min="1" value="30" />
<span>days</span>
</label>
<button class="btn-ghost" data-action="insights-run">▷ Compute</button>
</div>
</header>
<pre class="raw" id="insights-raw">Press “▷ Compute”. Analyzes session trajectories for token usage, costs, tool patterns.</pre>
</section>
<!-- §18 SYSTEM ────────────────────────────────────────────────────── -->
<section class="pane" data-pane="system">
<header class="folio">
<div class="folio-rail">
<span class="folio-num">§&nbsp;20</span>
<span class="folio-tag">DIAGNOSTICS&nbsp;·&nbsp;LOGS&nbsp;·&nbsp;UPDATE</span>
</div>
<h1 class="folio-title">System</h1>
<div class="folio-actions">
<button class="btn-ghost" data-action="run-doctor">▷ Doctor</button>
<button class="btn-ghost" data-action="refresh">▷ Status</button>
<button class="btn-ghost" data-action="tail-logs">▷ Tail logs</button>
<button class="btn-ghost" data-action="prompt-size">▷ Prompt size</button>
<button class="btn-ghost" data-action="dump">▷ Dump</button>
<button class="btn-ghost" data-action="portal-info">▷ Portal</button>
<button class="btn-ghost" data-action="run-update">▷ Update</button>
</div>
</header>
<pre class="raw" id="system-raw">loading…</pre>
</section>
</main>
<!-- ─── Readout / status bar ─────────────────────────────────────────── -->
<footer class="readout">
<span class="rd-tag">OPS</span>
<span class="rd-sep"></span>
<span class="rd-cell"><span class="rd-k">pri</span> <span id="bar-primary">·</span></span>
<span class="rd-sep"></span>
<span class="rd-cell"><span class="rd-k">fbk</span> <span id="bar-chain">·</span></span>
<span class="rd-sep"></span>
<span class="rd-cell"><span class="rd-k">ver</span> <span id="bar-version">·</span></span>
<span class="rd-sep"></span>
<span class="grow"></span>
<span class="rd-cell rd-msg" id="bar-msg"></span>
<span class="rd-led" id="bar-led" aria-hidden="true"></span>
</footer>
<script src="app.js"></script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
{
"name": "hermes-control-plane",
"version": "0.1.0",
"private": true,
"description": "Dockerized Hermes Agent control plane.",
"type": "commonjs",
"scripts": {
"start": "node server.cjs",
"check": "node -c server.cjs && docker compose --env-file .env.example config",
"test": "node test/status-identities.test.cjs"
},
"engines": {
"node": ">=20"
},
"dependencies": {},
"devDependencies": {}
}
+1448
View File
File diff suppressed because it is too large Load Diff
+1313
View File
File diff suppressed because it is too large Load Diff
+128
View File
@@ -0,0 +1,128 @@
"use strict"
const assert = require("assert")
const fs = require("fs")
const http = require("http")
const os = require("os")
const path = require("path")
const { spawn } = require("child_process")
const root = path.join(__dirname, "..")
const server = path.join(root, "server.cjs")
function jwt(claims) {
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url")
const payload = Buffer.from(JSON.stringify(claims)).toString("base64url")
return `${header}.${payload}.`
}
function requestJson(url) {
return new Promise((resolve, reject) => {
http.get(url, (res) => {
let body = ""
res.setEncoding("utf8")
res.on("data", (chunk) => { body += chunk })
res.on("end", () => {
try {
resolve(JSON.parse(body))
} catch (err) {
reject(err)
}
})
}).on("error", reject)
})
}
async function waitForServer(url, proc) {
const deadline = Date.now() + 5000
while (Date.now() < deadline) {
if (proc.exitCode !== null) break
try {
return await requestJson(url)
} catch {
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
throw new Error("server did not become ready")
}
async function main() {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "hermes-control-plane-test-"))
const hermesHome = path.join(tmp, ".hermes")
const codexHome = path.join(tmp, ".codex")
fs.mkdirSync(hermesHome, { recursive: true })
fs.mkdirSync(codexHome, { recursive: true })
fs.writeFileSync(path.join(hermesHome, "config.yaml"), "model:\n provider: openai-codex\n default: gpt-5\nfallback_providers: []\n")
fs.writeFileSync(path.join(codexHome, "auth.json"), JSON.stringify({ tokens: { id_token: jwt({ email: "zach@example.com", name: "Zach" }) } }))
fs.writeFileSync(path.join(codexHome, "authEmma.json"), JSON.stringify({ tokens: { id_token: jwt({ email: "emma@example.com", name: "Emma" }) } }))
fs.writeFileSync(path.join(codexHome, "authMom.json"), JSON.stringify({ tokens: { id_token: jwt({ email: "mom@example.com", name: "Mom" }) } }))
fs.writeFileSync(path.join(hermesHome, "auth.json"), JSON.stringify({
credential_pool: {
"openai-codex": [
{ id: "codex-main", label: "default", access_token: jwt({ sub: "codex-main" }) },
{ id: "codex-emma", label: "emma", access_token: jwt({ sub: "codex-emma" }) },
{ id: "codex-mom", label: "mom", access_token: jwt({ sub: "codex-mom" }) }
],
anthropic: [
{ id: "claude-1", label: "claude", access_token: jwt({ email: "claude@example.com", name: "Claude User" }) }
],
"google-gemini-cli": [
{ id: "gemini-1", label: "gemini", id_token: jwt({ email: "gemini@example.com", name: "Gemini User" }) }
]
}
}))
const fakeHermes = path.join(tmp, "hermes")
fs.writeFileSync(fakeHermes, `#!/bin/sh
if [ "$1" = "auth" ] && [ "$2" = "list" ]; then
cat <<'EOF'
openai-codex (3 credentials):
#1 default ←
#2 emma
#3 mom
anthropic (1 credential):
#1 claude ←
google-gemini-cli (1 credential):
#1 gemini ←
EOF
elif [ "$1" = "fallback" ] && [ "$2" = "list" ]; then
echo "Primary: gpt-5 (via openai-codex)"
elif [ "$1" = "version" ]; then
echo "test-hermes"
else
echo "unsupported command: $*" >&2
exit 1
fi
`)
fs.chmodSync(fakeHermes, 0o755)
const env = {
...process.env,
HOME: tmp,
HERMES_HOME: hermesHome,
HERMES_EXE: fakeHermes,
HERMES_SETUP_UI_HOST: "127.0.0.1",
HERMES_SETUP_UI_PORT: "19743"
}
const proc = spawn(process.execPath, [server], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] })
try {
const status = await waitForServer("http://127.0.0.1:19743/api/status", proc)
const byProvider = Object.fromEntries(status.pools.map((pool) => [pool.provider, pool]))
assert.deepStrictEqual(byProvider["openai-codex"].entries.map((entry) => entry.email), [
"zach@example.com",
"emma@example.com",
"mom@example.com"
])
assert.strictEqual(byProvider.anthropic.entries[0].email, "claude@example.com")
assert.strictEqual(byProvider["google-gemini-cli"].entries[0].email, "gemini@example.com")
} finally {
proc.kill()
fs.rmSync(tmp, { recursive: true, force: true })
}
}
main().catch((err) => {
console.error(err)
process.exit(1)
})