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,97 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import * as gitUtils from "../git-utils.js";
|
|
4
|
+
import type { PRPoller } from "../pr-poller.js";
|
|
5
|
+
|
|
6
|
+
export function registerGitRoutes(api: Hono, prPoller?: PRPoller): void {
|
|
7
|
+
api.get("/git/repo-info", (c) => {
|
|
8
|
+
const path = c.req.query("path");
|
|
9
|
+
if (!path) return c.json({ error: "path required" }, 400);
|
|
10
|
+
const info = gitUtils.getRepoInfo(path);
|
|
11
|
+
if (!info) return c.json({ error: "Not a git repository" }, 400);
|
|
12
|
+
return c.json(info);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
api.get("/git/branches", (c) => {
|
|
16
|
+
const repoRoot = c.req.query("repoRoot");
|
|
17
|
+
if (!repoRoot) return c.json({ error: "repoRoot required" }, 400);
|
|
18
|
+
try {
|
|
19
|
+
return c.json(gitUtils.listBranches(repoRoot));
|
|
20
|
+
} catch (e: unknown) {
|
|
21
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
api.post("/git/fetch", async (c) => {
|
|
26
|
+
const body = await c.req.json().catch(() => ({}));
|
|
27
|
+
const { repoRoot } = body;
|
|
28
|
+
if (!repoRoot) return c.json({ error: "repoRoot required" }, 400);
|
|
29
|
+
return c.json(gitUtils.gitFetch(repoRoot));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
api.get("/git/worktrees", (c) => {
|
|
33
|
+
const repoRoot = c.req.query("repoRoot");
|
|
34
|
+
if (!repoRoot) return c.json({ error: "repoRoot required" }, 400);
|
|
35
|
+
return c.json(gitUtils.listWorktrees(repoRoot));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
api.post("/git/worktree", async (c) => {
|
|
39
|
+
const body = await c.req.json().catch(() => ({}));
|
|
40
|
+
const { repoRoot, branch, baseBranch, createBranch } = body;
|
|
41
|
+
if (!repoRoot || !branch) return c.json({ error: "repoRoot and branch required" }, 400);
|
|
42
|
+
const result = gitUtils.ensureWorktree(repoRoot, branch, { baseBranch, createBranch });
|
|
43
|
+
return c.json(result);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
api.delete("/git/worktree", async (c) => {
|
|
47
|
+
const body = await c.req.json().catch(() => ({}));
|
|
48
|
+
const { repoRoot, worktreePath, force } = body;
|
|
49
|
+
if (!repoRoot || !worktreePath) return c.json({ error: "repoRoot and worktreePath required" }, 400);
|
|
50
|
+
const result = gitUtils.removeWorktree(repoRoot, worktreePath, { force });
|
|
51
|
+
return c.json(result);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
api.post("/git/pull", async (c) => {
|
|
55
|
+
const body = await c.req.json().catch(() => ({}));
|
|
56
|
+
const { cwd } = body;
|
|
57
|
+
if (!cwd) return c.json({ error: "cwd required" }, 400);
|
|
58
|
+
const result = gitUtils.gitPull(cwd);
|
|
59
|
+
let git_ahead = 0;
|
|
60
|
+
let git_behind = 0;
|
|
61
|
+
try {
|
|
62
|
+
const counts = execSync(
|
|
63
|
+
"git rev-list --left-right --count @{upstream}...HEAD",
|
|
64
|
+
{
|
|
65
|
+
cwd,
|
|
66
|
+
encoding: "utf-8",
|
|
67
|
+
timeout: 3000,
|
|
68
|
+
},
|
|
69
|
+
).trim();
|
|
70
|
+
const [behind, ahead] = counts.split(/\s+/).map(Number);
|
|
71
|
+
git_ahead = ahead || 0;
|
|
72
|
+
git_behind = behind || 0;
|
|
73
|
+
} catch {
|
|
74
|
+
// no upstream
|
|
75
|
+
}
|
|
76
|
+
return c.json({ ...result, git_ahead, git_behind });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
api.get("/git/pr-status", async (c) => {
|
|
80
|
+
const cwd = c.req.query("cwd");
|
|
81
|
+
const branch = c.req.query("branch");
|
|
82
|
+
if (!cwd || !branch) return c.json({ error: "cwd and branch required" }, 400);
|
|
83
|
+
|
|
84
|
+
if (prPoller) {
|
|
85
|
+
const cached = prPoller.getCached(cwd, branch);
|
|
86
|
+
if (cached) return c.json(cached);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { isGhAvailable, fetchPRInfoAsync } = await import("../github-pr.js");
|
|
90
|
+
if (!isGhAvailable()) {
|
|
91
|
+
return c.json({ available: false, pr: null });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const pr = await fetchPRInfoAsync(cwd, branch);
|
|
95
|
+
return c.json({ available: true, pr });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// ─── LLM Provider Routes ─────────────────────────────────────────────────────
|
|
2
|
+
// REST API for calling non-CLI LLM providers (Ollama, OpenRouter, Gemini)
|
|
3
|
+
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
import { streamSSE } from "hono/streaming";
|
|
6
|
+
import {
|
|
7
|
+
callLLM,
|
|
8
|
+
streamOllama,
|
|
9
|
+
listOllamaModels,
|
|
10
|
+
pullOllamaModel,
|
|
11
|
+
} from "../llm-providers.js";
|
|
12
|
+
import type { LLMProviderConfig, LLMMessage } from "../llm-providers.js";
|
|
13
|
+
|
|
14
|
+
export function registerLLMRoutes(api: Hono): void {
|
|
15
|
+
/** Call an LLM provider (non-streaming) */
|
|
16
|
+
api.post("/llm/chat", async (c) => {
|
|
17
|
+
const body = await c.req.json().catch(() => ({}));
|
|
18
|
+
|
|
19
|
+
if (!body.messages || !body.provider || !body.model) {
|
|
20
|
+
return c.json(
|
|
21
|
+
{ error: "messages, provider, and model are required" },
|
|
22
|
+
400,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config: LLMProviderConfig = {
|
|
27
|
+
provider: body.provider,
|
|
28
|
+
model: body.model,
|
|
29
|
+
apiKey: body.apiKey,
|
|
30
|
+
baseUrl: body.baseUrl,
|
|
31
|
+
temperature: body.temperature,
|
|
32
|
+
maxTokens: body.maxTokens,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const response = await callLLM(body.messages as LLMMessage[], config);
|
|
37
|
+
return c.json(response);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
return c.json({ error: message }, 500);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** Stream from Ollama via SSE */
|
|
45
|
+
api.post("/llm/stream", (c) => {
|
|
46
|
+
return streamSSE(c, async (stream) => {
|
|
47
|
+
let body: Record<string, unknown>;
|
|
48
|
+
try {
|
|
49
|
+
body = await c.req.json();
|
|
50
|
+
} catch {
|
|
51
|
+
await stream.writeSSE({ data: JSON.stringify({ error: "Invalid JSON" }) });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!body.messages || !body.model) {
|
|
56
|
+
await stream.writeSSE({
|
|
57
|
+
data: JSON.stringify({ error: "messages and model required" }),
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const config: LLMProviderConfig = {
|
|
63
|
+
provider: (body.provider as LLMProviderConfig["provider"]) || "ollama",
|
|
64
|
+
model: body.model as string,
|
|
65
|
+
baseUrl: body.baseUrl as string | undefined,
|
|
66
|
+
temperature: body.temperature as number | undefined,
|
|
67
|
+
maxTokens: body.maxTokens as number | undefined,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (config.provider === "ollama") {
|
|
72
|
+
for await (const chunk of streamOllama(
|
|
73
|
+
body.messages as LLMMessage[],
|
|
74
|
+
config,
|
|
75
|
+
)) {
|
|
76
|
+
await stream.writeSSE({
|
|
77
|
+
data: JSON.stringify(chunk),
|
|
78
|
+
});
|
|
79
|
+
if (chunk.done) break;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Non-streaming fallback for other providers
|
|
83
|
+
const response = await callLLM(
|
|
84
|
+
body.messages as LLMMessage[],
|
|
85
|
+
config,
|
|
86
|
+
);
|
|
87
|
+
await stream.writeSSE({
|
|
88
|
+
data: JSON.stringify({ content: response.content, done: true }),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
await stream.writeSSE({ data: JSON.stringify({ error: message }) });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/** List available Ollama models */
|
|
99
|
+
api.get("/llm/ollama/models", async (c) => {
|
|
100
|
+
const models = await listOllamaModels();
|
|
101
|
+
return c.json({ models });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** Pull an Ollama model */
|
|
105
|
+
api.post("/llm/ollama/pull", async (c) => {
|
|
106
|
+
const body = await c.req.json().catch(() => ({}));
|
|
107
|
+
if (!body.model) {
|
|
108
|
+
return c.json({ error: "model is required" }, 400);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
await pullOllamaModel(body.model);
|
|
112
|
+
return c.json({ ok: true, model: body.model });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
115
|
+
return c.json({ error: message }, 500);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/** Get available providers and their status */
|
|
120
|
+
api.get("/llm/providers", async (c) => {
|
|
121
|
+
const providers = [];
|
|
122
|
+
|
|
123
|
+
// Check Ollama
|
|
124
|
+
try {
|
|
125
|
+
const models = await listOllamaModels();
|
|
126
|
+
providers.push({
|
|
127
|
+
name: "ollama",
|
|
128
|
+
status: "available",
|
|
129
|
+
models: models.map((m) => m.name),
|
|
130
|
+
endpoint: "http://localhost:11434",
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
providers.push({ name: "ollama", status: "unavailable", models: [] });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check OpenRouter
|
|
137
|
+
providers.push({
|
|
138
|
+
name: "openrouter",
|
|
139
|
+
status: process.env.OPENROUTER_API_KEY ? "configured" : "needs_api_key",
|
|
140
|
+
endpoint: "https://openrouter.ai/api/v1",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Check Gemini
|
|
144
|
+
providers.push({
|
|
145
|
+
name: "gemini",
|
|
146
|
+
status: process.env.GEMINI_API_KEY ? "configured" : "needs_api_key",
|
|
147
|
+
endpoint: "https://generativelanguage.googleapis.com/v1beta",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Claude (always via CLI)
|
|
151
|
+
providers.push({
|
|
152
|
+
name: "claude",
|
|
153
|
+
status: "available",
|
|
154
|
+
note: "Uses Claude CLI (Max subscription)",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Codex (always via CLI)
|
|
158
|
+
providers.push({
|
|
159
|
+
name: "codex",
|
|
160
|
+
status: "available",
|
|
161
|
+
note: "Uses Codex CLI",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return c.json({ providers });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// ─── Media Routes ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Image generation (Imagen) and Video generation (Veo) via Google AI APIs
|
|
3
|
+
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join, basename } from "node:path";
|
|
7
|
+
import { HEYHANK_HOME } from "../paths.js";
|
|
8
|
+
import { generateImage, generateVideo, pollVideoOperation, listMedia } from "../google-media.js";
|
|
9
|
+
|
|
10
|
+
const MEDIA_DIR = join(HEYHANK_HOME, "media");
|
|
11
|
+
|
|
12
|
+
const MIME_TYPES: Record<string, string> = {
|
|
13
|
+
png: "image/png",
|
|
14
|
+
jpg: "image/jpeg",
|
|
15
|
+
jpeg: "image/jpeg",
|
|
16
|
+
webp: "image/webp",
|
|
17
|
+
gif: "image/gif",
|
|
18
|
+
mp4: "video/mp4",
|
|
19
|
+
webm: "video/webm",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function registerMediaRoutes(api: Hono): void {
|
|
23
|
+
/** Generate an image using Imagen/Gemini */
|
|
24
|
+
api.post("/media/generate-image", async (c) => {
|
|
25
|
+
const body = await c.req.json().catch(() => ({}));
|
|
26
|
+
if (!body.prompt) {
|
|
27
|
+
return c.json({ error: "prompt is required" }, 400);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const results = await generateImage(body.prompt, {
|
|
31
|
+
model: body.model,
|
|
32
|
+
aspectRatio: body.aspectRatio,
|
|
33
|
+
numberOfImages: body.numberOfImages,
|
|
34
|
+
});
|
|
35
|
+
return c.json({ ok: true, images: results });
|
|
36
|
+
} catch (err: unknown) {
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
return c.json({ error: message }, 500);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Start video generation using Veo */
|
|
43
|
+
api.post("/media/generate-video", async (c) => {
|
|
44
|
+
const body = await c.req.json().catch(() => ({}));
|
|
45
|
+
if (!body.prompt) {
|
|
46
|
+
return c.json({ error: "prompt is required" }, 400);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const result = await generateVideo(body.prompt, {
|
|
50
|
+
model: body.model,
|
|
51
|
+
durationSeconds: body.durationSeconds,
|
|
52
|
+
aspectRatio: body.aspectRatio,
|
|
53
|
+
imageUri: body.imageUri,
|
|
54
|
+
});
|
|
55
|
+
return c.json({ ok: true, ...result });
|
|
56
|
+
} catch (err: unknown) {
|
|
57
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
+
return c.json({ error: message }, 500);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Poll a video generation operation */
|
|
63
|
+
api.get("/media/video-status/:operationName{.+}", async (c) => {
|
|
64
|
+
const operationName = c.req.param("operationName");
|
|
65
|
+
try {
|
|
66
|
+
const result = await pollVideoOperation(operationName);
|
|
67
|
+
return c.json(result);
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70
|
+
return c.json({ error: message }, 500);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Upload a media file (image/video) */
|
|
75
|
+
api.post("/media/upload", async (c) => {
|
|
76
|
+
try {
|
|
77
|
+
const contentType = c.req.header("content-type") || "";
|
|
78
|
+
let filename: string;
|
|
79
|
+
let data: Buffer;
|
|
80
|
+
|
|
81
|
+
if (contentType.includes("multipart/form-data")) {
|
|
82
|
+
const formData = await c.req.formData();
|
|
83
|
+
const file = formData.get("file");
|
|
84
|
+
if (!file || !(file instanceof File)) {
|
|
85
|
+
return c.json({ error: "Missing 'file' field" }, 400);
|
|
86
|
+
}
|
|
87
|
+
const ext = file.name.split(".").pop()?.toLowerCase() || "png";
|
|
88
|
+
filename = `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
|
89
|
+
data = Buffer.from(await file.arrayBuffer());
|
|
90
|
+
} else {
|
|
91
|
+
// Base64 JSON body: { base64, mimeType, filename? }
|
|
92
|
+
const body = await c.req.json().catch(() => ({}));
|
|
93
|
+
if (!body.base64) {
|
|
94
|
+
return c.json({ error: "base64 data is required" }, 400);
|
|
95
|
+
}
|
|
96
|
+
const mime = body.mimeType || "image/png";
|
|
97
|
+
const ext = mime.includes("png") ? "png" : mime.includes("jpeg") || mime.includes("jpg") ? "jpg" : mime.includes("webp") ? "webp" : mime.includes("mp4") ? "mp4" : "bin";
|
|
98
|
+
filename = body.filename || `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
|
99
|
+
data = Buffer.from(body.base64, "base64");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
mkdirSync(MEDIA_DIR, { recursive: true });
|
|
103
|
+
const filepath = join(MEDIA_DIR, filename);
|
|
104
|
+
writeFileSync(filepath, data);
|
|
105
|
+
|
|
106
|
+
return c.json({ ok: true, filename, url: `/api/media/file/${filename}` });
|
|
107
|
+
} catch (err: unknown) {
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
return c.json({ error: message }, 500);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/** List all generated media files */
|
|
114
|
+
api.get("/media", (c) => {
|
|
115
|
+
return c.json({ files: listMedia() });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** Serve a media file by filename */
|
|
119
|
+
api.get("/media/file/:filename", (c) => {
|
|
120
|
+
const filename = basename(c.req.param("filename"));
|
|
121
|
+
const filepath = join(MEDIA_DIR, filename);
|
|
122
|
+
if (!existsSync(filepath)) {
|
|
123
|
+
return c.json({ error: "Not found" }, 404);
|
|
124
|
+
}
|
|
125
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
126
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
127
|
+
const data = readFileSync(filepath);
|
|
128
|
+
return new Response(data, {
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": mime,
|
|
131
|
+
"Cache-Control": "public, max-age=86400",
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { GaugeDataProvider } from "../metrics-collector.js";
|
|
3
|
+
import { metricsCollector } from "../metrics-collector.js";
|
|
4
|
+
|
|
5
|
+
export function registerMetricsRoutes(
|
|
6
|
+
api: Hono,
|
|
7
|
+
deps: { gaugeProvider: GaugeDataProvider },
|
|
8
|
+
): void {
|
|
9
|
+
api.get("/metrics", (c) => {
|
|
10
|
+
const snapshot = metricsCollector.getSnapshot(deps.gaugeProvider);
|
|
11
|
+
return c.json(snapshot);
|
|
12
|
+
});
|
|
13
|
+
}
|