@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 +21 -0
- package/README.md +86 -0
- package/dist/HermesOffice.d.ts +10 -0
- package/dist/HermesOffice.js +27 -0
- package/dist/event-source.d.ts +2 -0
- package/dist/event-source.js +31 -0
- package/dist/event-store.d.ts +15 -0
- package/dist/event-store.js +35 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|