@vgalletti/hermes-office 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # Hermes Office
2
+
3
+ Hermes Office is a Next.js-powered visual workspace for Hermes Agent activity.
4
+ It is a companion surface for an admin dashboard: Hermes Workspace remains the
5
+ operational console for chat, sessions, configuration, jobs, and costs.
6
+
7
+ ## Goals
8
+
9
+ - Render Hermes agents as visible characters in a pixel office.
10
+ - Consume normalized Hermes snapshots and server-sent events without polling.
11
+ - Keep Hermes secrets and dashboard tokens on the server side.
12
+ - Ship a reusable React package named `hermes-office`.
13
+
14
+ ## Architecture
15
+
16
+ ```text
17
+ Hermes API / Workspace events
18
+ -> Admin backend adapter
19
+ -> authenticated SSE endpoint
20
+ -> Hermes Office React renderer
21
+ ```
22
+
23
+ The current public contract accepts `HermesAgent` snapshots and incremental
24
+ `OfficeEvent` messages. The adapter is intentionally separate from the visual
25
+ renderer so the UI does not depend on Hermes internal files or credentials.
26
+
27
+ ## Pixel Agents attribution
28
+
29
+ This project vendors the Pixel Agents webview renderer under
30
+ `vendor/pixel-agents-webview`. The renderer and included assets are licensed
31
+ under MIT by Pablo De Lucca. Their copyright and license are preserved in that
32
+ directory and in `THIRD_PARTY_LICENSES_PIXEL_AGENTS.txt`.
33
+
34
+ The vendored renderer is the source for the full office engine: tiles, layout,
35
+ pathfinding, furniture, sprite animation, and character states. Hermes Office
36
+ will replace its temporary demo renderer with an adapter over that engine.
37
+
38
+ ## Development
39
+
40
+ ```bash
41
+ npm install
42
+ npm run dev
43
+ ```
44
+
45
+ Open `http://127.0.0.1:4173` for the Next.js demo.
46
+
47
+ Useful checks:
48
+
49
+ ```bash
50
+ npm run typecheck
51
+ npm run build
52
+ npm run demo:build
53
+ ```
54
+
55
+ ## Publishing
56
+
57
+ The package name is `hermes-office`.
58
+
59
+ ```bash
60
+ pnpm publish
61
+ pnpm add hermes-office
62
+ ```
63
+
64
+ The equivalent npm command is:
65
+
66
+ ```bash
67
+ npm install hermes-office
68
+ ```
69
+
70
+ ## Event contract
71
+
72
+ The backend exposes an authenticated SSE stream:
73
+
74
+ ```text
75
+ event: snapshot
76
+ data: {"agents":[...]}
77
+
78
+ event: agent.updated
79
+ data: {"agent":{...}}
80
+
81
+ event: agent.removed
82
+ data: {"id":"..."}
83
+ ```
84
+
85
+ The visual layer does not poll. It renders the last valid snapshot while the
86
+ SSE connection reconnects.
@@ -0,0 +1,10 @@
1
+ import type { HermesAgent, OfficeTheme } from "./types.js";
2
+ export type HermesOfficeProps = {
3
+ agents?: HermesAgent[];
4
+ className?: string;
5
+ theme?: Partial<OfficeTheme>;
6
+ engineUrl?: string;
7
+ title?: string;
8
+ height?: string;
9
+ };
10
+ export declare function HermesOffice({ agents, className, engineUrl, title, height }: HermesOfficeProps): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ export function HermesOffice({ agents = [], className, engineUrl = "/hermes-office", title = "Hermes Office", height = "calc(100vh - 9rem)" }) {
5
+ const frameRef = useRef(null);
6
+ const source = `${engineUrl.replace(/\/$/, "")}/index.html?external=1`;
7
+ useEffect(() => {
8
+ const frame = frameRef.current;
9
+ if (!frame?.contentWindow)
10
+ return;
11
+ const timer = window.setTimeout(() => {
12
+ const send = (data) => frame.contentWindow?.postMessage(data, window.location.origin);
13
+ const ids = agents.map((_, index) => index + 1);
14
+ send({ type: "existingAgents", agents: ids });
15
+ agents.forEach((agent, index) => {
16
+ const id = index + 1;
17
+ send({ type: "agentTeamInfo", id, agentName: agent.name });
18
+ if (agent.activity === "waiting")
19
+ send({ type: "agentStatus", id, status: "waiting", awaitingInput: true });
20
+ else if (agent.activity !== "idle")
21
+ send({ type: "agentToolStart", id, toolId: `hermes-${agent.id}`, status: `${agent.activity}: ${agent.name}` });
22
+ });
23
+ }, 350);
24
+ return () => window.clearTimeout(timer);
25
+ }, [agents]);
26
+ return _jsx("section", { className: className, "aria-label": title, children: _jsx("iframe", { ref: frameRef, src: source, title: title, className: "block w-full border-0 bg-background", style: { height, minHeight: 760 }, allow: "clipboard-read; clipboard-write" }) });
27
+ }
@@ -0,0 +1,2 @@
1
+ import type { AgentEventSource } from "./types.js";
2
+ export declare function createSseEventSource(url: string): AgentEventSource;
@@ -0,0 +1,31 @@
1
+ const eventTypes = ["snapshot", "agent.updated", "agent.removed"];
2
+ export function createSseEventSource(url) {
3
+ return {
4
+ subscribe(onEvent) {
5
+ const source = new EventSource(url);
6
+ const listeners = eventTypes.map((type) => {
7
+ const listener = (message) => {
8
+ try {
9
+ const payload = JSON.parse(message.data);
10
+ onEvent({ type, ...payload });
11
+ }
12
+ catch {
13
+ // Ignore malformed events. The next server snapshot restores state.
14
+ }
15
+ };
16
+ source.addEventListener(type, listener);
17
+ return { type, listener };
18
+ });
19
+ source.onerror = () => {
20
+ // EventSource reconnects by itself. Consumers should render the last
21
+ // valid snapshot while the connection is recovering.
22
+ };
23
+ return () => {
24
+ for (const { type, listener } of listeners) {
25
+ source.removeEventListener(type, listener);
26
+ }
27
+ source.close();
28
+ };
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,15 @@
1
+ import type { HermesAgent, OfficeEvent } from "./types.js";
2
+ export declare class OfficeEventStore {
3
+ private readonly agents;
4
+ constructor(initialAgents?: HermesAgent[]);
5
+ apply(event: OfficeEvent): HermesAgent[];
6
+ snapshot(): HermesAgent[];
7
+ }
8
+ export declare function sessionToAgent(session: {
9
+ id: string;
10
+ source?: string;
11
+ model?: string;
12
+ title?: string | null;
13
+ last_active?: number;
14
+ end_reason?: string | null;
15
+ }): HermesAgent;
@@ -0,0 +1,35 @@
1
+ export class OfficeEventStore {
2
+ agents = new Map();
3
+ constructor(initialAgents = []) {
4
+ for (const agent of initialAgents)
5
+ this.agents.set(agent.id, agent);
6
+ }
7
+ apply(event) {
8
+ if (event.type === "snapshot") {
9
+ this.agents.clear();
10
+ for (const agent of event.agents)
11
+ this.agents.set(agent.id, agent);
12
+ }
13
+ if (event.type === "agent.updated") {
14
+ const existing = this.agents.get(event.agent.id);
15
+ this.agents.set(event.agent.id, { ...existing, ...event.agent });
16
+ }
17
+ if (event.type === "agent.removed")
18
+ this.agents.delete(event.id);
19
+ return this.snapshot();
20
+ }
21
+ snapshot() {
22
+ return [...this.agents.values()];
23
+ }
24
+ }
25
+ export function sessionToAgent(session) {
26
+ return {
27
+ id: session.id,
28
+ name: session.title || session.source || "Hermes session",
29
+ activity: session.end_reason ? "idle" : "waiting",
30
+ source: session.source,
31
+ model: session.model,
32
+ sessionId: session.id,
33
+ lastActiveAt: session.last_active ? new Date(session.last_active * 1000).toISOString() : undefined,
34
+ };
35
+ }
@@ -0,0 +1,4 @@
1
+ export { HermesOffice } from "./HermesOffice.js";
2
+ export { OfficeEventStore, sessionToAgent } from "./event-store.js";
3
+ export { createSseEventSource } from "./event-source.js";
4
+ export type { AgentActivity, AgentEventSource, HermesAgent, HermesRunEvent, HermesSessionSummary, OfficeEvent, OfficeTheme, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { HermesOffice } from "./HermesOffice.js";
2
+ export { OfficeEventStore, sessionToAgent } from "./event-store.js";
3
+ export { createSseEventSource } from "./event-source.js";
@@ -0,0 +1,45 @@
1
+ export type AgentActivity = "idle" | "thinking" | "writing" | "reading" | "waiting" | "error";
2
+ export type HermesAgent = {
3
+ id: string;
4
+ name: string;
5
+ activity: AgentActivity;
6
+ source?: string;
7
+ model?: string;
8
+ sessionId?: string;
9
+ lastActiveAt?: string;
10
+ x?: number;
11
+ y?: number;
12
+ };
13
+ export type OfficeEvent = {
14
+ type: "snapshot";
15
+ agents: HermesAgent[];
16
+ } | {
17
+ type: "agent.updated";
18
+ agent: HermesAgent;
19
+ } | {
20
+ type: "agent.removed";
21
+ id: string;
22
+ };
23
+ export interface AgentEventSource {
24
+ subscribe(onEvent: (event: OfficeEvent) => void): () => void;
25
+ }
26
+ export type HermesSessionSummary = {
27
+ id: string;
28
+ source?: string;
29
+ model?: string;
30
+ title?: string | null;
31
+ last_active?: number;
32
+ end_reason?: string | null;
33
+ };
34
+ export type HermesRunEvent = {
35
+ type?: string;
36
+ event?: string;
37
+ data?: unknown;
38
+ };
39
+ export type OfficeTheme = {
40
+ floor: string;
41
+ wall: string;
42
+ desk: string;
43
+ text: string;
44
+ accent: string;
45
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@vgalletti/hermes-office",
3
+ "version": "0.1.0",
4
+ "description": "Canvas visualization library for Hermes agent activity.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "peerDependencies": {
21
+ "react": ">=19",
22
+ "react-dom": ">=19"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "26.1.1",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
28
+ "next": "^16.2.10",
29
+ "react": "^19.2.4",
30
+ "react-dom": "^19.2.4",
31
+ "typescript": "^5.7.0"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.lib.json",
35
+ "typecheck": "tsc --noEmit -p tsconfig.lib.json",
36
+ "dev": "next dev",
37
+ "demo:build": "next build",
38
+ "demo:start": "next start"
39
+ }
40
+ }