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
package/server/logger.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Lightweight structured logger for the HeyHank server.
|
|
2
|
+
// Provides JSON-structured log output for operational events while
|
|
3
|
+
// keeping the familiar console.log interface for human-readable logs.
|
|
4
|
+
//
|
|
5
|
+
// Log file persistence:
|
|
6
|
+
// By default, all log output is also written to ~/.heyhank/logs/ with
|
|
7
|
+
// automatic rotation (oldest files deleted when total lines exceed 2M).
|
|
8
|
+
// Disable with HEYHANK_LOG_FILE=0, override dir with HEYHANK_LOG_DIR,
|
|
9
|
+
// and configure rotation with HEYHANK_LOG_MAX_LINES.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// import { log } from "./logger.js";
|
|
13
|
+
// log.info("ws-bridge", "Browser connected", { sessionId, browsers: 3 });
|
|
14
|
+
// log.warn("orchestrator", "Git fetch failed", { sessionId, error: "..." });
|
|
15
|
+
// log.error("cli-launcher", "Process crashed", { sessionId, exitCode: 1 });
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
mkdirSync,
|
|
19
|
+
openSync,
|
|
20
|
+
writeSync,
|
|
21
|
+
closeSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
statSync,
|
|
24
|
+
unlinkSync,
|
|
25
|
+
} from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { HEYHANK_HOME } from "./paths.js";
|
|
28
|
+
import { countFileLines } from "./fs-utils.js";
|
|
29
|
+
|
|
30
|
+
type LogLevel = "info" | "warn" | "error";
|
|
31
|
+
|
|
32
|
+
interface LogEntry {
|
|
33
|
+
ts: string;
|
|
34
|
+
level: LogLevel;
|
|
35
|
+
module: string;
|
|
36
|
+
msg: string;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const STRUCTURED = (process.env.HEYHANK_LOG_FORMAT || process.env.COMPANION_LOG_FORMAT) === "json";
|
|
41
|
+
|
|
42
|
+
function formatEntry(level: LogLevel, module: string, msg: string, data?: Record<string, unknown>): string {
|
|
43
|
+
if (STRUCTURED) {
|
|
44
|
+
const entry: LogEntry = {
|
|
45
|
+
...data,
|
|
46
|
+
ts: new Date().toISOString(),
|
|
47
|
+
level,
|
|
48
|
+
module,
|
|
49
|
+
msg,
|
|
50
|
+
};
|
|
51
|
+
return JSON.stringify(entry);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Human-readable format (default): [module] msg key=value key=value
|
|
55
|
+
let line = `[${module}] ${msg}`;
|
|
56
|
+
if (data) {
|
|
57
|
+
const pairs = Object.entries(data)
|
|
58
|
+
.map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : v}`)
|
|
59
|
+
.join(" ");
|
|
60
|
+
if (pairs) line += ` | ${pairs}`;
|
|
61
|
+
}
|
|
62
|
+
return line;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Log File Writer ────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const DEFAULT_LOG_MAX_LINES = 2_000_000;
|
|
68
|
+
const LOG_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Writes log lines to a file under ~/.heyhank/logs/ with automatic rotation.
|
|
72
|
+
* A new log file is created each time the server starts. When total lines across
|
|
73
|
+
* all log files exceed maxLines (default 2M), the oldest files are deleted.
|
|
74
|
+
*
|
|
75
|
+
* Follows the same pattern as RecorderManager for recordings.
|
|
76
|
+
*/
|
|
77
|
+
export class LogFileWriter {
|
|
78
|
+
readonly filePath: string;
|
|
79
|
+
private logsDir: string;
|
|
80
|
+
private maxLines: number;
|
|
81
|
+
private fd: number;
|
|
82
|
+
private closed = false;
|
|
83
|
+
private dirCreated = false;
|
|
84
|
+
private initialCleanupTimer: ReturnType<typeof setTimeout> | null = null;
|
|
85
|
+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
86
|
+
|
|
87
|
+
constructor(options?: { logsDir?: string; maxLines?: number }) {
|
|
88
|
+
this.logsDir = options?.logsDir ?? LogFileWriter.resolveDir();
|
|
89
|
+
this.maxLines =
|
|
90
|
+
options?.maxLines ??
|
|
91
|
+
(Number(process.env.HEYHANK_LOG_MAX_LINES || process.env.COMPANION_LOG_MAX_LINES) || DEFAULT_LOG_MAX_LINES);
|
|
92
|
+
|
|
93
|
+
this.ensureDir();
|
|
94
|
+
|
|
95
|
+
// Create a new log file for this server run and keep the fd open
|
|
96
|
+
const ts = new Date().toISOString().replace(/:/g, "-");
|
|
97
|
+
const pid = process.pid;
|
|
98
|
+
this.filePath = join(this.logsDir, `heyhank_${ts}_${pid}.log`);
|
|
99
|
+
this.fd = openSync(this.filePath, "a");
|
|
100
|
+
|
|
101
|
+
// Defer initial cleanup so it doesn't block the event loop at startup
|
|
102
|
+
this.initialCleanupTimer = setTimeout(() => {
|
|
103
|
+
this.initialCleanupTimer = null;
|
|
104
|
+
this.cleanup();
|
|
105
|
+
}, 2000);
|
|
106
|
+
if (this.initialCleanupTimer.unref) this.initialCleanupTimer.unref();
|
|
107
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), LOG_CLEANUP_INTERVAL_MS);
|
|
108
|
+
if (this.cleanupTimer.unref) this.cleanupTimer.unref();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static resolveDir(): string {
|
|
112
|
+
return process.env.HEYHANK_LOG_DIR || process.env.COMPANION_LOG_DIR || join(HEYHANK_HOME, "logs");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether log file writing is enabled. Disable with HEYHANK_LOG_FILE=0|false. */
|
|
116
|
+
static isEnabled(): boolean {
|
|
117
|
+
const env = process.env.HEYHANK_LOG_FILE || process.env.COMPANION_LOG_FILE;
|
|
118
|
+
if (env === "0" || env === "false") return false;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getLogsDir(): string {
|
|
123
|
+
return this.logsDir;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getMaxLines(): number {
|
|
127
|
+
return this.maxLines;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
write(line: string): void {
|
|
131
|
+
if (this.closed) return;
|
|
132
|
+
try {
|
|
133
|
+
writeSync(this.fd, line + "\n");
|
|
134
|
+
} catch {
|
|
135
|
+
// Never throw — logging must not disrupt normal operation
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Delete oldest log files until total lines are under maxLines.
|
|
141
|
+
* Skips the current log file (still being written to).
|
|
142
|
+
*/
|
|
143
|
+
cleanup(): number {
|
|
144
|
+
try {
|
|
145
|
+
this.ensureDir();
|
|
146
|
+
const files = readdirSync(this.logsDir).filter((f) => f.endsWith(".log"));
|
|
147
|
+
if (files.length === 0) return 0;
|
|
148
|
+
|
|
149
|
+
const entries: { filename: string; path: string; lines: number; mtimeMs: number }[] = [];
|
|
150
|
+
let totalLines = 0;
|
|
151
|
+
|
|
152
|
+
for (const filename of files) {
|
|
153
|
+
const fullPath = join(this.logsDir, filename);
|
|
154
|
+
const lines = countFileLines(fullPath);
|
|
155
|
+
let mtimeMs = 0;
|
|
156
|
+
try {
|
|
157
|
+
mtimeMs = statSync(fullPath).mtimeMs;
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
entries.push({ filename, path: fullPath, lines, mtimeMs });
|
|
162
|
+
totalLines += lines;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (totalLines <= this.maxLines) return 0;
|
|
166
|
+
|
|
167
|
+
// Sort oldest first
|
|
168
|
+
entries.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
169
|
+
|
|
170
|
+
let deleted = 0;
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (totalLines <= this.maxLines) break;
|
|
173
|
+
// Don't delete the current log file
|
|
174
|
+
if (entry.path === this.filePath) continue;
|
|
175
|
+
try {
|
|
176
|
+
unlinkSync(entry.path);
|
|
177
|
+
totalLines -= entry.lines;
|
|
178
|
+
deleted++;
|
|
179
|
+
} catch {
|
|
180
|
+
// File may have been removed concurrently
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (deleted > 0) {
|
|
185
|
+
// Log to console only (avoid recursion)
|
|
186
|
+
console.log(`[logger] Cleanup: deleted ${deleted} old log file(s), ${totalLines} lines remaining`);
|
|
187
|
+
}
|
|
188
|
+
return deleted;
|
|
189
|
+
} catch {
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
close(): void {
|
|
195
|
+
this.closed = true;
|
|
196
|
+
if (this.initialCleanupTimer) {
|
|
197
|
+
clearTimeout(this.initialCleanupTimer);
|
|
198
|
+
this.initialCleanupTimer = null;
|
|
199
|
+
}
|
|
200
|
+
if (this.cleanupTimer) {
|
|
201
|
+
clearInterval(this.cleanupTimer);
|
|
202
|
+
this.cleanupTimer = null;
|
|
203
|
+
}
|
|
204
|
+
try { closeSync(this.fd); } catch { /* ignore */ }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private ensureDir(): void {
|
|
208
|
+
if (this.dirCreated) return;
|
|
209
|
+
mkdirSync(this.logsDir, { recursive: true });
|
|
210
|
+
this.dirCreated = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Singleton file writer (initialized lazily) ─────────────────────────────
|
|
215
|
+
|
|
216
|
+
let fileWriter: LogFileWriter | null = null;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Initialize the log file writer. Call once at server startup.
|
|
220
|
+
* Returns the writer instance for status reporting, or null if disabled.
|
|
221
|
+
*/
|
|
222
|
+
export function initLogFile(options?: { logsDir?: string; maxLines?: number }): LogFileWriter | null {
|
|
223
|
+
if (!LogFileWriter.isEnabled()) return null;
|
|
224
|
+
if (fileWriter) {
|
|
225
|
+
fileWriter.close();
|
|
226
|
+
}
|
|
227
|
+
fileWriter = new LogFileWriter(options);
|
|
228
|
+
return fileWriter;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Shut down the log file writer (clears cleanup timer). */
|
|
232
|
+
export function closeLogFile(): void {
|
|
233
|
+
if (fileWriter) {
|
|
234
|
+
fileWriter.close();
|
|
235
|
+
fileWriter = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Public logger ──────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
export const log = {
|
|
242
|
+
info(module: string, msg: string, data?: Record<string, unknown>): void {
|
|
243
|
+
const line = formatEntry("info", module, msg, data);
|
|
244
|
+
console.log(line);
|
|
245
|
+
fileWriter?.write(line);
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
warn(module: string, msg: string, data?: Record<string, unknown>): void {
|
|
249
|
+
const line = formatEntry("warn", module, msg, data);
|
|
250
|
+
console.warn(line);
|
|
251
|
+
fileWriter?.write(line);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
error(module: string, msg: string, data?: Record<string, unknown>): void {
|
|
255
|
+
const line = formatEntry("error", module, msg, data);
|
|
256
|
+
console.error(line);
|
|
257
|
+
fileWriter?.write(line);
|
|
258
|
+
},
|
|
259
|
+
};
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// ─── MCP Server Registry ──────────────────────────────────────────────────────
|
|
2
|
+
// Manages MCP server configurations: add, remove, enable/disable, catalog install.
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { HEYHANK_HOME } from "./paths.js";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface McpServerEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
type: "stdio" | "http" | "sse";
|
|
16
|
+
command?: string;
|
|
17
|
+
args?: string[];
|
|
18
|
+
url?: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
assignedAgents: string[];
|
|
23
|
+
requiresAuth?: { field: string; label: string; helpText: string }[];
|
|
24
|
+
authValues?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CatalogEntry {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
category: string;
|
|
32
|
+
type: "stdio" | "http" | "sse";
|
|
33
|
+
command?: string;
|
|
34
|
+
args?: string[];
|
|
35
|
+
url?: string;
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
env?: Record<string, string>;
|
|
38
|
+
requiresAuth?: { field: string; label: string; helpText: string }[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const CONFIG_PATH = join(HEYHANK_HOME, "mcp-servers.json");
|
|
44
|
+
|
|
45
|
+
function ensureDir(): void {
|
|
46
|
+
if (!existsSync(HEYHANK_HOME)) {
|
|
47
|
+
mkdirSync(HEYHANK_HOME, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Persistence ────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function loadServers(): McpServerEntry[] {
|
|
54
|
+
ensureDir();
|
|
55
|
+
try {
|
|
56
|
+
if (existsSync(CONFIG_PATH)) {
|
|
57
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as McpServerEntry[];
|
|
58
|
+
}
|
|
59
|
+
} catch { /* ignore corrupt files */ }
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function saveServers(servers: McpServerEntry[]): void {
|
|
64
|
+
ensureDir();
|
|
65
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(servers, null, 2), "utf-8");
|
|
66
|
+
try {
|
|
67
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
68
|
+
} catch { /* may fail on some platforms */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function slugify(name: string): string {
|
|
74
|
+
return name
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.replace(/\s+/g, "-")
|
|
77
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
78
|
+
.replace(/-+/g, "-")
|
|
79
|
+
.replace(/^-|-$/g, "");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function uniqueId(name: string, existing: McpServerEntry[]): string {
|
|
83
|
+
const base = slugify(name);
|
|
84
|
+
if (!existing.some((s) => s.id === base)) return base;
|
|
85
|
+
// Append short suffix to avoid collision
|
|
86
|
+
const suffix = randomUUID().slice(0, 6);
|
|
87
|
+
return `${base}-${suffix}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Catalog ────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const CATALOG: CatalogEntry[] = [
|
|
93
|
+
{
|
|
94
|
+
id: "github",
|
|
95
|
+
name: "GitHub",
|
|
96
|
+
description: "Pull Requests, Issues und Repositories verwalten via GitHub API.",
|
|
97
|
+
category: "development",
|
|
98
|
+
type: "http",
|
|
99
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
100
|
+
requiresAuth: [
|
|
101
|
+
{ field: "GITHUB_PERSONAL_ACCESS_TOKEN", label: "GitHub Personal Access Token", helpText: "Erstelle ein Token unter github.com/settings/tokens mit repo & issues Berechtigung." },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "playwright",
|
|
106
|
+
name: "Playwright",
|
|
107
|
+
description: "Browser-Automatisierung und Testing mit Playwright MCP.",
|
|
108
|
+
category: "browser",
|
|
109
|
+
type: "stdio",
|
|
110
|
+
command: "npx",
|
|
111
|
+
args: ["@playwright/mcp@latest"],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "context7",
|
|
115
|
+
name: "Context7",
|
|
116
|
+
description: "Dokumentations-Lookup fuer Bibliotheken und Frameworks. Liefert aktuelle Docs direkt in den Kontext.",
|
|
117
|
+
category: "development",
|
|
118
|
+
type: "stdio",
|
|
119
|
+
command: "npx",
|
|
120
|
+
args: ["-y", "@upstash/context7-mcp@latest"],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "brave-search",
|
|
124
|
+
name: "Brave Search",
|
|
125
|
+
description: "Web-Suche ohne Google. Nutzt die Brave Search API fuer datenschutzfreundliche Suchergebnisse.",
|
|
126
|
+
category: "search",
|
|
127
|
+
type: "stdio",
|
|
128
|
+
command: "npx",
|
|
129
|
+
args: ["-y", "@anthropic/mcp-server-brave-search"],
|
|
130
|
+
env: { BRAVE_API_KEY: "" },
|
|
131
|
+
requiresAuth: [
|
|
132
|
+
{ field: "BRAVE_API_KEY", label: "Brave API Key", helpText: "API-Key von brave.com/search/api erstellen (kostenlos bis 2000 Anfragen/Monat)." },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "notion",
|
|
137
|
+
name: "Notion",
|
|
138
|
+
description: "Notion-Seiten und Datenbanken lesen und bearbeiten.",
|
|
139
|
+
category: "productivity",
|
|
140
|
+
type: "stdio",
|
|
141
|
+
command: "npx",
|
|
142
|
+
args: ["-y", "@anthropic/mcp-server-notion"],
|
|
143
|
+
env: { NOTION_API_KEY: "" },
|
|
144
|
+
requiresAuth: [
|
|
145
|
+
{ field: "NOTION_API_KEY", label: "Notion Integration Token", helpText: "Erstelle eine Integration unter notion.so/my-integrations und kopiere den Secret." },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "zapier",
|
|
150
|
+
name: "Zapier",
|
|
151
|
+
description: "Zugriff auf ueber 5000+ App-Automationen via Zapier MCP.",
|
|
152
|
+
category: "productivity",
|
|
153
|
+
type: "http",
|
|
154
|
+
url: "https://actions.zapier.com/mcp/",
|
|
155
|
+
requiresAuth: [
|
|
156
|
+
{ field: "ZAPIER_API_KEY", label: "Zapier API Key", helpText: "API-Key aus den Zapier-Einstellungen unter zapier.com/app/settings kopieren." },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "google-drive",
|
|
161
|
+
name: "Google Drive",
|
|
162
|
+
description: "Google Docs, Sheets und Drive-Dateien durchsuchen und verwalten.",
|
|
163
|
+
category: "productivity",
|
|
164
|
+
type: "stdio",
|
|
165
|
+
command: "npx",
|
|
166
|
+
args: ["-y", "@anthropic/mcp-server-google-drive"],
|
|
167
|
+
env: { GOOGLE_APPLICATION_CREDENTIALS: "" },
|
|
168
|
+
requiresAuth: [
|
|
169
|
+
{ field: "GOOGLE_APPLICATION_CREDENTIALS", label: "Google Service Account JSON-Pfad", helpText: "Pfad zur Service-Account JSON-Datei. Erstelle einen Service Account in der Google Cloud Console." },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "google-maps",
|
|
174
|
+
name: "Google Maps",
|
|
175
|
+
description: "Orte, Adressen und Routen via Google Maps API abfragen.",
|
|
176
|
+
category: "other",
|
|
177
|
+
type: "stdio",
|
|
178
|
+
command: "npx",
|
|
179
|
+
args: ["-y", "@anthropic/mcp-server-google-maps"],
|
|
180
|
+
env: { GOOGLE_MAPS_API_KEY: "" },
|
|
181
|
+
requiresAuth: [
|
|
182
|
+
{ field: "GOOGLE_MAPS_API_KEY", label: "Google Maps API Key", helpText: "API-Key in der Google Cloud Console unter APIs & Services erstellen." },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "puppeteer",
|
|
187
|
+
name: "Puppeteer",
|
|
188
|
+
description: "Browser-Automatisierung mit Puppeteer. Screenshots, Navigation, Formular-Ausfuellung.",
|
|
189
|
+
category: "browser",
|
|
190
|
+
type: "stdio",
|
|
191
|
+
command: "npx",
|
|
192
|
+
args: ["-y", "@anthropic/mcp-server-puppeteer"],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "memory",
|
|
196
|
+
name: "Memory",
|
|
197
|
+
description: "Langzeit-Gedaechtnis fuer Agenten. Speichert und ruft Wissen ueber Sessions hinweg ab.",
|
|
198
|
+
category: "ai",
|
|
199
|
+
type: "stdio",
|
|
200
|
+
command: "npx",
|
|
201
|
+
args: ["-y", "@anthropic/mcp-server-memory"],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "filesystem",
|
|
205
|
+
name: "Filesystem",
|
|
206
|
+
description: "Kontrollierter Dateizugriff. Lesen, Schreiben und Durchsuchen von Dateien in definierten Verzeichnissen.",
|
|
207
|
+
category: "other",
|
|
208
|
+
type: "stdio",
|
|
209
|
+
command: "npx",
|
|
210
|
+
args: ["-y", "@anthropic/mcp-server-filesystem", "/home"],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "slack",
|
|
214
|
+
name: "Slack",
|
|
215
|
+
description: "Slack-Nachrichten lesen und senden. Channels und Threads verwalten.",
|
|
216
|
+
category: "communication",
|
|
217
|
+
type: "http",
|
|
218
|
+
url: "https://slack.com/api/mcp",
|
|
219
|
+
requiresAuth: [
|
|
220
|
+
{ field: "SLACK_BOT_TOKEN", label: "Slack Bot Token", helpText: "Bot Token (xoxb-...) aus der Slack App Konfiguration unter api.slack.com/apps." },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "stripe",
|
|
225
|
+
name: "Stripe",
|
|
226
|
+
description: "Zahlungen, Kunden und Abonnements ueber die Stripe API verwalten.",
|
|
227
|
+
category: "database",
|
|
228
|
+
type: "http",
|
|
229
|
+
url: "https://mcp.stripe.com/",
|
|
230
|
+
requiresAuth: [
|
|
231
|
+
{ field: "STRIPE_API_KEY", label: "Stripe Secret Key", helpText: "Secret Key aus dem Stripe Dashboard unter dashboard.stripe.com/apikeys." },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "supabase",
|
|
236
|
+
name: "Supabase",
|
|
237
|
+
description: "Supabase-Datenbank abfragen und verwalten. SQL-Queries und Tabellen-Operationen.",
|
|
238
|
+
category: "database",
|
|
239
|
+
type: "http",
|
|
240
|
+
url: "https://supabase.com/api/mcp",
|
|
241
|
+
requiresAuth: [
|
|
242
|
+
{ field: "SUPABASE_URL", label: "Supabase Project URL", helpText: "Projekt-URL aus dem Supabase Dashboard (z.B. https://xyz.supabase.co)." },
|
|
243
|
+
{ field: "SUPABASE_SERVICE_ROLE_KEY", label: "Supabase Service Role Key", helpText: "Service Role Key aus den Supabase Projekt-Einstellungen > API." },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "firebase",
|
|
248
|
+
name: "Firebase",
|
|
249
|
+
description: "Firebase-Projekte verwalten. Firestore, Auth, Hosting und Cloud Functions.",
|
|
250
|
+
category: "database",
|
|
251
|
+
type: "stdio",
|
|
252
|
+
command: "npx",
|
|
253
|
+
args: ["-y", "firebase-mcp-server"],
|
|
254
|
+
env: { GOOGLE_APPLICATION_CREDENTIALS: "" },
|
|
255
|
+
requiresAuth: [
|
|
256
|
+
{ field: "GOOGLE_APPLICATION_CREDENTIALS", label: "Firebase Service Account JSON-Pfad", helpText: "Pfad zur Service-Account JSON-Datei aus der Firebase Console." },
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: "exa",
|
|
261
|
+
name: "Exa",
|
|
262
|
+
description: "KI-optimierte Web-Suche. Semantische Suche und Content-Extraktion.",
|
|
263
|
+
category: "search",
|
|
264
|
+
type: "stdio",
|
|
265
|
+
command: "npx",
|
|
266
|
+
args: ["-y", "@anthropic/mcp-server-exa"],
|
|
267
|
+
env: { EXA_API_KEY: "" },
|
|
268
|
+
requiresAuth: [
|
|
269
|
+
{ field: "EXA_API_KEY", label: "Exa API Key", helpText: "API-Key von exa.ai erstellen." },
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: "sentry",
|
|
274
|
+
name: "Sentry",
|
|
275
|
+
description: "Fehler-Tracking und Performance-Monitoring. Issues und Events aus Sentry abfragen.",
|
|
276
|
+
category: "development",
|
|
277
|
+
type: "stdio",
|
|
278
|
+
command: "npx",
|
|
279
|
+
args: ["-y", "@sentry/mcp-server"],
|
|
280
|
+
env: { SENTRY_AUTH_TOKEN: "" },
|
|
281
|
+
requiresAuth: [
|
|
282
|
+
{ field: "SENTRY_AUTH_TOKEN", label: "Sentry Auth Token", helpText: "Auth Token unter sentry.io/settings/auth-tokens erstellen." },
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "figma",
|
|
287
|
+
name: "Figma",
|
|
288
|
+
description: "Figma-Designs und Dateien lesen. Frames, Komponenten und Styles abfragen.",
|
|
289
|
+
category: "development",
|
|
290
|
+
type: "http",
|
|
291
|
+
url: "https://api.figma.com/mcp",
|
|
292
|
+
requiresAuth: [
|
|
293
|
+
{ field: "FIGMA_ACCESS_TOKEN", label: "Figma Access Token", helpText: "Personal Access Token unter figma.com/developers/api erstellen." },
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: "nanobanana",
|
|
298
|
+
name: "nanobanana",
|
|
299
|
+
description: "KI-Bildgenerierung. Erstellt Bilder aus Text-Beschreibungen via nanobanana.",
|
|
300
|
+
category: "ai",
|
|
301
|
+
type: "stdio",
|
|
302
|
+
command: "npx",
|
|
303
|
+
args: ["-y", "nanobanana-mcp"],
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
// ─── CRUD Operations ────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
export function listServers(): McpServerEntry[] {
|
|
310
|
+
return loadServers();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function getServer(id: string): McpServerEntry | undefined {
|
|
314
|
+
return loadServers().find((s) => s.id === id);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function addServer(entry: Omit<McpServerEntry, "id">): McpServerEntry {
|
|
318
|
+
const servers = loadServers();
|
|
319
|
+
const server: McpServerEntry = {
|
|
320
|
+
...entry,
|
|
321
|
+
id: uniqueId(entry.name, servers),
|
|
322
|
+
};
|
|
323
|
+
servers.push(server);
|
|
324
|
+
saveServers(servers);
|
|
325
|
+
return server;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function addFromCatalog(catalogId: string): McpServerEntry {
|
|
329
|
+
const template = CATALOG.find((c) => c.id === catalogId);
|
|
330
|
+
if (!template) {
|
|
331
|
+
throw new Error(`Catalog entry "${catalogId}" not found`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const servers = loadServers();
|
|
335
|
+
|
|
336
|
+
// Check if already installed
|
|
337
|
+
if (servers.some((s) => s.id === catalogId)) {
|
|
338
|
+
throw new Error(`MCP server "${template.name}" is already installed`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const server: McpServerEntry = {
|
|
342
|
+
id: template.id,
|
|
343
|
+
name: template.name,
|
|
344
|
+
description: template.description,
|
|
345
|
+
type: template.type,
|
|
346
|
+
command: template.command,
|
|
347
|
+
args: template.args ? [...template.args] : undefined,
|
|
348
|
+
url: template.url,
|
|
349
|
+
headers: template.headers ? { ...template.headers } : undefined,
|
|
350
|
+
env: template.env ? { ...template.env } : undefined,
|
|
351
|
+
enabled: true,
|
|
352
|
+
assignedAgents: ["*"],
|
|
353
|
+
requiresAuth: template.requiresAuth,
|
|
354
|
+
authValues: {},
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
servers.push(server);
|
|
358
|
+
saveServers(servers);
|
|
359
|
+
return server;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function updateServer(
|
|
363
|
+
id: string,
|
|
364
|
+
updates: Partial<McpServerEntry>,
|
|
365
|
+
): McpServerEntry | null {
|
|
366
|
+
const servers = loadServers();
|
|
367
|
+
const idx = servers.findIndex((s) => s.id === id);
|
|
368
|
+
if (idx === -1) return null;
|
|
369
|
+
|
|
370
|
+
// Prevent changing ID
|
|
371
|
+
const { id: _ignoreId, ...safeUpdates } = updates;
|
|
372
|
+
servers[idx] = { ...servers[idx], ...safeUpdates };
|
|
373
|
+
saveServers(servers);
|
|
374
|
+
return servers[idx];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function removeServer(id: string): boolean {
|
|
378
|
+
const servers = loadServers();
|
|
379
|
+
const idx = servers.findIndex((s) => s.id === id);
|
|
380
|
+
if (idx === -1) return false;
|
|
381
|
+
servers.splice(idx, 1);
|
|
382
|
+
saveServers(servers);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function getCatalog(): CatalogEntry[] {
|
|
387
|
+
const installed = loadServers();
|
|
388
|
+
// Return catalog with an "installed" hint by checking existing servers
|
|
389
|
+
return CATALOG.map((c) => ({
|
|
390
|
+
...c,
|
|
391
|
+
installed: installed.some((s) => s.id === c.id),
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function getServersForAgent(agentId: string): McpServerEntry[] {
|
|
396
|
+
return loadServers().filter((s) => {
|
|
397
|
+
if (!s.enabled) return false;
|
|
398
|
+
if (s.assignedAgents.includes("*")) return true;
|
|
399
|
+
return s.assignedAgents.includes(agentId);
|
|
400
|
+
});
|
|
401
|
+
}
|