heyhank 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/README.md +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// ─── Node Manager ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Orchestrates all federation connections. Singleton.
|
|
3
|
+
|
|
4
|
+
import type { ServerWebSocket } from "bun";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import type {
|
|
7
|
+
NodeMessage,
|
|
8
|
+
NodeStatus,
|
|
9
|
+
RemoteSessionSummary,
|
|
10
|
+
RemoteAgentSummary,
|
|
11
|
+
NodeSessionProxyMessage,
|
|
12
|
+
NodeSessionProxyResponseMessage,
|
|
13
|
+
} from "./node-types.js";
|
|
14
|
+
import { PROTOCOL_VERSION, PING_INTERVAL_MS } from "./node-types.js";
|
|
15
|
+
import { getNodeIdentity, listNodes, getNode } from "./node-store.js";
|
|
16
|
+
import { NodeConnection } from "./node-connection.js";
|
|
17
|
+
|
|
18
|
+
interface InboundPeer {
|
|
19
|
+
ws: WebSocket | ServerWebSocket<unknown>;
|
|
20
|
+
nodeId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
sessions: RemoteSessionSummary[];
|
|
23
|
+
lastPong: number;
|
|
24
|
+
pingTimer: ReturnType<typeof setInterval> | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ProxyCallback = (result: { result?: { replyText: string }; error?: string }) => void;
|
|
28
|
+
|
|
29
|
+
export class NodeManager {
|
|
30
|
+
/** Outbound connections keyed by node config ID */
|
|
31
|
+
private outbound = new Map<string, NodeConnection>();
|
|
32
|
+
/** Inbound connections keyed by remote node ID */
|
|
33
|
+
private inbound = new Map<string, InboundPeer>();
|
|
34
|
+
/** Pending proxy requests */
|
|
35
|
+
private pendingProxies = new Map<string, ProxyCallback>();
|
|
36
|
+
|
|
37
|
+
/** Provider: returns local session summaries for syncing */
|
|
38
|
+
getLocalSessions: (() => RemoteSessionSummary[]) | null = null;
|
|
39
|
+
/** Provider: handles a proxy request (runs command on local session) */
|
|
40
|
+
proxyHandler: ((sessionId: string, text: string) => Promise<{ replyText: string }>) | null = null;
|
|
41
|
+
|
|
42
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
initialize(): void {
|
|
45
|
+
const nodes = listNodes();
|
|
46
|
+
for (const node of nodes) {
|
|
47
|
+
this.connectNode(node.id);
|
|
48
|
+
}
|
|
49
|
+
console.log(`[federation] Initialized with ${nodes.length} configured node(s)`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
shutdown(): void {
|
|
53
|
+
for (const conn of this.outbound.values()) conn.destroy();
|
|
54
|
+
this.outbound.clear();
|
|
55
|
+
for (const info of this.inbound.values()) {
|
|
56
|
+
if (info.pingTimer) clearInterval(info.pingTimer);
|
|
57
|
+
try { (info.ws as WebSocket).close?.(); } catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
this.inbound.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
connectNode(nodeConfigId: string): void {
|
|
63
|
+
if (this.outbound.has(nodeConfigId)) return;
|
|
64
|
+
const config = getNode(nodeConfigId);
|
|
65
|
+
if (!config) return;
|
|
66
|
+
const conn = new NodeConnection(config, this);
|
|
67
|
+
this.outbound.set(nodeConfigId, conn);
|
|
68
|
+
conn.connect();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
disconnectNode(nodeConfigId: string): void {
|
|
72
|
+
const conn = this.outbound.get(nodeConfigId);
|
|
73
|
+
if (conn) {
|
|
74
|
+
conn.destroy();
|
|
75
|
+
this.outbound.delete(nodeConfigId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Inbound WebSocket handling ─────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
handleInboundConnection(ws: WebSocket | ServerWebSocket<unknown>): void {
|
|
82
|
+
let authTimeout: ReturnType<typeof setTimeout> | null = setTimeout(() => {
|
|
83
|
+
try { (ws as WebSocket).close?.(); } catch { /* ignore */ }
|
|
84
|
+
}, 10_000);
|
|
85
|
+
|
|
86
|
+
let authed = false;
|
|
87
|
+
const sendJson = (msg: NodeMessage) => {
|
|
88
|
+
try {
|
|
89
|
+
const payload = JSON.stringify(msg);
|
|
90
|
+
ws.send(payload);
|
|
91
|
+
} catch { /* ignore */ }
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onMessage = (data: string | Buffer | ArrayBuffer) => {
|
|
95
|
+
let msg: NodeMessage;
|
|
96
|
+
try {
|
|
97
|
+
msg = JSON.parse(typeof data === "string" ? data : data.toString()) as NodeMessage;
|
|
98
|
+
} catch { return; }
|
|
99
|
+
|
|
100
|
+
if (!authed) {
|
|
101
|
+
if (msg.type !== "auth") {
|
|
102
|
+
try { (ws as WebSocket).close?.(); } catch { /* ignore */ }
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (authTimeout) { clearTimeout(authTimeout); authTimeout = null; }
|
|
106
|
+
|
|
107
|
+
// Verify secret against any configured node
|
|
108
|
+
const nodes = listNodes();
|
|
109
|
+
const match = nodes.find((n) => n.secret === msg.secret);
|
|
110
|
+
if (!match) {
|
|
111
|
+
sendJson({ type: "auth_error", error: "invalid secret" });
|
|
112
|
+
setTimeout(() => { try { (ws as WebSocket).close?.(); } catch { /* ignore */ } }, 100);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
authed = true;
|
|
117
|
+
const identity = getNodeIdentity();
|
|
118
|
+
|
|
119
|
+
// Replace any old inbound from same nodeId
|
|
120
|
+
const old = this.inbound.get(msg.nodeId);
|
|
121
|
+
if (old) {
|
|
122
|
+
if (old.pingTimer) clearInterval(old.pingTimer);
|
|
123
|
+
try { (old.ws as WebSocket).close?.(); } catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const info: InboundPeer = {
|
|
127
|
+
ws,
|
|
128
|
+
nodeId: msg.nodeId,
|
|
129
|
+
name: msg.name || "Unknown",
|
|
130
|
+
sessions: [],
|
|
131
|
+
lastPong: Date.now(),
|
|
132
|
+
pingTimer: null,
|
|
133
|
+
};
|
|
134
|
+
this.inbound.set(msg.nodeId, info);
|
|
135
|
+
|
|
136
|
+
sendJson({ type: "auth_ok", nodeId: identity.nodeId, name: identity.name, version: PROTOCOL_VERSION });
|
|
137
|
+
|
|
138
|
+
// Start ping
|
|
139
|
+
info.pingTimer = setInterval(() => {
|
|
140
|
+
if (Date.now() - info.lastPong > PING_INTERVAL_MS * 3) {
|
|
141
|
+
console.log(`[federation] Inbound ping timeout for ${info.name}`);
|
|
142
|
+
try { (ws as WebSocket).close?.(); } catch { /* ignore */ }
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
sendJson({ type: "ping" });
|
|
146
|
+
}, PING_INTERVAL_MS);
|
|
147
|
+
|
|
148
|
+
console.log(`[federation] Inbound connection from ${info.name} (${info.nodeId})`);
|
|
149
|
+
|
|
150
|
+
// Send our sessions
|
|
151
|
+
sendJson({ type: "sessions_sync", sessions: this.getLocalSessionSummaries() });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Find inbound peer by ws reference
|
|
156
|
+
const peer = [...this.inbound.values()].find((i) => i.ws === ws);
|
|
157
|
+
if (!peer) return;
|
|
158
|
+
|
|
159
|
+
switch (msg.type) {
|
|
160
|
+
case "pong":
|
|
161
|
+
peer.lastPong = Date.now();
|
|
162
|
+
break;
|
|
163
|
+
case "ping":
|
|
164
|
+
sendJson({ type: "pong" });
|
|
165
|
+
break;
|
|
166
|
+
case "sessions_request":
|
|
167
|
+
sendJson({ type: "sessions_sync", sessions: this.getLocalSessionSummaries() });
|
|
168
|
+
break;
|
|
169
|
+
case "sessions_sync":
|
|
170
|
+
peer.sessions = msg.sessions ?? [];
|
|
171
|
+
break;
|
|
172
|
+
case "session_proxy":
|
|
173
|
+
this.handleProxyRequest({ sendMsg: sendJson }, msg);
|
|
174
|
+
break;
|
|
175
|
+
case "session_proxy_response":
|
|
176
|
+
if (msg.requestId) this.resolveProxy(msg.requestId, msg);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Bun ServerWebSocket uses `.data` property and events are handled via the global websocket handler,
|
|
182
|
+
// but for the federation inbound we store the handler and call it from index.ts
|
|
183
|
+
// Store message handler on the ws object for retrieval
|
|
184
|
+
(ws as unknown as Record<string, unknown>).__federationOnMessage = onMessage;
|
|
185
|
+
(ws as unknown as Record<string, unknown>).__federationOnClose = () => {
|
|
186
|
+
if (authTimeout) clearTimeout(authTimeout);
|
|
187
|
+
for (const [nodeId, info] of this.inbound) {
|
|
188
|
+
if (info.ws === ws) {
|
|
189
|
+
if (info.pingTimer) clearInterval(info.pingTimer);
|
|
190
|
+
this.inbound.delete(nodeId);
|
|
191
|
+
console.log(`[federation] Inbound disconnected: ${info.name}`);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Session summaries ──────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
getLocalSessionSummaries(): RemoteSessionSummary[] {
|
|
201
|
+
return this.getLocalSessions?.() ?? [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getRemoteSessions(): (RemoteSessionSummary & { nodeId: string; nodeName: string })[] {
|
|
205
|
+
const sessions: (RemoteSessionSummary & { nodeId: string; nodeName: string })[] = [];
|
|
206
|
+
const seen = new Set<string>();
|
|
207
|
+
|
|
208
|
+
for (const conn of this.outbound.values()) {
|
|
209
|
+
if (!conn.connected) continue;
|
|
210
|
+
for (const s of conn.remoteSessions) {
|
|
211
|
+
const key = `${conn.remoteNodeId}:${s.sessionId}`;
|
|
212
|
+
if (!seen.has(key)) {
|
|
213
|
+
seen.add(key);
|
|
214
|
+
sessions.push({ ...s, nodeId: conn.remoteNodeId!, nodeName: conn.remoteName! });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const info of this.inbound.values()) {
|
|
220
|
+
for (const s of info.sessions) {
|
|
221
|
+
const key = `${info.nodeId}:${s.sessionId}`;
|
|
222
|
+
if (!seen.has(key)) {
|
|
223
|
+
seen.add(key);
|
|
224
|
+
sessions.push({ ...s, nodeId: info.nodeId, nodeName: info.name });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return sessions;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getRemoteAgents(): (RemoteAgentSummary & { nodeId: string; nodeName: string })[] {
|
|
233
|
+
const agents: (RemoteAgentSummary & { nodeId: string; nodeName: string })[] = [];
|
|
234
|
+
// Agents sync is not yet implemented in the protocol, but return empty for now
|
|
235
|
+
// This will be populated when agents_sync messages are exchanged
|
|
236
|
+
return agents;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Node statuses ──────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
getNodeStatuses(): NodeStatus[] {
|
|
242
|
+
const nodes = listNodes();
|
|
243
|
+
return nodes.map((n) => {
|
|
244
|
+
const conn = this.outbound.get(n.id);
|
|
245
|
+
const connected = conn?.connected ?? false;
|
|
246
|
+
const remoteNodeId = conn?.remoteNodeId ?? null;
|
|
247
|
+
const remoteName = conn?.remoteName ?? null;
|
|
248
|
+
const sessionCount = conn?.remoteSessions.length ?? 0;
|
|
249
|
+
const inboundConnected = remoteNodeId ? this.inbound.has(remoteNodeId) : false;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...n,
|
|
253
|
+
connected,
|
|
254
|
+
inboundConnected,
|
|
255
|
+
remoteNodeId,
|
|
256
|
+
remoteName,
|
|
257
|
+
sessionCount,
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── Proxy ──────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
async sendToRemoteSession(sessionId: string, text: string): Promise<{ result?: { replyText: string }; error?: string }> {
|
|
265
|
+
// Find in outbound
|
|
266
|
+
for (const conn of this.outbound.values()) {
|
|
267
|
+
if (!conn.connected) continue;
|
|
268
|
+
if (conn.remoteSessions.some((s) => s.sessionId === sessionId)) {
|
|
269
|
+
return this.proxyVia(
|
|
270
|
+
(msg) => conn.sendMsg(msg),
|
|
271
|
+
sessionId,
|
|
272
|
+
text,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Find in inbound
|
|
277
|
+
for (const info of this.inbound.values()) {
|
|
278
|
+
if (info.sessions.some((s) => s.sessionId === sessionId)) {
|
|
279
|
+
return this.proxyVia(
|
|
280
|
+
(msg) => { try { info.ws.send(JSON.stringify(msg)); } catch { /* ignore */ } },
|
|
281
|
+
sessionId,
|
|
282
|
+
text,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { error: "session not found on any remote node" };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private proxyVia(
|
|
290
|
+
send: (msg: NodeMessage) => void,
|
|
291
|
+
sessionId: string,
|
|
292
|
+
text: string,
|
|
293
|
+
): Promise<{ result?: { replyText: string }; error?: string }> {
|
|
294
|
+
return new Promise((resolve) => {
|
|
295
|
+
const requestId = randomUUID();
|
|
296
|
+
const timeout = setTimeout(() => {
|
|
297
|
+
this.pendingProxies.delete(requestId);
|
|
298
|
+
resolve({ error: "proxy timeout" });
|
|
299
|
+
}, 120_000);
|
|
300
|
+
|
|
301
|
+
this.pendingProxies.set(requestId, (result) => {
|
|
302
|
+
clearTimeout(timeout);
|
|
303
|
+
resolve(result);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
send({ type: "session_proxy", requestId, sessionId, text });
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
resolveProxy(requestId: string, msg: NodeSessionProxyResponseMessage): void {
|
|
311
|
+
const cb = this.pendingProxies.get(requestId);
|
|
312
|
+
if (cb) {
|
|
313
|
+
this.pendingProxies.delete(requestId);
|
|
314
|
+
cb(msg.error ? { error: msg.error } : { result: msg.result });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async handleProxyRequest(
|
|
319
|
+
sender: { sendMsg: (msg: NodeMessage) => void },
|
|
320
|
+
msg: NodeSessionProxyMessage,
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
if (!this.proxyHandler) {
|
|
323
|
+
sender.sendMsg({ type: "session_proxy_response", requestId: msg.requestId, error: "no proxy handler" });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const result = await this.proxyHandler(msg.sessionId, msg.text);
|
|
328
|
+
sender.sendMsg({ type: "session_proxy_response", requestId: msg.requestId, result });
|
|
329
|
+
} catch (err) {
|
|
330
|
+
sender.sendMsg({
|
|
331
|
+
type: "session_proxy_response",
|
|
332
|
+
requestId: msg.requestId,
|
|
333
|
+
error: err instanceof Error ? err.message : String(err),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Events ─────────────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
onNodeConnected(conn: NodeConnection): void {
|
|
341
|
+
conn.sendMsg({ type: "sessions_sync", sessions: this.getLocalSessionSummaries() });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
onNodeDisconnected(conn: NodeConnection): void {
|
|
345
|
+
conn.remoteSessions = [];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
onSessionsUpdated(): void {
|
|
349
|
+
// Future: emit event bus events
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
broadcastLocalSessions(): void {
|
|
353
|
+
const sessions = this.getLocalSessionSummaries();
|
|
354
|
+
const msg: NodeMessage = { type: "sessions_sync", sessions };
|
|
355
|
+
for (const conn of this.outbound.values()) {
|
|
356
|
+
if (conn.connected) conn.sendMsg(msg);
|
|
357
|
+
}
|
|
358
|
+
for (const info of this.inbound.values()) {
|
|
359
|
+
try { info.ws.send(JSON.stringify(msg)); } catch { /* ignore */ }
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ─── Singleton ────────────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
export const nodeManager = new NodeManager();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ─── Node Store ───────────────────────────────────────────────────────────────
|
|
2
|
+
// File-based CRUD for node configs. Stores identity + peer list in HEYHANK_HOME.
|
|
3
|
+
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { randomUUID, randomBytes } from "node:crypto";
|
|
7
|
+
import { HEYHANK_HOME } from "../paths.js";
|
|
8
|
+
import type { NodeIdentity, NodeConfig } from "./node-types.js";
|
|
9
|
+
|
|
10
|
+
const IDENTITY_FILE = join(HEYHANK_HOME, "node-identity.json");
|
|
11
|
+
const NODES_FILE = join(HEYHANK_HOME, "nodes.json");
|
|
12
|
+
|
|
13
|
+
// ─── Node Identity ────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
let _identity: NodeIdentity | null = null;
|
|
16
|
+
|
|
17
|
+
export function getNodeIdentity(): NodeIdentity {
|
|
18
|
+
if (_identity) return _identity;
|
|
19
|
+
try {
|
|
20
|
+
if (existsSync(IDENTITY_FILE)) {
|
|
21
|
+
const raw = JSON.parse(readFileSync(IDENTITY_FILE, "utf-8")) as Partial<NodeIdentity>;
|
|
22
|
+
if (raw.nodeId && raw.name) {
|
|
23
|
+
_identity = raw as NodeIdentity;
|
|
24
|
+
return _identity;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch { /* corrupted file, regenerate */ }
|
|
28
|
+
|
|
29
|
+
_identity = {
|
|
30
|
+
nodeId: randomUUID(),
|
|
31
|
+
name: `Hank-${randomBytes(3).toString("hex")}`,
|
|
32
|
+
createdAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
mkdirSync(dirname(IDENTITY_FILE), { recursive: true });
|
|
35
|
+
writeFileSync(IDENTITY_FILE, JSON.stringify(_identity, null, 2));
|
|
36
|
+
return _identity;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function updateNodeName(name: string): NodeIdentity {
|
|
40
|
+
const id = getNodeIdentity();
|
|
41
|
+
id.name = name;
|
|
42
|
+
writeFileSync(IDENTITY_FILE, JSON.stringify(id, null, 2));
|
|
43
|
+
return id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Node Configs ─────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function readNodes(): NodeConfig[] {
|
|
49
|
+
try {
|
|
50
|
+
if (existsSync(NODES_FILE)) {
|
|
51
|
+
return JSON.parse(readFileSync(NODES_FILE, "utf-8")) as NodeConfig[];
|
|
52
|
+
}
|
|
53
|
+
} catch { /* corrupted */ }
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeNodes(nodes: NodeConfig[]): void {
|
|
58
|
+
mkdirSync(dirname(NODES_FILE), { recursive: true });
|
|
59
|
+
writeFileSync(NODES_FILE, JSON.stringify(nodes, null, 2));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function listNodes(): NodeConfig[] {
|
|
63
|
+
return readNodes();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getNode(id: string): NodeConfig | null {
|
|
67
|
+
return readNodes().find((n) => n.id === id) ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function addNode(opts: { url: string; secret: string; name: string }): NodeConfig {
|
|
71
|
+
const nodes = readNodes();
|
|
72
|
+
const node: NodeConfig = {
|
|
73
|
+
id: randomUUID(),
|
|
74
|
+
url: opts.url.replace(/\/+$/, ""),
|
|
75
|
+
secret: opts.secret,
|
|
76
|
+
name: opts.name || "Remote Node",
|
|
77
|
+
addedAt: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
nodes.push(node);
|
|
80
|
+
writeNodes(nodes);
|
|
81
|
+
return node;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function removeNode(id: string): void {
|
|
85
|
+
writeNodes(readNodes().filter((n) => n.id !== id));
|
|
86
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// ─── Federation Types ──────────────────────────────────────────────────────────
|
|
2
|
+
// Peer-to-peer WebSocket mesh for multi-node HeyHank connectivity.
|
|
3
|
+
|
|
4
|
+
export interface NodeIdentity {
|
|
5
|
+
nodeId: string;
|
|
6
|
+
name: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface NodeConfig {
|
|
11
|
+
id: string;
|
|
12
|
+
url: string;
|
|
13
|
+
secret: string;
|
|
14
|
+
name: string;
|
|
15
|
+
addedAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NodeStatus extends NodeConfig {
|
|
19
|
+
connected: boolean;
|
|
20
|
+
inboundConnected: boolean;
|
|
21
|
+
remoteNodeId: string | null;
|
|
22
|
+
remoteName: string | null;
|
|
23
|
+
sessionCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Protocol Messages ────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export type NodeMessage =
|
|
29
|
+
| NodeAuthMessage
|
|
30
|
+
| NodeAuthOkMessage
|
|
31
|
+
| NodeAuthErrorMessage
|
|
32
|
+
| NodePingMessage
|
|
33
|
+
| NodePongMessage
|
|
34
|
+
| NodeSessionsRequestMessage
|
|
35
|
+
| NodeSessionsSyncMessage
|
|
36
|
+
| NodeSessionProxyMessage
|
|
37
|
+
| NodeSessionProxyResponseMessage
|
|
38
|
+
| NodeAgentsSyncMessage;
|
|
39
|
+
|
|
40
|
+
export interface NodeAuthMessage {
|
|
41
|
+
type: "auth";
|
|
42
|
+
nodeId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
secret: string;
|
|
45
|
+
version: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface NodeAuthOkMessage {
|
|
49
|
+
type: "auth_ok";
|
|
50
|
+
nodeId: string;
|
|
51
|
+
name: string;
|
|
52
|
+
version: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface NodeAuthErrorMessage {
|
|
56
|
+
type: "auth_error";
|
|
57
|
+
error: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface NodePingMessage {
|
|
61
|
+
type: "ping";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface NodePongMessage {
|
|
65
|
+
type: "pong";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface NodeSessionsRequestMessage {
|
|
69
|
+
type: "sessions_request";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface NodeSessionsSyncMessage {
|
|
73
|
+
type: "sessions_sync";
|
|
74
|
+
sessions: RemoteSessionSummary[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface NodeSessionProxyMessage {
|
|
78
|
+
type: "session_proxy";
|
|
79
|
+
requestId: string;
|
|
80
|
+
sessionId: string;
|
|
81
|
+
text: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface NodeSessionProxyResponseMessage {
|
|
85
|
+
type: "session_proxy_response";
|
|
86
|
+
requestId: string;
|
|
87
|
+
result?: { replyText: string };
|
|
88
|
+
error?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface NodeAgentsSyncMessage {
|
|
92
|
+
type: "agents_sync";
|
|
93
|
+
agents: RemoteAgentSummary[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Remote Summaries ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export interface RemoteSessionSummary {
|
|
99
|
+
sessionId: string;
|
|
100
|
+
name?: string;
|
|
101
|
+
model?: string;
|
|
102
|
+
cwd?: string;
|
|
103
|
+
status?: string;
|
|
104
|
+
backendType?: string;
|
|
105
|
+
isConnected?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface RemoteAgentSummary {
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
backendType?: string;
|
|
113
|
+
status?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export const PROTOCOL_VERSION = 1;
|
|
119
|
+
export const PING_INTERVAL_MS = 30_000;
|
|
120
|
+
export const RECONNECT_MIN_MS = 1_000;
|
|
121
|
+
export const RECONNECT_MAX_MS = 30_000;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/** Count newlines in a file. Fast: reads raw buffer, counts 0x0A bytes. */
|
|
4
|
+
export function countFileLines(path: string): number {
|
|
5
|
+
try {
|
|
6
|
+
const buf = readFileSync(path);
|
|
7
|
+
let count = 0;
|
|
8
|
+
for (let i = 0; i < buf.length; i++) {
|
|
9
|
+
if (buf[i] === 0x0a) count++;
|
|
10
|
+
}
|
|
11
|
+
return count;
|
|
12
|
+
} catch {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
}
|