heyhank 0.1.0 → 0.2.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-B-AAmsMK.js} +3 -3
- package/dist/assets/AssistantPage-BV1Mfwdt.js +2 -0
- package/dist/assets/BusinessPage-tLpNEz19.js +1 -0
- package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-B-K_n3Jg.js} +1 -1
- package/dist/assets/HelpPage-Bhf_j6Xr.js +1 -0
- package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-DAMjs9tM.js} +1 -1
- package/dist/assets/JarvisHUD-C_TGXCCn.js +120 -0
- package/dist/assets/MediaPage-C48HTTrt.js +1 -0
- package/dist/assets/MemoryPage-JkC-qtgp.js +1 -0
- package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-AUo7tNnE.js} +1 -1
- package/dist/assets/{Playground-Fc5cdc5p.js → Playground-AzNMsRBL.js} +1 -1
- package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-DpE_2sX3.js} +1 -1
- package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-C2RQOs6p.js} +2 -2
- package/dist/assets/RunsPage-B9UOyO79.js +1 -0
- package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-jHvYjwfh.js} +1 -1
- package/dist/assets/SettingsPage-BBJax6gt.js +51 -0
- package/dist/assets/SkillsMarketplace-IjmjfdjD.js +1 -0
- package/dist/assets/SocialMediaPage-DoPZHhr2.js +10 -0
- package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DDEY7ckO.js} +1 -1
- package/dist/assets/TelephonyPage-OPNBZYKt.js +9 -0
- package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-BjMbHHW3.js} +1 -1
- package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
- package/dist/assets/{index-CEqZnThB.js → index-BgYM4wXw.js} +94 -93
- package/dist/assets/index-BkjSoVgn.css +32 -0
- package/dist/assets/sw-register-C7NOHtIu.js +1 -0
- package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +6 -1
- package/server/agent-executor.ts +37 -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/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 +2 -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/sw-register-LSSpj6RU.js +0 -1
- package/server/socialmedia/adapters/ayrshare-adapter.ts +0 -169
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// ─── SocialView Types ────────────────────────────────────────────────────────
|
|
2
|
+
// Browser-based social media viewing tool. User logs in manually via noVNC,
|
|
3
|
+
// backend uses Playwright to navigate and extract posts on command.
|
|
4
|
+
|
|
5
|
+
export type SocialPlatform =
|
|
6
|
+
| "instagram"
|
|
7
|
+
| "twitter"
|
|
8
|
+
| "linkedin"
|
|
9
|
+
| "facebook"
|
|
10
|
+
| "tiktok";
|
|
11
|
+
|
|
12
|
+
export const SOCIAL_PLATFORMS: SocialPlatform[] = [
|
|
13
|
+
"instagram",
|
|
14
|
+
"twitter",
|
|
15
|
+
"linkedin",
|
|
16
|
+
"facebook",
|
|
17
|
+
"tiktok",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** URLs the browser navigates to when opening a platform (login page or feed). */
|
|
21
|
+
export const PLATFORM_URLS: Record<SocialPlatform, string> = {
|
|
22
|
+
instagram: "https://www.instagram.com/",
|
|
23
|
+
twitter: "https://x.com/home",
|
|
24
|
+
linkedin: "https://www.linkedin.com/feed/",
|
|
25
|
+
facebook: "https://www.facebook.com/",
|
|
26
|
+
tiktok: "https://www.tiktok.com/",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface SocialViewStatus {
|
|
30
|
+
platform: SocialPlatform;
|
|
31
|
+
running: boolean;
|
|
32
|
+
/** Heuristic: true if current URL suggests user is logged in (not on login page). */
|
|
33
|
+
loggedIn: boolean | null;
|
|
34
|
+
currentUrl: string | null;
|
|
35
|
+
startedAt: number | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A single post captured into the reference library — used as few-shot
|
|
39
|
+
* examples when the content agent generates new posts. */
|
|
40
|
+
export interface LibraryPost {
|
|
41
|
+
id: string;
|
|
42
|
+
platform: SocialPlatform;
|
|
43
|
+
/** "own" = Markus's accounts, "role-model" = other top performers we learn from. */
|
|
44
|
+
source: "own" | "role-model";
|
|
45
|
+
url: string;
|
|
46
|
+
author: {
|
|
47
|
+
handle: string;
|
|
48
|
+
displayName?: string;
|
|
49
|
+
followers?: number;
|
|
50
|
+
verified?: boolean;
|
|
51
|
+
};
|
|
52
|
+
text: string;
|
|
53
|
+
/** First 1–2 sentences isolated — the opening hook. */
|
|
54
|
+
hook: string;
|
|
55
|
+
/** Detected call-to-action (question, link, imperative). Null if none detected. */
|
|
56
|
+
cta: string | null;
|
|
57
|
+
hashtags: string[];
|
|
58
|
+
mentions: string[];
|
|
59
|
+
media: Array<{
|
|
60
|
+
type: "image" | "video";
|
|
61
|
+
localPath: string | null;
|
|
62
|
+
remoteUrl: string | null;
|
|
63
|
+
/** Claude Vision description of the image/video frame. */
|
|
64
|
+
description: string;
|
|
65
|
+
}>;
|
|
66
|
+
engagement: {
|
|
67
|
+
likes: number | null;
|
|
68
|
+
comments: number | null;
|
|
69
|
+
shares: number | null;
|
|
70
|
+
views: number | null;
|
|
71
|
+
saves: number | null;
|
|
72
|
+
};
|
|
73
|
+
/** (likes + comments + shares) / followers. Used for quality filtering. */
|
|
74
|
+
engagementRate: number | null;
|
|
75
|
+
postType: "image" | "carousel" | "reel" | "video" | "text" | "unknown";
|
|
76
|
+
postedAt: string | null;
|
|
77
|
+
/** Manual tags set by user. */
|
|
78
|
+
tags: string[];
|
|
79
|
+
/** Marked as gold standard after manual review. Only "gold" posts feed the agent. */
|
|
80
|
+
isGold: boolean;
|
|
81
|
+
/** When we captured this record. */
|
|
82
|
+
extractedAt: string;
|
|
83
|
+
notes: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface LibraryQuery {
|
|
87
|
+
platform?: SocialPlatform;
|
|
88
|
+
source?: "own" | "role-model";
|
|
89
|
+
goldOnly?: boolean;
|
|
90
|
+
minEngagementRate?: number;
|
|
91
|
+
tags?: string[];
|
|
92
|
+
limit?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Distilled writing-style profile derived from all library posts of a single
|
|
97
|
+
* handle. Used as an "instruction block" in the content-generation prompt so
|
|
98
|
+
* the agent writes in the role-model's voice without us having to ship 5+ raw
|
|
99
|
+
* example posts every time (token-efficient + interpretable).
|
|
100
|
+
*
|
|
101
|
+
* One profile per (platform, handle). Saved at:
|
|
102
|
+
* ~/.heyhank/socialview/style-profiles/<platform>-<handle>.json
|
|
103
|
+
*/
|
|
104
|
+
export interface StyleProfile {
|
|
105
|
+
id: string;
|
|
106
|
+
platform: SocialPlatform;
|
|
107
|
+
handle: string;
|
|
108
|
+
displayName: string;
|
|
109
|
+
|
|
110
|
+
/** How many library posts the profile was distilled from. */
|
|
111
|
+
basedOnPostCount: number;
|
|
112
|
+
/** Library post IDs used in the analysis (for re-runs / audit). */
|
|
113
|
+
basedOnPostIds: string[];
|
|
114
|
+
|
|
115
|
+
/** Avg word count across analyzed posts. */
|
|
116
|
+
averageWordCount: number;
|
|
117
|
+
/** Coarse length descriptor — useful as a one-word hint to the LLM. */
|
|
118
|
+
lengthCategory: "kompakt" | "mittel" | "lang";
|
|
119
|
+
|
|
120
|
+
/** Common opening-hook patterns and their relative frequency [0..1]. */
|
|
121
|
+
hookPatterns: Array<{
|
|
122
|
+
type: string;
|
|
123
|
+
frequency: number;
|
|
124
|
+
examples: string[];
|
|
125
|
+
}>;
|
|
126
|
+
|
|
127
|
+
/** Common closing/CTA patterns and frequency. */
|
|
128
|
+
ctaPatterns: Array<{
|
|
129
|
+
type: string;
|
|
130
|
+
frequency: number;
|
|
131
|
+
examples: string[];
|
|
132
|
+
}>;
|
|
133
|
+
|
|
134
|
+
emojiStyle: "keine" | "sparsam" | "moderat" | "dicht";
|
|
135
|
+
/** Frequently used emojis (most-common first). */
|
|
136
|
+
emojiList: string[];
|
|
137
|
+
|
|
138
|
+
hashtagStyle: "keine" | "wenige" | "viele";
|
|
139
|
+
|
|
140
|
+
/** High-level content themes the handle posts about. */
|
|
141
|
+
contentPillars: string[];
|
|
142
|
+
|
|
143
|
+
/** Free-form description of voice/tone. */
|
|
144
|
+
toneOfVoice: string;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Engagement strategy observed in the post-author's own comments under
|
|
148
|
+
* their posts. Captures patterns like "antwortet mit Frage zurück" or
|
|
149
|
+
* "ergänzt CTA in erstem Eigenkommentar". May be empty if no own-comments
|
|
150
|
+
* were extracted.
|
|
151
|
+
*/
|
|
152
|
+
commentEngagementPattern: string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Synthesis of the visual style across all analyzed posts: composition,
|
|
156
|
+
* color palette, recurring overlay patterns, production quality. Distilled
|
|
157
|
+
* from the per-post image descriptions. Empty string if no images present.
|
|
158
|
+
*/
|
|
159
|
+
visualStyle: string;
|
|
160
|
+
|
|
161
|
+
/** Full LLM analysis as prose — for transparency / manual editing. */
|
|
162
|
+
rawAnalysis: string;
|
|
163
|
+
|
|
164
|
+
createdAt: string;
|
|
165
|
+
updatedAt: string;
|
|
166
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ─── Claude Vision for post images ───────────────────────────────────────────
|
|
2
|
+
// Thin wrapper around the Anthropic Messages API with an image input. Used to
|
|
3
|
+
// describe the visual style of reference posts so the content agent can learn
|
|
4
|
+
// what "high visual quality" looks like per platform.
|
|
5
|
+
|
|
6
|
+
import { getSettings } from "../settings-manager.js";
|
|
7
|
+
|
|
8
|
+
const VISION_MODEL = "claude-sonnet-4-6";
|
|
9
|
+
const VISION_MAX_TOKENS = 512;
|
|
10
|
+
|
|
11
|
+
/** Prompt focused on the things the content agent actually needs to reproduce. */
|
|
12
|
+
const DESCRIBE_PROMPT = `Describe this social media post image for a content agent that needs to learn what works visually.
|
|
13
|
+
|
|
14
|
+
Cover (in 3–5 concise bullet points, under 120 words total):
|
|
15
|
+
- Subject & composition (what's in frame, how it's arranged)
|
|
16
|
+
- Color palette & mood (warm/cool, saturated/muted, dominant colors)
|
|
17
|
+
- Text overlays if any (quote them verbatim)
|
|
18
|
+
- Style (flatlay, portrait, candid, stock-ish, UGC, studio, meme, infographic)
|
|
19
|
+
- Production quality signals (natural light, shallow depth of field, brand consistency)
|
|
20
|
+
|
|
21
|
+
No preamble, just the bullets.`;
|
|
22
|
+
|
|
23
|
+
function resolveAnthropicKey(): string | null {
|
|
24
|
+
const s = getSettings();
|
|
25
|
+
const key = (s.anthropicApiKey as string | undefined)?.trim();
|
|
26
|
+
return key || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Describe an image at an HTTP(S) URL via Claude Vision. Returns empty string on failure. */
|
|
30
|
+
export async function describeImageByUrl(url: string, timeoutMs = 30_000): Promise<string> {
|
|
31
|
+
const apiKey = resolveAnthropicKey();
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.warn("[socialview/vision] no anthropicApiKey configured — skipping vision");
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
"x-api-key": apiKey,
|
|
47
|
+
"anthropic-version": "2023-06-01",
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
model: VISION_MODEL,
|
|
51
|
+
max_tokens: VISION_MAX_TOKENS,
|
|
52
|
+
messages: [
|
|
53
|
+
{
|
|
54
|
+
role: "user",
|
|
55
|
+
content: [
|
|
56
|
+
{ type: "image", source: { type: "url", url } },
|
|
57
|
+
{ type: "text", text: DESCRIBE_PROMPT },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
}),
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.warn(`[socialview/vision] ${res.status} ${res.statusText}`);
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = (await res.json()) as { content?: Array<{ type: string; text?: string }> };
|
|
72
|
+
const text = data.content?.find((b) => b.type === "text")?.text ?? "";
|
|
73
|
+
return text.trim();
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// eslint-disable-next-line no-console
|
|
76
|
+
console.warn("[socialview/vision] error:", e instanceof Error ? e.message : e);
|
|
77
|
+
return "";
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Describe an image from a base64 string. Useful when we download the file locally. */
|
|
84
|
+
export async function describeImageBase64(
|
|
85
|
+
base64Data: string,
|
|
86
|
+
mediaType: "image/jpeg" | "image/png" | "image/webp" | "image/gif",
|
|
87
|
+
timeoutMs = 30_000,
|
|
88
|
+
): Promise<string> {
|
|
89
|
+
const apiKey = resolveAnthropicKey();
|
|
90
|
+
if (!apiKey) return "";
|
|
91
|
+
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"x-api-key": apiKey,
|
|
101
|
+
"anthropic-version": "2023-06-01",
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
model: VISION_MODEL,
|
|
105
|
+
max_tokens: VISION_MAX_TOKENS,
|
|
106
|
+
messages: [
|
|
107
|
+
{
|
|
108
|
+
role: "user",
|
|
109
|
+
content: [
|
|
110
|
+
{ type: "image", source: { type: "base64", media_type: mediaType, data: base64Data } },
|
|
111
|
+
{ type: "text", text: DESCRIBE_PROMPT },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
}),
|
|
116
|
+
signal: controller.signal,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!res.ok) return "";
|
|
120
|
+
const data = (await res.json()) as { content?: Array<{ type: string; text?: string }> };
|
|
121
|
+
return (data.content?.find((b) => b.type === "text")?.text ?? "").trim();
|
|
122
|
+
} catch {
|
|
123
|
+
return "";
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ─── VNC Manager ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Runs x11vnc attached to Xvfb :99, and websockify serving noVNC on
|
|
3
|
+
// 127.0.0.1:6080. Nginx proxies /socialview/vnc/ to this port, so the frontend
|
|
4
|
+
// can iframe the noVNC client.
|
|
5
|
+
|
|
6
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { createConnection } from "node:net";
|
|
9
|
+
|
|
10
|
+
const VNC_DISPLAY = ":99";
|
|
11
|
+
const VNC_PORT = 5900;
|
|
12
|
+
const WEBSOCKIFY_PORT = 6080;
|
|
13
|
+
const NOVNC_WEB_ROOT = "/usr/share/novnc";
|
|
14
|
+
|
|
15
|
+
let x11vncProc: ChildProcess | null = null;
|
|
16
|
+
let websockifyProc: ChildProcess | null = null;
|
|
17
|
+
|
|
18
|
+
function isAlive(p: ChildProcess | null): boolean {
|
|
19
|
+
return !!p && !p.killed && p.exitCode === null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Probe a local TCP port — more reliable than tracking ChildProcess when the
|
|
23
|
+
* spawned daemon forks itself off (x11vnc does this). */
|
|
24
|
+
function probePort(port: number): Promise<boolean> {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const sock = createConnection({ host: "127.0.0.1", port }, () => {
|
|
27
|
+
sock.end();
|
|
28
|
+
resolve(true);
|
|
29
|
+
});
|
|
30
|
+
sock.on("error", () => resolve(false));
|
|
31
|
+
sock.setTimeout(500, () => {
|
|
32
|
+
sock.destroy();
|
|
33
|
+
resolve(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Start x11vnc + websockify if not running. Idempotent. */
|
|
39
|
+
export async function ensureVnc(): Promise<void> {
|
|
40
|
+
if (!existsSync(NOVNC_WEB_ROOT)) {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.warn(`[socialview] noVNC not installed at ${NOVNC_WEB_ROOT}; VNC viewer disabled`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check actual port, not just ChildProcess handle, because x11vnc forks.
|
|
47
|
+
const x11vncUp = await probePort(VNC_PORT);
|
|
48
|
+
if (!x11vncUp && !isAlive(x11vncProc)) {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.log(`[socialview] starting x11vnc on display ${VNC_DISPLAY} port ${VNC_PORT}`);
|
|
51
|
+
x11vncProc = spawn(
|
|
52
|
+
"x11vnc",
|
|
53
|
+
[
|
|
54
|
+
"-display", VNC_DISPLAY,
|
|
55
|
+
"-rfbport", String(VNC_PORT),
|
|
56
|
+
"-localhost", // bind only to 127.0.0.1
|
|
57
|
+
"-forever",
|
|
58
|
+
"-shared",
|
|
59
|
+
"-nopw", // auth is enforced by nginx basic auth + heyhank token on the outer proxy
|
|
60
|
+
"-quiet",
|
|
61
|
+
"-noxdamage",
|
|
62
|
+
],
|
|
63
|
+
{ stdio: "ignore" },
|
|
64
|
+
);
|
|
65
|
+
x11vncProc.on("exit", (code) => {
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.log(`[socialview] x11vnc exited with code ${code}`);
|
|
68
|
+
x11vncProc = null;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const wsUp = await probePort(WEBSOCKIFY_PORT);
|
|
73
|
+
if (!wsUp && !isAlive(websockifyProc)) {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.log(`[socialview] starting websockify on 127.0.0.1:${WEBSOCKIFY_PORT}`);
|
|
76
|
+
websockifyProc = spawn(
|
|
77
|
+
"websockify",
|
|
78
|
+
[
|
|
79
|
+
`127.0.0.1:${WEBSOCKIFY_PORT}`,
|
|
80
|
+
`127.0.0.1:${VNC_PORT}`,
|
|
81
|
+
`--web=${NOVNC_WEB_ROOT}`,
|
|
82
|
+
],
|
|
83
|
+
{ stdio: "ignore" },
|
|
84
|
+
);
|
|
85
|
+
websockifyProc.on("exit", (code) => {
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.log(`[socialview] websockify exited with code ${code}`);
|
|
88
|
+
websockifyProc = null;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Give processes a moment to bind.
|
|
93
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function getVncStatus(): Promise<{ x11vnc: boolean; websockify: boolean; port: number }> {
|
|
97
|
+
const [vncUp, wsUp] = await Promise.all([probePort(VNC_PORT), probePort(WEBSOCKIFY_PORT)]);
|
|
98
|
+
return { x11vnc: vncUp, websockify: wsUp, port: WEBSOCKIFY_PORT };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function shutdownVnc(): Promise<void> {
|
|
102
|
+
if (x11vncProc && !x11vncProc.killed) {
|
|
103
|
+
x11vncProc.kill();
|
|
104
|
+
x11vncProc = null;
|
|
105
|
+
}
|
|
106
|
+
if (websockifyProc && !websockifyProc.killed) {
|
|
107
|
+
websockifyProc.kill();
|
|
108
|
+
websockifyProc = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// ─── Persona Style Injector ──────────────────────────────────────────────────
|
|
2
|
+
// Scans a free-text task description for known SocialView persona references
|
|
3
|
+
// (display name or handle) and produces a binding "STIL-PROFIL"-block that
|
|
4
|
+
// downstream agents (Content Agent etc.) can apply as hard rules.
|
|
5
|
+
//
|
|
6
|
+
// Used by `run_agent` executor to enrich the task automatically — neither
|
|
7
|
+
// Hank nor the agent need to remember to load the profile.
|
|
8
|
+
|
|
9
|
+
import { listProfiles } from "./socialview/style-profiles.js";
|
|
10
|
+
import type { StyleProfile } from "./socialview/types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Find personas mentioned in `text`. A persona matches if its handle OR
|
|
14
|
+
* display-name (case-insensitive, multi-token) appears in the text.
|
|
15
|
+
* Returns the matched profiles, deduped, ordered by length of the matched
|
|
16
|
+
* token (longer = more specific match wins).
|
|
17
|
+
*/
|
|
18
|
+
function findMentionedPersonas(text: string): StyleProfile[] {
|
|
19
|
+
if (!text || !text.trim()) return [];
|
|
20
|
+
const haystack = text.toLowerCase();
|
|
21
|
+
const profiles = listProfiles();
|
|
22
|
+
const hits: Array<{ profile: StyleProfile; matchLen: number }> = [];
|
|
23
|
+
const seen = new Set<string>();
|
|
24
|
+
|
|
25
|
+
for (const p of profiles) {
|
|
26
|
+
const key = `${p.platform}:${p.handle.toLowerCase()}`;
|
|
27
|
+
if (seen.has(key)) continue;
|
|
28
|
+
|
|
29
|
+
const candidates: string[] = [];
|
|
30
|
+
if (p.handle) candidates.push(p.handle.toLowerCase());
|
|
31
|
+
if (p.displayName) {
|
|
32
|
+
const dn = p.displayName.toLowerCase().trim();
|
|
33
|
+
if (dn && dn !== p.handle.toLowerCase()) {
|
|
34
|
+
candidates.push(dn);
|
|
35
|
+
// Also try last-name only (e.g. "remsik" matches "rene remsik")
|
|
36
|
+
const tokens = dn.split(/\s+/).filter((t) => t.length >= 4);
|
|
37
|
+
if (tokens.length > 1) candidates.push(tokens[tokens.length - 1]!);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const c of candidates) {
|
|
42
|
+
// Word-boundary check so "rene" in "renewable" doesn't match.
|
|
43
|
+
const re = new RegExp(`\\b${c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
44
|
+
if (re.test(haystack)) {
|
|
45
|
+
hits.push({ profile: p, matchLen: c.length });
|
|
46
|
+
seen.add(key);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
hits.sort((a, b) => b.matchLen - a.matchLen);
|
|
53
|
+
return hits.map((h) => h.profile);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render a single profile as a markdown block with binding rules. Designed
|
|
58
|
+
* to be appended verbatim to an agent task so the LLM treats it as
|
|
59
|
+
* authoritative.
|
|
60
|
+
*/
|
|
61
|
+
function renderProfileBlock(p: StyleProfile): string {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
lines.push(`## STIL-PROFIL: ${p.displayName || p.handle} (@${p.handle} auf ${p.platform})`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push("DIESE REGELN SIND BINDEND und überschreiben die Plattform-Defaults im System-Prompt:");
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push(`- **Tonfall**: ${p.toneOfVoice || "(nicht spezifiziert)"}`);
|
|
68
|
+
lines.push(`- **Länge**: ${p.lengthCategory} (Ø ${p.averageWordCount} Wörter)`);
|
|
69
|
+
lines.push(`- **Hashtag-Stil**: ${p.hashtagStyle}${p.hashtagStyle === "keine" ? " — VERWENDE KEINE HASHTAGS, auch wenn der Plattform-Default welche vorschlägt" : ""}`);
|
|
70
|
+
lines.push(`- **Emoji-Stil**: ${p.emojiStyle}${p.emojiStyle === "keine" ? " — VERWENDE KEINE EMOJIS" : ""}`);
|
|
71
|
+
|
|
72
|
+
if (p.hookPatterns.length > 0) {
|
|
73
|
+
const top = p.hookPatterns.slice().sort((a, b) => b.frequency - a.frequency).slice(0, 3);
|
|
74
|
+
lines.push(`- **Bevorzugte Hooks** (verwende einen davon, NICHT die Standard-Hook-Liste):`);
|
|
75
|
+
for (const h of top) {
|
|
76
|
+
const ex = h.examples[0] ? ` — z.B. "${h.examples[0].slice(0, 100)}"` : "";
|
|
77
|
+
lines.push(` - ${h.type}${ex}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (p.ctaPatterns.length > 0) {
|
|
82
|
+
const top = p.ctaPatterns.slice().sort((a, b) => b.frequency - a.frequency).slice(0, 2);
|
|
83
|
+
lines.push(`- **CTA-Muster**:`);
|
|
84
|
+
for (const c of top) {
|
|
85
|
+
const ex = c.examples[0] ? ` — z.B. "${c.examples[0].slice(0, 100)}"` : "";
|
|
86
|
+
lines.push(` - ${c.type}${ex}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (p.contentPillars.length > 0) {
|
|
91
|
+
lines.push(`- **Content-Säulen**: ${p.contentPillars.join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (p.commentEngagementPattern && p.commentEngagementPattern.trim()) {
|
|
95
|
+
lines.push(`- **Eigenkommentar-Pattern**: ${p.commentEngagementPattern}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (p.visualStyle && p.visualStyle.trim()) {
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push("### Visueller Stil (für Bildgenerierung)");
|
|
101
|
+
lines.push(p.visualStyle);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (p.rawAnalysis && p.rawAnalysis.trim()) {
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push("### Gesamteinschätzung");
|
|
107
|
+
lines.push(p.rawAnalysis);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Public entry: given a task text, return a string that should be APPENDED
|
|
115
|
+
* to the task. Empty string if no persona is mentioned.
|
|
116
|
+
*/
|
|
117
|
+
export function buildStyleProfileBlockFromText(text: string): string {
|
|
118
|
+
const matches = findMentionedPersonas(text);
|
|
119
|
+
if (matches.length === 0) return "";
|
|
120
|
+
// Cap to top 2 to keep the prompt focused.
|
|
121
|
+
const blocks = matches.slice(0, 2).map(renderProfileBlock);
|
|
122
|
+
return [
|
|
123
|
+
"",
|
|
124
|
+
"---",
|
|
125
|
+
"",
|
|
126
|
+
"# AUTOMATISCH ERKANNTE PERSONA-REFERENZ",
|
|
127
|
+
"",
|
|
128
|
+
"Der User hat oben eine Persona genannt. Wende das folgende Profil als",
|
|
129
|
+
"BINDENDE Schreib- und Bildregel an. Die Profil-Regeln gewinnen IMMER",
|
|
130
|
+
"gegen die Plattform-Defaults im System-Prompt (z.B. Hashtag-Anzahl,",
|
|
131
|
+
"Hook-Liste, Emoji-Verwendung).",
|
|
132
|
+
"",
|
|
133
|
+
blocks.join("\n\n"),
|
|
134
|
+
].join("\n");
|
|
135
|
+
}
|