heyhank 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -10
- package/bin/cli.ts +7 -7
- package/bin/ctl.ts +42 -42
- package/dist/assets/{AgentsPage-BPhirnCe.js → AgentsPage-DqjDAcIw.js} +3 -3
- package/dist/assets/AssistantPage-C50CQFSB.js +2 -0
- package/dist/assets/BusinessPage-AY70tf1k.js +1 -0
- package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-Dt7LLuRr.js} +1 -1
- package/dist/assets/HelpPage-tlGx7fQF.js +1 -0
- package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-B4XOuHXu.js} +1 -1
- package/dist/assets/JarvisHUD-BDvuRd0I.js +120 -0
- package/dist/assets/MediaPage-CofV9Rd-.js +1 -0
- package/dist/assets/MemoryPage-Cj7FeqmJ.js +1 -0
- package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-B9kXAlH1.js} +1 -1
- package/dist/assets/{Playground-Fc5cdc5p.js → Playground-Cka-pRkP.js} +1 -1
- package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-BqhQgfYj.js} +1 -1
- package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-VveKc9uX.js} +2 -2
- package/dist/assets/RunsPage-DXVEk0AZ.js +1 -0
- package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-DACcwfDF.js} +1 -1
- package/dist/assets/SettingsPage-jfuQh8Tu.js +51 -0
- package/dist/assets/SkillsMarketplace-DrigiApe.js +1 -0
- package/dist/assets/SocialMediaPage-DOh3IPe8.js +10 -0
- package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DLhJWATT.js} +1 -1
- package/dist/assets/TelephonyPage-9C4C3_ot.js +9 -0
- package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-ChX-8Wu7.js} +1 -1
- package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
- package/dist/assets/index-C6Q5UQHD.js +229 -0
- package/dist/assets/index-ZxGXgiV3.css +32 -0
- package/dist/assets/sw-register-BBYuk-kw.js +1 -0
- package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
- package/dist/assets/workbox-window.prod.es5-BBnX5xw4.js +2 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist/{workbox-d2a0910a.js → workbox-080c8b91.js} +1 -1
- package/package.json +6 -1
- package/server/agent-executor.ts +102 -2
- package/server/agent-store.ts +3 -3
- package/server/agent-types.ts +11 -0
- package/server/assistant-store.ts +232 -6
- package/server/auth-manager.ts +9 -0
- package/server/cache-headers.ts +1 -1
- package/server/calendar-service.ts +10 -0
- package/server/ceo/document-store.ts +129 -0
- package/server/ceo/finance-store.ts +343 -0
- package/server/ceo/kpi-store.ts +208 -0
- package/server/ceo/memory-import.ts +277 -0
- package/server/ceo/news-store.ts +208 -0
- package/server/ceo/template-store.ts +134 -0
- package/server/ceo/time-tracking-store.ts +227 -0
- package/server/claude-auth-monitor.ts +128 -0
- package/server/claude-code-worker.ts +86 -0
- package/server/claude-session-discovery.ts +74 -1
- package/server/cli-launcher.ts +32 -10
- package/server/codex-adapter.ts +2 -2
- package/server/codex-ws-proxy.cjs +1 -1
- package/server/container-manager.ts +4 -4
- package/server/content-intelligence/content-engine.ts +1112 -0
- package/server/content-intelligence/platform-knowledge.ts +870 -0
- package/server/cron-store.ts +3 -3
- package/server/embedding-service.ts +49 -0
- package/server/event-bus-types.ts +13 -0
- package/server/execution-store.ts +54 -1
- package/server/federation/node-store.ts +5 -4
- package/server/fs-utils.ts +28 -1
- package/server/hank-notifications-store.ts +91 -0
- package/server/hank-tool-executor.ts +1835 -0
- package/server/hank-tools.ts +2107 -0
- package/server/image-pull-manager.ts +2 -2
- package/server/index.ts +25 -2
- package/server/llm-providers-streaming.ts +541 -0
- package/server/llm-providers.ts +12 -0
- package/server/marketplace.ts +249 -0
- package/server/mcp-registry.ts +158 -0
- package/server/memory-service.ts +296 -0
- package/server/obsidian-sync.ts +184 -0
- package/server/provider-manager.ts +5 -2
- package/server/provider-registry.ts +12 -0
- package/server/reminder-scheduler.ts +37 -1
- package/server/routes/agent-routes.ts +44 -1
- package/server/routes/assistant-routes.ts +198 -5
- package/server/routes/ceo-finance-kpi-routes.ts +167 -0
- package/server/routes/ceo-news-time-routes.ts +137 -0
- package/server/routes/ceo-routes.ts +99 -0
- package/server/routes/content-routes.ts +116 -0
- package/server/routes/email-routes.ts +147 -0
- package/server/routes/env-routes.ts +3 -3
- package/server/routes/fs-routes.ts +12 -9
- package/server/routes/hank-chat-routes.ts +592 -0
- package/server/routes/llm-routes.ts +12 -0
- package/server/routes/marketplace-routes.ts +63 -0
- package/server/routes/media-routes.ts +1 -1
- package/server/routes/memory-routes.ts +127 -0
- package/server/routes/platform-routes.ts +14 -675
- package/server/routes/sandbox-routes.ts +1 -1
- package/server/routes/settings-routes.ts +51 -1
- package/server/routes/socialmedia-routes.ts +152 -2
- package/server/routes/system-routes.ts +2 -2
- package/server/routes/team-routes.ts +71 -0
- package/server/routes/telephony-routes.ts +98 -18
- package/server/routes.ts +36 -9
- package/server/session-creation-service.ts +2 -2
- package/server/session-orchestrator.ts +54 -2
- package/server/session-types.ts +2 -0
- package/server/settings-manager.ts +50 -2
- package/server/skill-discovery.ts +68 -0
- package/server/socialmedia/adapters/browser-adapter.ts +179 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +291 -14
- package/server/socialmedia/manager.ts +234 -15
- package/server/socialmedia/store.ts +51 -1
- package/server/socialmedia/types.ts +35 -2
- package/server/socialview/browser-manager.ts +150 -0
- package/server/socialview/extractors.ts +1298 -0
- package/server/socialview/image-describe.ts +188 -0
- package/server/socialview/library.ts +119 -0
- package/server/socialview/poster.ts +276 -0
- package/server/socialview/routes.ts +371 -0
- package/server/socialview/style-analyzer.ts +187 -0
- package/server/socialview/style-profiles.ts +67 -0
- package/server/socialview/types.ts +166 -0
- package/server/socialview/vision.ts +127 -0
- package/server/socialview/vnc-manager.ts +110 -0
- package/server/style-injector.ts +135 -0
- package/server/team-service.ts +239 -0
- package/server/team-store.ts +75 -0
- package/server/team-types.ts +52 -0
- package/server/telephony/audio-bridge.ts +281 -35
- package/server/telephony/audio-recorder.ts +132 -0
- package/server/telephony/call-manager.ts +803 -104
- package/server/telephony/call-types.ts +67 -1
- package/server/telephony/esl-client.ts +319 -0
- package/server/telephony/freeswitch-sync.ts +155 -0
- package/server/telephony/phone-utils.ts +63 -0
- package/server/telephony/telephony-store.ts +9 -8
- package/server/url-validator.ts +82 -0
- package/server/vault-markdown.ts +317 -0
- package/server/vault-migration.ts +121 -0
- package/server/vault-store.ts +466 -0
- package/server/vault-watcher.ts +59 -0
- package/server/vector-store.ts +210 -0
- package/server/voice-pipeline/gemini-live-adapter.ts +97 -0
- package/server/voice-pipeline/greeting-cache.ts +200 -0
- package/server/voice-pipeline/manager.ts +249 -0
- package/server/voice-pipeline/pipeline.ts +335 -0
- package/server/voice-pipeline/providers/index.ts +47 -0
- package/server/voice-pipeline/providers/llm-internal.ts +527 -0
- package/server/voice-pipeline/providers/stt-google.ts +157 -0
- package/server/voice-pipeline/providers/tts-google.ts +126 -0
- package/server/voice-pipeline/types.ts +247 -0
- package/server/ws-bridge-types.ts +6 -1
- package/dist/assets/AssistantPage-DJ-cMQfb.js +0 -1
- package/dist/assets/HelpPage-DMfkzERp.js +0 -1
- package/dist/assets/MediaPage-CE5rdvkC.js +0 -1
- package/dist/assets/RunsPage-C5BZF5Rx.js +0 -1
- package/dist/assets/SettingsPage-DirhjQrJ.js +0 -51
- package/dist/assets/SocialMediaPage-DBuM28vD.js +0 -1
- package/dist/assets/TelephonyPage-x0VV0fOo.js +0 -1
- package/dist/assets/index-C8M_PUmX.css +0 -32
- package/dist/assets/index-CEqZnThB.js +0 -204
- package/dist/assets/sw-register-LSSpj6RU.js +0 -1
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +0 -2
- package/server/socialmedia/adapters/ayrshare-adapter.ts +0 -169
|
@@ -81,7 +81,7 @@ export function registerSandboxRoutes(
|
|
|
81
81
|
|
|
82
82
|
if (!containerManager.checkDocker()) return c.json({ error: "Docker is not available" }, 503);
|
|
83
83
|
|
|
84
|
-
const effectiveImage = "
|
|
84
|
+
const effectiveImage = "heyhank:latest";
|
|
85
85
|
if (!imagePullManager.isReady(effectiveImage)) {
|
|
86
86
|
return c.json({ error: `Docker image ${effectiveImage} is not available. Pull it first.` }, 503);
|
|
87
87
|
}
|
|
@@ -87,6 +87,12 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
87
87
|
publicUrl: settings.publicUrl,
|
|
88
88
|
updateChannel: settings.updateChannel,
|
|
89
89
|
dockerAutoUpdate: settings.dockerAutoUpdate,
|
|
90
|
+
hankChatProvider: settings.hankChatProvider,
|
|
91
|
+
hankChatModel: settings.hankChatModel,
|
|
92
|
+
hankChatAvatarEnabled: settings.hankChatAvatarEnabled,
|
|
93
|
+
hankChatAvatarUrl: settings.hankChatAvatarUrl,
|
|
94
|
+
obsidianVaultPath: settings.obsidianVaultPath,
|
|
95
|
+
openrouterApiKeyConfigured: !!process.env.OPENROUTER_API_KEY?.trim(),
|
|
90
96
|
});
|
|
91
97
|
});
|
|
92
98
|
|
|
@@ -140,6 +146,15 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
140
146
|
if (body.dockerAutoUpdate !== undefined && typeof body.dockerAutoUpdate !== "boolean") {
|
|
141
147
|
return c.json({ error: "dockerAutoUpdate must be a boolean" }, 400);
|
|
142
148
|
}
|
|
149
|
+
if (body.obsidianVaultPath !== undefined && typeof body.obsidianVaultPath !== "string") {
|
|
150
|
+
return c.json({ error: "obsidianVaultPath must be a string" }, 400);
|
|
151
|
+
}
|
|
152
|
+
if (body.hankChatAvatarEnabled !== undefined && typeof body.hankChatAvatarEnabled !== "boolean") {
|
|
153
|
+
return c.json({ error: "hankChatAvatarEnabled must be a boolean" }, 400);
|
|
154
|
+
}
|
|
155
|
+
if (body.hankChatAvatarUrl !== undefined && typeof body.hankChatAvatarUrl !== "string") {
|
|
156
|
+
return c.json({ error: "hankChatAvatarUrl must be a string" }, 400);
|
|
157
|
+
}
|
|
143
158
|
const hasAnyField = body.anthropicApiKey !== undefined || body.anthropicModel !== undefined
|
|
144
159
|
|| body.claudeCodeOAuthToken !== undefined || body.openaiApiKey !== undefined
|
|
145
160
|
|| body.onboardingCompleted !== undefined
|
|
@@ -150,7 +165,12 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
150
165
|
|| body.aiValidationAutoDeny !== undefined
|
|
151
166
|
|| body.publicUrl !== undefined
|
|
152
167
|
|| body.updateChannel !== undefined
|
|
153
|
-
|| body.dockerAutoUpdate !== undefined
|
|
168
|
+
|| body.dockerAutoUpdate !== undefined
|
|
169
|
+
|| body.hankChatProvider !== undefined
|
|
170
|
+
|| body.hankChatModel !== undefined
|
|
171
|
+
|| body.hankChatAvatarEnabled !== undefined
|
|
172
|
+
|| body.hankChatAvatarUrl !== undefined
|
|
173
|
+
|| body.obsidianVaultPath !== undefined;
|
|
154
174
|
if (!hasAnyField) {
|
|
155
175
|
return c.json({ error: "At least one settings field is required" }, 400);
|
|
156
176
|
}
|
|
@@ -224,8 +244,32 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
224
244
|
typeof body.dockerAutoUpdate === "boolean"
|
|
225
245
|
? body.dockerAutoUpdate
|
|
226
246
|
: undefined,
|
|
247
|
+
hankChatProvider:
|
|
248
|
+
typeof body.hankChatProvider === "string"
|
|
249
|
+
? body.hankChatProvider.trim()
|
|
250
|
+
: undefined,
|
|
251
|
+
hankChatModel:
|
|
252
|
+
typeof body.hankChatModel === "string"
|
|
253
|
+
? body.hankChatModel.trim()
|
|
254
|
+
: undefined,
|
|
255
|
+
hankChatAvatarEnabled:
|
|
256
|
+
typeof body.hankChatAvatarEnabled === "boolean"
|
|
257
|
+
? body.hankChatAvatarEnabled
|
|
258
|
+
: undefined,
|
|
259
|
+
hankChatAvatarUrl:
|
|
260
|
+
typeof body.hankChatAvatarUrl === "string"
|
|
261
|
+
? body.hankChatAvatarUrl.trim()
|
|
262
|
+
: undefined,
|
|
263
|
+
obsidianVaultPath:
|
|
264
|
+
typeof body.obsidianVaultPath === "string"
|
|
265
|
+
? body.obsidianVaultPath.trim()
|
|
266
|
+
: undefined,
|
|
227
267
|
});
|
|
228
268
|
|
|
269
|
+
if (body.obsidianVaultPath !== undefined) {
|
|
270
|
+
import("../memory-service.js").then(m => m.restartVaultSync()).catch(() => {});
|
|
271
|
+
}
|
|
272
|
+
|
|
229
273
|
const claudeAuthAfter = detectClaudeAuthStatus();
|
|
230
274
|
const codexAuthAfter = detectCodexAuthStatus();
|
|
231
275
|
return c.json({
|
|
@@ -249,6 +293,12 @@ export function registerSettingsRoutes(api: Hono): void {
|
|
|
249
293
|
publicUrl: settings.publicUrl,
|
|
250
294
|
updateChannel: settings.updateChannel,
|
|
251
295
|
dockerAutoUpdate: settings.dockerAutoUpdate,
|
|
296
|
+
hankChatProvider: settings.hankChatProvider,
|
|
297
|
+
hankChatModel: settings.hankChatModel,
|
|
298
|
+
hankChatAvatarEnabled: settings.hankChatAvatarEnabled,
|
|
299
|
+
hankChatAvatarUrl: settings.hankChatAvatarUrl,
|
|
300
|
+
obsidianVaultPath: settings.obsidianVaultPath,
|
|
301
|
+
openrouterApiKeyConfigured: !!process.env.OPENROUTER_API_KEY?.trim(),
|
|
252
302
|
});
|
|
253
303
|
});
|
|
254
304
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import type { Hono } from "hono";
|
|
5
5
|
import * as store from "../socialmedia/store.js";
|
|
6
6
|
import * as manager from "../socialmedia/manager.js";
|
|
7
|
+
import * as browser from "../socialview/browser-manager.js";
|
|
8
|
+
import type { SocialPlatform as ViewPlatform } from "../socialview/types.js";
|
|
7
9
|
|
|
8
10
|
export function registerSocialMediaRoutes(api: Hono): void {
|
|
9
11
|
// ─── Settings ───────────────────────────────────────────────────────
|
|
@@ -56,6 +58,26 @@ export function registerSocialMediaRoutes(api: Hono): void {
|
|
|
56
58
|
}
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Browser-backed status for the platforms we post via Playwright
|
|
63
|
+
* (X / Twitter and TikTok). Other platforms continue to use the
|
|
64
|
+
* primary backend (Postiz).
|
|
65
|
+
*/
|
|
66
|
+
api.get("/socialmedia/browser-status", (c) => {
|
|
67
|
+
const platforms: ViewPlatform[] = ["twitter", "tiktok"];
|
|
68
|
+
const statuses = platforms.map((p) => {
|
|
69
|
+
const s = browser.getStatus(p);
|
|
70
|
+
return {
|
|
71
|
+
platform: p,
|
|
72
|
+
running: s.running,
|
|
73
|
+
loggedIn: s.loggedIn,
|
|
74
|
+
currentUrl: s.currentUrl,
|
|
75
|
+
hasProfile: browser.hasProfile(p),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
return c.json({ platforms: statuses });
|
|
79
|
+
});
|
|
80
|
+
|
|
59
81
|
/** Get connected profiles */
|
|
60
82
|
api.get("/socialmedia/profiles", async (c) => {
|
|
61
83
|
try {
|
|
@@ -74,12 +96,23 @@ export function registerSocialMediaRoutes(api: Hono): void {
|
|
|
74
96
|
const body = await c.req.json();
|
|
75
97
|
const text = (body.text || "").trim();
|
|
76
98
|
if (!text) return c.json({ error: "text required" }, 400);
|
|
99
|
+
// Accept both mediaUrls (array) and imageUrl (string) for compatibility
|
|
100
|
+
let mediaUrls: string[] = body.mediaUrls || [];
|
|
101
|
+
if (mediaUrls.length === 0 && body.imageUrl) {
|
|
102
|
+
mediaUrls = [body.imageUrl];
|
|
103
|
+
}
|
|
77
104
|
const result = await manager.createPost({
|
|
78
105
|
text,
|
|
79
106
|
platforms: body.platforms || [],
|
|
80
107
|
scheduledAt: body.scheduledAt || null,
|
|
81
|
-
mediaUrls
|
|
108
|
+
mediaUrls,
|
|
109
|
+
isDraft: body.isDraft ?? false,
|
|
110
|
+
title: body.title,
|
|
111
|
+
firstComment: body.firstComment,
|
|
112
|
+
videoUrl: body.videoUrl,
|
|
113
|
+
thumbnailUrl: body.thumbnailUrl,
|
|
82
114
|
});
|
|
115
|
+
if (body.createdBy && result) (result as any).createdBy = body.createdBy;
|
|
83
116
|
return c.json(result, 201);
|
|
84
117
|
} catch (e) {
|
|
85
118
|
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
@@ -115,7 +148,22 @@ export function registerSocialMediaRoutes(api: Hono): void {
|
|
|
115
148
|
if (!post) return c.json({ error: "post not found" }, 404);
|
|
116
149
|
if (body.text !== undefined) post.text = body.text;
|
|
117
150
|
if (body.scheduledAt !== undefined) post.scheduledAt = body.scheduledAt;
|
|
118
|
-
if (body.platforms !== undefined)
|
|
151
|
+
if (body.platforms !== undefined) {
|
|
152
|
+
// Coerce object-array shapes (`[{id, name}]`) back to string[].
|
|
153
|
+
if (Array.isArray(body.platforms)) {
|
|
154
|
+
post.platforms = body.platforms
|
|
155
|
+
.map((p: unknown) => {
|
|
156
|
+
if (typeof p === "string") return p;
|
|
157
|
+
if (p && typeof p === "object") {
|
|
158
|
+
const o = p as { name?: unknown; platform?: unknown };
|
|
159
|
+
if (typeof o.name === "string") return o.name;
|
|
160
|
+
if (typeof o.platform === "string") return o.platform;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
})
|
|
164
|
+
.filter((s: unknown): s is string => typeof s === "string" && s.length > 0);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
119
167
|
if (body.scheduledAt && post.status === "published") post.status = "scheduled";
|
|
120
168
|
store.savePost(post);
|
|
121
169
|
return c.json(post);
|
|
@@ -135,12 +183,42 @@ export function registerSocialMediaRoutes(api: Hono): void {
|
|
|
135
183
|
}
|
|
136
184
|
});
|
|
137
185
|
|
|
186
|
+
/** Move a published/scheduled/failed post back to draft (deletes from backend) */
|
|
187
|
+
api.post("/socialmedia/posts/:id/move-to-draft", async (c) => {
|
|
188
|
+
try {
|
|
189
|
+
const post = await manager.moveToDraft(c.req.param("id"));
|
|
190
|
+
return c.json(post);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed" }, 400);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
138
196
|
/** Delete post */
|
|
139
197
|
api.delete("/socialmedia/posts/:id", async (c) => {
|
|
140
198
|
const ok = await manager.deletePost(c.req.param("id"));
|
|
141
199
|
return c.json({ ok }, ok ? 200 : 404);
|
|
142
200
|
});
|
|
143
201
|
|
|
202
|
+
/** Archive post (hide from default queue view) */
|
|
203
|
+
api.post("/socialmedia/posts/:id/archive", async (c) => {
|
|
204
|
+
try {
|
|
205
|
+
const post = await manager.setArchived(c.req.param("id"), true);
|
|
206
|
+
return c.json(post);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 400);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/** Unarchive post (restore previous status) */
|
|
213
|
+
api.post("/socialmedia/posts/:id/unarchive", async (c) => {
|
|
214
|
+
try {
|
|
215
|
+
const post = await manager.setArchived(c.req.param("id"), false);
|
|
216
|
+
return c.json(post);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 400);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
144
222
|
/** Get post analytics */
|
|
145
223
|
api.get("/socialmedia/posts/:id/analytics", async (c) => {
|
|
146
224
|
try {
|
|
@@ -194,6 +272,78 @@ export function registerSocialMediaRoutes(api: Hono): void {
|
|
|
194
272
|
}
|
|
195
273
|
});
|
|
196
274
|
|
|
275
|
+
// ─── Hashtag Pools ─────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
/** List all hashtag pools */
|
|
278
|
+
api.get("/socialmedia/hashtag-pools", (c) => {
|
|
279
|
+
try {
|
|
280
|
+
const pools = store.listHashtagPools();
|
|
281
|
+
return c.json({ pools });
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/** Get single hashtag pool */
|
|
288
|
+
api.get("/socialmedia/hashtag-pools/:id", (c) => {
|
|
289
|
+
const pool = store.getHashtagPool(c.req.param("id"));
|
|
290
|
+
if (!pool) return c.json({ error: "pool not found" }, 404);
|
|
291
|
+
return c.json(pool);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/** Create or update hashtag pool */
|
|
295
|
+
api.post("/socialmedia/hashtag-pools", async (c) => {
|
|
296
|
+
try {
|
|
297
|
+
const body = await c.req.json();
|
|
298
|
+
if (!body.name) return c.json({ error: "name required" }, 400);
|
|
299
|
+
const { randomUUID } = await import("node:crypto");
|
|
300
|
+
const now = new Date().toISOString();
|
|
301
|
+
const pool = {
|
|
302
|
+
id: body.id || randomUUID(),
|
|
303
|
+
name: body.name,
|
|
304
|
+
industry: body.industry || "",
|
|
305
|
+
language: body.language || "de",
|
|
306
|
+
popular: body.popular || [],
|
|
307
|
+
medium: body.medium || [],
|
|
308
|
+
niche: body.niche || [],
|
|
309
|
+
branded: body.branded || [],
|
|
310
|
+
blocked: body.blocked || [],
|
|
311
|
+
createdAt: body.createdAt || now,
|
|
312
|
+
updatedAt: now,
|
|
313
|
+
};
|
|
314
|
+
store.saveHashtagPool(pool);
|
|
315
|
+
return c.json(pool, 201);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
return c.json({ error: e instanceof Error ? e.message : "bad request" }, 400);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
/** Update hashtag pool */
|
|
322
|
+
api.put("/socialmedia/hashtag-pools/:id", async (c) => {
|
|
323
|
+
try {
|
|
324
|
+
const id = c.req.param("id");
|
|
325
|
+
const existing = store.getHashtagPool(id);
|
|
326
|
+
if (!existing) return c.json({ error: "pool not found" }, 404);
|
|
327
|
+
const body = await c.req.json();
|
|
328
|
+
const pool = {
|
|
329
|
+
...existing,
|
|
330
|
+
...body,
|
|
331
|
+
id, // don't allow id change
|
|
332
|
+
updatedAt: new Date().toISOString(),
|
|
333
|
+
};
|
|
334
|
+
store.saveHashtagPool(pool);
|
|
335
|
+
return c.json(pool);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
return c.json({ error: e instanceof Error ? e.message : "bad request" }, 400);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
/** Delete hashtag pool */
|
|
342
|
+
api.delete("/socialmedia/hashtag-pools/:id", (c) => {
|
|
343
|
+
const ok = store.deleteHashtagPool(c.req.param("id"));
|
|
344
|
+
return c.json({ ok }, ok ? 200 : 404);
|
|
345
|
+
});
|
|
346
|
+
|
|
197
347
|
// ─── Account Analytics ──────────────────────────────────────────────
|
|
198
348
|
|
|
199
349
|
/** Get account analytics for a profile */
|
|
@@ -133,8 +133,8 @@ export function registerSystemRoutes(
|
|
|
133
133
|
if (getSettings().dockerAutoUpdate) {
|
|
134
134
|
try {
|
|
135
135
|
console.log("[update] Re-pulling Docker image (dockerAutoUpdate enabled)...");
|
|
136
|
-
imagePullManager.pull("
|
|
137
|
-
const ready = await imagePullManager.waitForReady("
|
|
136
|
+
imagePullManager.pull("heyhank:latest");
|
|
137
|
+
const ready = await imagePullManager.waitForReady("heyhank:latest", 120_000);
|
|
138
138
|
if (ready) {
|
|
139
139
|
console.log("[update] Docker image re-pull complete.");
|
|
140
140
|
} else {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import * as teamService from "../team-service.js";
|
|
3
|
+
|
|
4
|
+
export function registerTeamRoutes(api: Hono): void {
|
|
5
|
+
api.get("/teams", (c) => {
|
|
6
|
+
return c.json(teamService.listTeams());
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
api.get("/teams/:id", (c) => {
|
|
10
|
+
const team = teamService.getTeam(c.req.param("id"));
|
|
11
|
+
if (!team) return c.json({ error: "Team not found" }, 404);
|
|
12
|
+
return c.json(team);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
api.get("/teams/:id/status", (c) => {
|
|
16
|
+
const status = teamService.getTeamStatus(c.req.param("id"));
|
|
17
|
+
if (!status) return c.json({ error: "Team not found" }, 404);
|
|
18
|
+
return c.json(status);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
api.post("/teams", async (c) => {
|
|
22
|
+
const body = await c.req.json();
|
|
23
|
+
const team = teamService.createTeam({
|
|
24
|
+
goal: body.goal,
|
|
25
|
+
repoRoot: body.repoRoot || body.cwd,
|
|
26
|
+
baseBranch: body.baseBranch,
|
|
27
|
+
suggestedAgents: body.suggestedAgents,
|
|
28
|
+
});
|
|
29
|
+
return c.json(team, 201);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
api.put("/teams/:id", async (c) => {
|
|
33
|
+
const body = await c.req.json();
|
|
34
|
+
const team = teamService.updateTeamState(c.req.param("id"), body.state, body);
|
|
35
|
+
if (!team) return c.json({ error: "Team not found" }, 404);
|
|
36
|
+
return c.json(team);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
api.put("/teams/:id/tasks/:taskId", async (c) => {
|
|
40
|
+
const body = await c.req.json();
|
|
41
|
+
const team = teamService.updateTask(c.req.param("id"), c.req.param("taskId"), body);
|
|
42
|
+
if (!team) return c.json({ error: "Team or task not found" }, 404);
|
|
43
|
+
return c.json(team);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
api.post("/teams/:id/complete", async (c) => {
|
|
47
|
+
const body = await c.req.json();
|
|
48
|
+
const team = teamService.updateTeamState(c.req.param("id"), "completed", {
|
|
49
|
+
result: body.result,
|
|
50
|
+
completedAt: Date.now(),
|
|
51
|
+
});
|
|
52
|
+
if (!team) return c.json({ error: "Team not found" }, 404);
|
|
53
|
+
return c.json(team);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
api.post("/teams/:id/fail", async (c) => {
|
|
57
|
+
const body = await c.req.json();
|
|
58
|
+
const team = teamService.updateTeamState(c.req.param("id"), "failed", {
|
|
59
|
+
error: body.error,
|
|
60
|
+
completedAt: Date.now(),
|
|
61
|
+
});
|
|
62
|
+
if (!team) return c.json({ error: "Team not found" }, 404);
|
|
63
|
+
return c.json(team);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
api.delete("/teams/:id", (c) => {
|
|
67
|
+
const success = teamService.deleteTeam(c.req.param("id"));
|
|
68
|
+
if (!success) return c.json({ error: "Team not found" }, 404);
|
|
69
|
+
return c.json({ ok: true });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -5,7 +5,33 @@ import type { Hono } from "hono";
|
|
|
5
5
|
import { callManager } from "../telephony/call-manager.js";
|
|
6
6
|
import * as store from "../telephony/telephony-store.js";
|
|
7
7
|
import type { CallConfig, SipTrunkConfig, TelephonyContact } from "../telephony/call-types.js";
|
|
8
|
+
import { eslCommand, eslStatus } from "../telephony/esl-client.js";
|
|
9
|
+
import { syncAndReload, checkGatewayStatus } from "../telephony/freeswitch-sync.js";
|
|
8
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { existsSync, statSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { getPipelineSettingsFrom } from "../voice-pipeline/manager.js";
|
|
15
|
+
import { preRenderAllGreetings, clearCache } from "../voice-pipeline/greeting-cache.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Background-warm the greeting cache so the first call has 0ms TTS latency.
|
|
19
|
+
* Fire-and-forget — failures are logged but don't block the calling endpoint.
|
|
20
|
+
*/
|
|
21
|
+
function warmGreetingsAsync(opts: { clearFirst?: boolean } = {}): void {
|
|
22
|
+
setImmediate(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const settings = store.getSettings();
|
|
25
|
+
const pipelineSettings = getPipelineSettingsFrom(settings);
|
|
26
|
+
if (!pipelineSettings.enabled || !pipelineSettings.preRenderGreetings) return;
|
|
27
|
+
if (opts.clearFirst) clearCache();
|
|
28
|
+
const stats = await preRenderAllGreetings(settings.contacts, pipelineSettings);
|
|
29
|
+
console.log(`[telephony] Greeting cache warmed: ${stats.rendered} rendered, ${stats.cached} cached, ${stats.errors} errors`);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error("[telephony] Greeting warm failed:", e);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
9
35
|
|
|
10
36
|
export function registerTelephonyRoutes(api: Hono): void {
|
|
11
37
|
// ─── Calls ───────────────────────────────────────────────────────────
|
|
@@ -72,6 +98,29 @@ export function registerTelephonyRoutes(api: Hono): void {
|
|
|
72
98
|
}
|
|
73
99
|
});
|
|
74
100
|
|
|
101
|
+
/** Serve call audio recording (WAV) */
|
|
102
|
+
api.get("/telephony/calls/:id/audio", (c) => {
|
|
103
|
+
const id = c.req.param("id");
|
|
104
|
+
// Validate UUID format to prevent path traversal
|
|
105
|
+
if (!/^[a-f0-9-]{36}$/.test(id)) {
|
|
106
|
+
return c.json({ error: "Invalid call ID" }, 400);
|
|
107
|
+
}
|
|
108
|
+
const wavPath = join(homedir(), ".heyhank", "telephony", "calls", `${id}.wav`);
|
|
109
|
+
if (!existsSync(wavPath)) {
|
|
110
|
+
return c.json({ error: "Audio recording not found" }, 404);
|
|
111
|
+
}
|
|
112
|
+
const file = Bun.file(wavPath);
|
|
113
|
+
const stat = statSync(wavPath);
|
|
114
|
+
return new Response(file, {
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": "audio/wav",
|
|
117
|
+
"Content-Length": String(stat.size),
|
|
118
|
+
"Content-Disposition": `inline; filename="${id}.wav"`,
|
|
119
|
+
"Cache-Control": "private, max-age=86400",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
75
124
|
/** Call history */
|
|
76
125
|
api.get("/telephony/history", (c) => {
|
|
77
126
|
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
@@ -122,6 +171,21 @@ export function registerTelephonyRoutes(api: Hono): void {
|
|
|
122
171
|
}
|
|
123
172
|
|
|
124
173
|
store.saveSettings(updated);
|
|
174
|
+
|
|
175
|
+
// If voice settings changed, invalidate greeting cache and re-warm
|
|
176
|
+
const oldVoice = getPipelineSettingsFrom(current).tts.voice;
|
|
177
|
+
const newVoice = getPipelineSettingsFrom(updated).tts.voice;
|
|
178
|
+
const voiceChanged = oldVoice !== newVoice;
|
|
179
|
+
warmGreetingsAsync({ clearFirst: voiceChanged });
|
|
180
|
+
|
|
181
|
+
// Re-sync inbound ESL listener with new settings (start if enabled, stop otherwise)
|
|
182
|
+
try {
|
|
183
|
+
callManager.stopInboundListener();
|
|
184
|
+
callManager.startInboundListener();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error("[telephony-routes] Failed to restart inbound listener:", err);
|
|
187
|
+
}
|
|
188
|
+
|
|
125
189
|
return c.json({ success: true });
|
|
126
190
|
} catch (err) {
|
|
127
191
|
return c.json({ error: err instanceof Error ? err.message : "Failed to save settings" }, 500);
|
|
@@ -148,6 +212,9 @@ export function registerTelephonyRoutes(api: Hono): void {
|
|
|
148
212
|
if (!settings.defaultTrunkId) settings.defaultTrunkId = trunk.id;
|
|
149
213
|
store.saveSettings(settings);
|
|
150
214
|
|
|
215
|
+
// Auto-sync gateway to FreeSWITCH
|
|
216
|
+
syncAndReload().catch((err) => console.error("[telephony] Auto-sync failed:", err));
|
|
217
|
+
|
|
151
218
|
return c.json(trunk);
|
|
152
219
|
} catch (err) {
|
|
153
220
|
return c.json({ error: err instanceof Error ? err.message : "Failed to add trunk" }, 500);
|
|
@@ -163,6 +230,10 @@ export function registerTelephonyRoutes(api: Hono): void {
|
|
|
163
230
|
settings.defaultTrunkId = settings.trunks[0]?.id || null;
|
|
164
231
|
}
|
|
165
232
|
store.saveSettings(settings);
|
|
233
|
+
|
|
234
|
+
// Auto-sync after trunk removal
|
|
235
|
+
syncAndReload().catch((err) => console.error("[telephony] Auto-sync failed:", err));
|
|
236
|
+
|
|
166
237
|
return c.json({ success: true });
|
|
167
238
|
});
|
|
168
239
|
|
|
@@ -192,6 +263,7 @@ export function registerTelephonyRoutes(api: Hono): void {
|
|
|
192
263
|
notes: body.notes?.trim() || undefined,
|
|
193
264
|
};
|
|
194
265
|
store.addContact(contact);
|
|
266
|
+
warmGreetingsAsync();
|
|
195
267
|
return c.json(contact);
|
|
196
268
|
} catch (err) {
|
|
197
269
|
return c.json({ error: err instanceof Error ? err.message : "Failed to add contact" }, 500);
|
|
@@ -213,6 +285,8 @@ export function registerTelephonyRoutes(api: Hono): void {
|
|
|
213
285
|
}
|
|
214
286
|
const updated = store.updateContact(id, body);
|
|
215
287
|
if (!updated) return c.json({ error: "Contact not found" }, 404);
|
|
288
|
+
// Re-render greeting if name/phone changed
|
|
289
|
+
if (body.name || body.phone) warmGreetingsAsync();
|
|
216
290
|
return c.json(updated);
|
|
217
291
|
} catch (err) {
|
|
218
292
|
return c.json({ error: err instanceof Error ? err.message : "Failed to update contact" }, 500);
|
|
@@ -227,28 +301,13 @@ export function registerTelephonyRoutes(api: Hono): void {
|
|
|
227
301
|
return c.json({ success: true });
|
|
228
302
|
});
|
|
229
303
|
|
|
230
|
-
/** Test FreeSWITCH ESL connection */
|
|
304
|
+
/** Test FreeSWITCH ESL connection (TCP) */
|
|
231
305
|
api.post("/telephony/test-connection", async (c) => {
|
|
232
306
|
const settings = store.getSettings();
|
|
233
|
-
const { eslHost, eslPort, eslPassword } = settings.freeswitch;
|
|
234
307
|
|
|
235
308
|
try {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
method: "POST",
|
|
239
|
-
headers: {
|
|
240
|
-
"Content-Type": "text/plain",
|
|
241
|
-
"Authorization": `Basic ${btoa(`freeswitch:${eslPassword}`)}`,
|
|
242
|
-
},
|
|
243
|
-
body: "status",
|
|
244
|
-
signal: AbortSignal.timeout(5000),
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
if (res.ok) {
|
|
248
|
-
const text = await res.text();
|
|
249
|
-
return c.json({ connected: true, status: text.trim().slice(0, 200) });
|
|
250
|
-
}
|
|
251
|
-
return c.json({ connected: false, error: `HTTP ${res.status}` });
|
|
309
|
+
const result = await eslStatus(settings.freeswitch);
|
|
310
|
+
return c.json({ connected: result.connected, status: result.status });
|
|
252
311
|
} catch (err) {
|
|
253
312
|
return c.json({
|
|
254
313
|
connected: false,
|
|
@@ -256,4 +315,25 @@ export function registerTelephonyRoutes(api: Hono): void {
|
|
|
256
315
|
});
|
|
257
316
|
}
|
|
258
317
|
});
|
|
318
|
+
|
|
319
|
+
/** Sync gateway configs to FreeSWITCH and reload */
|
|
320
|
+
api.post("/telephony/sync", async (c) => {
|
|
321
|
+
try {
|
|
322
|
+
await syncAndReload();
|
|
323
|
+
return c.json({ success: true, message: "Gateway configs synced and reloaded." });
|
|
324
|
+
} catch (err) {
|
|
325
|
+
return c.json({ error: err instanceof Error ? err.message : "Sync failed" }, 500);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
/** Check gateway registration status */
|
|
330
|
+
api.get("/telephony/trunks/:id/status", async (c) => {
|
|
331
|
+
const id = c.req.param("id");
|
|
332
|
+
try {
|
|
333
|
+
const status = await checkGatewayStatus(id);
|
|
334
|
+
return c.json(status);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return c.json({ error: err instanceof Error ? err.message : "Status check failed" }, 500);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
259
339
|
}
|
package/server/routes.ts
CHANGED
|
@@ -16,6 +16,7 @@ import * as sessionNames from "./session-names.js";
|
|
|
16
16
|
import { containerManager } from "./container-manager.js";
|
|
17
17
|
import { registerFsRoutes } from "./routes/fs-routes.js";
|
|
18
18
|
import { registerSkillRoutes } from "./routes/skills-routes.js";
|
|
19
|
+
import { registerMarketplaceRoutes } from "./routes/marketplace-routes.js";
|
|
19
20
|
import { registerEnvRoutes } from "./routes/env-routes.js";
|
|
20
21
|
import { registerSandboxRoutes } from "./routes/sandbox-routes.js";
|
|
21
22
|
import { registerCronRoutes } from "./routes/cron-routes.js";
|
|
@@ -34,8 +35,17 @@ import { registerHubRoutes } from "./recording-hub/hub-routes.js";
|
|
|
34
35
|
import { registerFederationRoutes } from "./routes/federation-routes.js";
|
|
35
36
|
import { registerTelephonyRoutes } from "./routes/telephony-routes.js";
|
|
36
37
|
import { registerSocialMediaRoutes } from "./routes/socialmedia-routes.js";
|
|
38
|
+
import { registerSocialViewRoutes } from "./socialview/routes.js";
|
|
37
39
|
import { registerAssistantRoutes } from "./routes/assistant-routes.js";
|
|
40
|
+
import { registerEmailRoutes } from "./routes/email-routes.js";
|
|
38
41
|
import { registerProviderRoutes } from "./routes/provider-routes.js";
|
|
42
|
+
import { registerHankChatRoutes } from "./routes/hank-chat-routes.js";
|
|
43
|
+
import { registerMemoryRoutes } from "./routes/memory-routes.js";
|
|
44
|
+
import { registerTeamRoutes } from "./routes/team-routes.js";
|
|
45
|
+
import { registerContentRoutes } from "./routes/content-routes.js";
|
|
46
|
+
import { registerCeoDocumentRoutes } from "./routes/ceo-routes.js";
|
|
47
|
+
import { registerCeoNewsTimeRoutes } from "./routes/ceo-news-time-routes.js";
|
|
48
|
+
import { registerCeoFinanceKpiRoutes } from "./routes/ceo-finance-kpi-routes.js";
|
|
39
49
|
import { nodeManager } from "./federation/node-manager.js";
|
|
40
50
|
import { discoverClaudeSessions } from "./claude-session-discovery.js";
|
|
41
51
|
import { getClaudeSessionHistoryPage } from "./claude-session-history.js";
|
|
@@ -135,17 +145,22 @@ export function createRoutes(
|
|
|
135
145
|
// When behind a reverse proxy (Nginx), check X-Real-IP / X-Forwarded-For
|
|
136
146
|
// headers first, since the TCP source will always be 127.0.0.1 from the proxy.
|
|
137
147
|
function isLocalhostRequest(c: { env: unknown; req: { raw: Request; header: (name: string) => string | undefined } }): boolean {
|
|
138
|
-
//
|
|
148
|
+
// First determine the TCP peer address
|
|
149
|
+
const bunServer = c.env as { requestIP?: (req: Request) => { address: string } | null };
|
|
150
|
+
const ip = bunServer?.requestIP?.(c.req.raw);
|
|
151
|
+
const tcpAddr = ip?.address ?? "";
|
|
152
|
+
const tcpIsLocal = tcpAddr === "127.0.0.1" || tcpAddr === "::1" || tcpAddr === "::ffff:127.0.0.1";
|
|
153
|
+
|
|
154
|
+
// Only trust X-Real-IP when the TCP connection comes from localhost (i.e. a local reverse proxy)
|
|
139
155
|
const realIp = c.req.header("x-real-ip");
|
|
140
|
-
if (realIp) {
|
|
156
|
+
if (realIp && tcpIsLocal) {
|
|
141
157
|
const trimmed = realIp.trim();
|
|
142
158
|
return trimmed === "127.0.0.1" || trimmed === "::1" || trimmed === "::ffff:127.0.0.1";
|
|
143
159
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
160
|
+
|
|
161
|
+
// If X-Real-IP is present but TCP is NOT localhost, ignore the header (spoofed)
|
|
162
|
+
// Fall back to TCP peer address
|
|
163
|
+
return tcpIsLocal;
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
api.get("/auth/auto", (c) => {
|
|
@@ -181,7 +196,9 @@ export function createRoutes(
|
|
|
181
196
|
// send Authorization headers, but browsers do forward cookies automatically.
|
|
182
197
|
const cookieToken = getCookie(c, "heyhank_auth") ?? null;
|
|
183
198
|
if (!verifyToken(token) && !verifyToken(cookieToken)) {
|
|
184
|
-
|
|
199
|
+
// Use 403 instead of 401 to prevent browsers from re-showing
|
|
200
|
+
// the Basic Auth dialog when Nginx basic auth is in front
|
|
201
|
+
return c.json({ error: "unauthorized" }, 403);
|
|
185
202
|
}
|
|
186
203
|
return next();
|
|
187
204
|
});
|
|
@@ -601,7 +618,7 @@ export function createRoutes(
|
|
|
601
618
|
return c.json({
|
|
602
619
|
available: false,
|
|
603
620
|
mode: "container" as const,
|
|
604
|
-
message: "Browser preview requires Xvfb and noVNC in the container image. Rebuild with the latest
|
|
621
|
+
message: "Browser preview requires Xvfb and noVNC in the container image. Rebuild with the latest heyhank image.",
|
|
605
622
|
});
|
|
606
623
|
}
|
|
607
624
|
|
|
@@ -1324,6 +1341,7 @@ export function createRoutes(
|
|
|
1324
1341
|
});
|
|
1325
1342
|
|
|
1326
1343
|
registerSkillRoutes(api);
|
|
1344
|
+
registerMarketplaceRoutes(api);
|
|
1327
1345
|
registerCronRoutes(api, cronScheduler);
|
|
1328
1346
|
registerAgentRoutes(api, agentExecutor);
|
|
1329
1347
|
registerMetricsRoutes(api, { gaugeProvider: wsBridge });
|
|
@@ -1331,8 +1349,17 @@ export function createRoutes(
|
|
|
1331
1349
|
registerFederationRoutes(api);
|
|
1332
1350
|
registerTelephonyRoutes(api);
|
|
1333
1351
|
registerSocialMediaRoutes(api);
|
|
1352
|
+
registerSocialViewRoutes(api);
|
|
1334
1353
|
registerAssistantRoutes(api);
|
|
1354
|
+
registerEmailRoutes(api);
|
|
1335
1355
|
registerProviderRoutes(api);
|
|
1356
|
+
registerHankChatRoutes(api);
|
|
1357
|
+
registerMemoryRoutes(api);
|
|
1358
|
+
registerTeamRoutes(api);
|
|
1359
|
+
registerContentRoutes(api);
|
|
1360
|
+
registerCeoDocumentRoutes(api);
|
|
1361
|
+
registerCeoNewsTimeRoutes(api);
|
|
1362
|
+
registerCeoFinanceKpiRoutes(api);
|
|
1336
1363
|
|
|
1337
1364
|
// ─── Gemini → Session bridge ───────────────────────────────────────
|
|
1338
1365
|
// Allows Gemini voice chat tool calls to send messages to active sessions
|