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.
Files changed (161) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -10
  3. package/bin/cli.ts +7 -7
  4. package/bin/ctl.ts +42 -42
  5. package/dist/assets/{AgentsPage-BPhirnCe.js → AgentsPage-DqjDAcIw.js} +3 -3
  6. package/dist/assets/AssistantPage-C50CQFSB.js +2 -0
  7. package/dist/assets/BusinessPage-AY70tf1k.js +1 -0
  8. package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-Dt7LLuRr.js} +1 -1
  9. package/dist/assets/HelpPage-tlGx7fQF.js +1 -0
  10. package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-B4XOuHXu.js} +1 -1
  11. package/dist/assets/JarvisHUD-BDvuRd0I.js +120 -0
  12. package/dist/assets/MediaPage-CofV9Rd-.js +1 -0
  13. package/dist/assets/MemoryPage-Cj7FeqmJ.js +1 -0
  14. package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-B9kXAlH1.js} +1 -1
  15. package/dist/assets/{Playground-Fc5cdc5p.js → Playground-Cka-pRkP.js} +1 -1
  16. package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-BqhQgfYj.js} +1 -1
  17. package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-VveKc9uX.js} +2 -2
  18. package/dist/assets/RunsPage-DXVEk0AZ.js +1 -0
  19. package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-DACcwfDF.js} +1 -1
  20. package/dist/assets/SettingsPage-jfuQh8Tu.js +51 -0
  21. package/dist/assets/SkillsMarketplace-DrigiApe.js +1 -0
  22. package/dist/assets/SocialMediaPage-DOh3IPe8.js +10 -0
  23. package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DLhJWATT.js} +1 -1
  24. package/dist/assets/TelephonyPage-9C4C3_ot.js +9 -0
  25. package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-ChX-8Wu7.js} +1 -1
  26. package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
  27. package/dist/assets/index-C6Q5UQHD.js +229 -0
  28. package/dist/assets/index-ZxGXgiV3.css +32 -0
  29. package/dist/assets/sw-register-BBYuk-kw.js +1 -0
  30. package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
  31. package/dist/assets/workbox-window.prod.es5-BBnX5xw4.js +2 -0
  32. package/dist/index.html +2 -2
  33. package/dist/sw.js +1 -1
  34. package/dist/{workbox-d2a0910a.js → workbox-080c8b91.js} +1 -1
  35. package/package.json +6 -1
  36. package/server/agent-executor.ts +102 -2
  37. package/server/agent-store.ts +3 -3
  38. package/server/agent-types.ts +11 -0
  39. package/server/assistant-store.ts +232 -6
  40. package/server/auth-manager.ts +9 -0
  41. package/server/cache-headers.ts +1 -1
  42. package/server/calendar-service.ts +10 -0
  43. package/server/ceo/document-store.ts +129 -0
  44. package/server/ceo/finance-store.ts +343 -0
  45. package/server/ceo/kpi-store.ts +208 -0
  46. package/server/ceo/memory-import.ts +277 -0
  47. package/server/ceo/news-store.ts +208 -0
  48. package/server/ceo/template-store.ts +134 -0
  49. package/server/ceo/time-tracking-store.ts +227 -0
  50. package/server/claude-auth-monitor.ts +128 -0
  51. package/server/claude-code-worker.ts +86 -0
  52. package/server/claude-session-discovery.ts +74 -1
  53. package/server/cli-launcher.ts +32 -10
  54. package/server/codex-adapter.ts +2 -2
  55. package/server/codex-ws-proxy.cjs +1 -1
  56. package/server/container-manager.ts +4 -4
  57. package/server/content-intelligence/content-engine.ts +1112 -0
  58. package/server/content-intelligence/platform-knowledge.ts +870 -0
  59. package/server/cron-store.ts +3 -3
  60. package/server/embedding-service.ts +49 -0
  61. package/server/event-bus-types.ts +13 -0
  62. package/server/execution-store.ts +54 -1
  63. package/server/federation/node-store.ts +5 -4
  64. package/server/fs-utils.ts +28 -1
  65. package/server/hank-notifications-store.ts +91 -0
  66. package/server/hank-tool-executor.ts +1835 -0
  67. package/server/hank-tools.ts +2107 -0
  68. package/server/image-pull-manager.ts +2 -2
  69. package/server/index.ts +25 -2
  70. package/server/llm-providers-streaming.ts +541 -0
  71. package/server/llm-providers.ts +12 -0
  72. package/server/marketplace.ts +249 -0
  73. package/server/mcp-registry.ts +158 -0
  74. package/server/memory-service.ts +296 -0
  75. package/server/obsidian-sync.ts +184 -0
  76. package/server/provider-manager.ts +5 -2
  77. package/server/provider-registry.ts +12 -0
  78. package/server/reminder-scheduler.ts +37 -1
  79. package/server/routes/agent-routes.ts +44 -1
  80. package/server/routes/assistant-routes.ts +198 -5
  81. package/server/routes/ceo-finance-kpi-routes.ts +167 -0
  82. package/server/routes/ceo-news-time-routes.ts +137 -0
  83. package/server/routes/ceo-routes.ts +99 -0
  84. package/server/routes/content-routes.ts +116 -0
  85. package/server/routes/email-routes.ts +147 -0
  86. package/server/routes/env-routes.ts +3 -3
  87. package/server/routes/fs-routes.ts +12 -9
  88. package/server/routes/hank-chat-routes.ts +592 -0
  89. package/server/routes/llm-routes.ts +12 -0
  90. package/server/routes/marketplace-routes.ts +63 -0
  91. package/server/routes/media-routes.ts +1 -1
  92. package/server/routes/memory-routes.ts +127 -0
  93. package/server/routes/platform-routes.ts +14 -675
  94. package/server/routes/sandbox-routes.ts +1 -1
  95. package/server/routes/settings-routes.ts +51 -1
  96. package/server/routes/socialmedia-routes.ts +152 -2
  97. package/server/routes/system-routes.ts +2 -2
  98. package/server/routes/team-routes.ts +71 -0
  99. package/server/routes/telephony-routes.ts +98 -18
  100. package/server/routes.ts +36 -9
  101. package/server/session-creation-service.ts +2 -2
  102. package/server/session-orchestrator.ts +54 -2
  103. package/server/session-types.ts +2 -0
  104. package/server/settings-manager.ts +50 -2
  105. package/server/skill-discovery.ts +68 -0
  106. package/server/socialmedia/adapters/browser-adapter.ts +179 -0
  107. package/server/socialmedia/adapters/postiz-adapter.ts +291 -14
  108. package/server/socialmedia/manager.ts +234 -15
  109. package/server/socialmedia/store.ts +51 -1
  110. package/server/socialmedia/types.ts +35 -2
  111. package/server/socialview/browser-manager.ts +150 -0
  112. package/server/socialview/extractors.ts +1298 -0
  113. package/server/socialview/image-describe.ts +188 -0
  114. package/server/socialview/library.ts +119 -0
  115. package/server/socialview/poster.ts +276 -0
  116. package/server/socialview/routes.ts +371 -0
  117. package/server/socialview/style-analyzer.ts +187 -0
  118. package/server/socialview/style-profiles.ts +67 -0
  119. package/server/socialview/types.ts +166 -0
  120. package/server/socialview/vision.ts +127 -0
  121. package/server/socialview/vnc-manager.ts +110 -0
  122. package/server/style-injector.ts +135 -0
  123. package/server/team-service.ts +239 -0
  124. package/server/team-store.ts +75 -0
  125. package/server/team-types.ts +52 -0
  126. package/server/telephony/audio-bridge.ts +281 -35
  127. package/server/telephony/audio-recorder.ts +132 -0
  128. package/server/telephony/call-manager.ts +803 -104
  129. package/server/telephony/call-types.ts +67 -1
  130. package/server/telephony/esl-client.ts +319 -0
  131. package/server/telephony/freeswitch-sync.ts +155 -0
  132. package/server/telephony/phone-utils.ts +63 -0
  133. package/server/telephony/telephony-store.ts +9 -8
  134. package/server/url-validator.ts +82 -0
  135. package/server/vault-markdown.ts +317 -0
  136. package/server/vault-migration.ts +121 -0
  137. package/server/vault-store.ts +466 -0
  138. package/server/vault-watcher.ts +59 -0
  139. package/server/vector-store.ts +210 -0
  140. package/server/voice-pipeline/gemini-live-adapter.ts +97 -0
  141. package/server/voice-pipeline/greeting-cache.ts +200 -0
  142. package/server/voice-pipeline/manager.ts +249 -0
  143. package/server/voice-pipeline/pipeline.ts +335 -0
  144. package/server/voice-pipeline/providers/index.ts +47 -0
  145. package/server/voice-pipeline/providers/llm-internal.ts +527 -0
  146. package/server/voice-pipeline/providers/stt-google.ts +157 -0
  147. package/server/voice-pipeline/providers/tts-google.ts +126 -0
  148. package/server/voice-pipeline/types.ts +247 -0
  149. package/server/ws-bridge-types.ts +6 -1
  150. package/dist/assets/AssistantPage-DJ-cMQfb.js +0 -1
  151. package/dist/assets/HelpPage-DMfkzERp.js +0 -1
  152. package/dist/assets/MediaPage-CE5rdvkC.js +0 -1
  153. package/dist/assets/RunsPage-C5BZF5Rx.js +0 -1
  154. package/dist/assets/SettingsPage-DirhjQrJ.js +0 -51
  155. package/dist/assets/SocialMediaPage-DBuM28vD.js +0 -1
  156. package/dist/assets/TelephonyPage-x0VV0fOo.js +0 -1
  157. package/dist/assets/index-C8M_PUmX.css +0 -32
  158. package/dist/assets/index-CEqZnThB.js +0 -204
  159. package/dist/assets/sw-register-LSSpj6RU.js +0 -1
  160. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +0 -2
  161. 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 = "the-companion:latest";
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: body.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) post.platforms = body.platforms;
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("the-companion:latest");
137
- const ready = await imagePullManager.waitForReady("the-companion:latest", 120_000);
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 eslUrl = `http://${eslHost}:${eslPort}/api`;
237
- const res = await fetch(eslUrl, {
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
- // If a reverse proxy set X-Real-IP, use that (this is the real client IP)
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
- // Fallback to TCP socket address (direct connections without proxy)
145
- const bunServer = c.env as { requestIP?: (req: Request) => { address: string } | null };
146
- const ip = bunServer?.requestIP?.(c.req.raw);
147
- const addr = ip?.address ?? "";
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
- return c.json({ error: "unauthorized" }, 401);
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 the-companion image.",
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