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,277 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
|
|
3
|
+
// ─── Platform Detection ─────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type ImportPlatform = "chatgpt" | "claude" | "gemini" | "unknown";
|
|
6
|
+
|
|
7
|
+
export interface ImportResult {
|
|
8
|
+
platform: ImportPlatform;
|
|
9
|
+
memories: string[];
|
|
10
|
+
conversations: number;
|
|
11
|
+
messagesProcessed: number;
|
|
12
|
+
skipped: number;
|
|
13
|
+
errors: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Auto-detect which platform a JSON export belongs to.
|
|
18
|
+
*/
|
|
19
|
+
export function detectPlatform(data: unknown): ImportPlatform {
|
|
20
|
+
if (!data) return "unknown";
|
|
21
|
+
|
|
22
|
+
// ChatGPT: array of objects with "mapping" field (tree structure)
|
|
23
|
+
if (Array.isArray(data) && data.length > 0 && data[0].mapping) return "chatgpt";
|
|
24
|
+
|
|
25
|
+
// ChatGPT memories: array with "id" starting with "mem_"
|
|
26
|
+
if (Array.isArray(data) && data.length > 0 && typeof data[0].id === "string" && data[0].id.startsWith("mem_")) return "chatgpt";
|
|
27
|
+
|
|
28
|
+
// Claude: array of objects with "chat_messages" field
|
|
29
|
+
if (Array.isArray(data) && data.length > 0 && data[0].chat_messages) return "claude";
|
|
30
|
+
|
|
31
|
+
// Claude: object with uuid and chat_messages
|
|
32
|
+
if (typeof data === "object" && !Array.isArray(data) && (data as any).chat_messages) return "claude";
|
|
33
|
+
|
|
34
|
+
// Gemini: object with "messages" array where role is "model"
|
|
35
|
+
if (typeof data === "object" && !Array.isArray(data) && Array.isArray((data as any).messages)) {
|
|
36
|
+
const msgs = (data as any).messages;
|
|
37
|
+
if (msgs.some((m: any) => m.role === "model")) return "gemini";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Gemini: array of conversation objects with messages containing role "model"
|
|
41
|
+
if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0].messages)) {
|
|
42
|
+
if (data[0].messages.some((m: any) => m.role === "model")) return "gemini";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── ChatGPT Import ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Import ChatGPT memories (memories.json) — direct facts.
|
|
52
|
+
*/
|
|
53
|
+
export function parseChatGPTMemories(data: any[]): string[] {
|
|
54
|
+
return data
|
|
55
|
+
.filter(item => item.content && typeof item.content === "string")
|
|
56
|
+
.map(item => item.content.trim())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract user messages from ChatGPT conversations.json tree structure.
|
|
62
|
+
* Walk from current_node back through parent pointers to reconstruct linear thread.
|
|
63
|
+
*/
|
|
64
|
+
export function parseChatGPTConversations(data: any[]): { conversations: number; messages: Array<{ role: string; content: string; conversationTitle?: string }> } {
|
|
65
|
+
const allMessages: Array<{ role: string; content: string; conversationTitle?: string }> = [];
|
|
66
|
+
let conversations = 0;
|
|
67
|
+
|
|
68
|
+
for (const conv of data) {
|
|
69
|
+
if (!conv.mapping) continue;
|
|
70
|
+
conversations++;
|
|
71
|
+
const title = conv.title || "Untitled";
|
|
72
|
+
|
|
73
|
+
// Reconstruct linear thread from tree
|
|
74
|
+
let nodeId = conv.current_node;
|
|
75
|
+
const chain: any[] = [];
|
|
76
|
+
while (nodeId && conv.mapping[nodeId]) {
|
|
77
|
+
const node = conv.mapping[nodeId];
|
|
78
|
+
if (node.message?.content?.parts) {
|
|
79
|
+
const text = node.message.content.parts
|
|
80
|
+
.filter((p: any) => typeof p === "string")
|
|
81
|
+
.join("\n")
|
|
82
|
+
.trim();
|
|
83
|
+
if (text) {
|
|
84
|
+
chain.push({
|
|
85
|
+
role: node.message.author?.role === "user" ? "user" : "assistant",
|
|
86
|
+
content: text,
|
|
87
|
+
conversationTitle: title,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
nodeId = node.parent;
|
|
92
|
+
}
|
|
93
|
+
chain.reverse();
|
|
94
|
+
allMessages.push(...chain);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { conversations, messages: allMessages };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Claude Import ──────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract messages from Claude conversations.json export.
|
|
104
|
+
*/
|
|
105
|
+
export function parseClaudeConversations(data: any[]): { conversations: number; messages: Array<{ role: string; content: string; conversationTitle?: string }> } {
|
|
106
|
+
const allMessages: Array<{ role: string; content: string; conversationTitle?: string }> = [];
|
|
107
|
+
let conversations = 0;
|
|
108
|
+
|
|
109
|
+
const convList = Array.isArray(data) ? data : [data];
|
|
110
|
+
|
|
111
|
+
for (const conv of convList) {
|
|
112
|
+
if (!conv.chat_messages) continue;
|
|
113
|
+
conversations++;
|
|
114
|
+
const title = conv.name || "Untitled";
|
|
115
|
+
|
|
116
|
+
for (const msg of conv.chat_messages) {
|
|
117
|
+
const text = typeof msg.text === "string" ? msg.text.trim() : "";
|
|
118
|
+
if (!text) continue;
|
|
119
|
+
allMessages.push({
|
|
120
|
+
role: msg.sender === "human" ? "user" : "assistant",
|
|
121
|
+
content: text,
|
|
122
|
+
conversationTitle: title,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { conversations, messages: allMessages };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Gemini Import ──────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract messages from Gemini export (Google Takeout format).
|
|
134
|
+
*/
|
|
135
|
+
export function parseGeminiConversations(data: any): { conversations: number; messages: Array<{ role: string; content: string; conversationTitle?: string }> } {
|
|
136
|
+
const allMessages: Array<{ role: string; content: string; conversationTitle?: string }> = [];
|
|
137
|
+
let conversations = 0;
|
|
138
|
+
|
|
139
|
+
const convList = Array.isArray(data) ? data : [data];
|
|
140
|
+
|
|
141
|
+
for (const conv of convList) {
|
|
142
|
+
if (!Array.isArray(conv.messages)) continue;
|
|
143
|
+
conversations++;
|
|
144
|
+
const title = conv.title || "Untitled";
|
|
145
|
+
|
|
146
|
+
for (const msg of conv.messages) {
|
|
147
|
+
const text = typeof msg.content === "string" ? msg.content.trim() : "";
|
|
148
|
+
if (!text) continue;
|
|
149
|
+
allMessages.push({
|
|
150
|
+
role: msg.role === "user" ? "user" : "assistant",
|
|
151
|
+
content: text,
|
|
152
|
+
conversationTitle: title,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { conversations, messages: allMessages };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Fact Extraction ────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract memorable facts from user messages without LLM.
|
|
164
|
+
* Simple heuristic: look for "I am", "I work", "I prefer", "My name", etc.
|
|
165
|
+
*/
|
|
166
|
+
export function extractFactsHeuristic(messages: Array<{ role: string; content: string }>): string[] {
|
|
167
|
+
const userMessages = messages.filter(m => m.role === "user");
|
|
168
|
+
const facts: Set<string> = new Set();
|
|
169
|
+
|
|
170
|
+
const patterns = [
|
|
171
|
+
/\b(?:I am|I'm|Ich bin)\s+(.{5,80})/gi,
|
|
172
|
+
/\b(?:my name is|mein name ist)\s+(.{2,40})/gi,
|
|
173
|
+
/\b(?:I work|I'm working|Ich arbeite)\s+(.{5,80})/gi,
|
|
174
|
+
/\b(?:I prefer|I like|Ich bevorzuge|Ich mag)\s+(.{5,80})/gi,
|
|
175
|
+
/\b(?:I use|I'm using|Ich verwende|Ich nutze)\s+(.{5,80})/gi,
|
|
176
|
+
/\b(?:I live|I'm from|Ich wohne|Ich komme aus)\s+(.{3,60})/gi,
|
|
177
|
+
/\b(?:my company|my business|meine firma|mein unternehmen)\s+(.{3,60})/gi,
|
|
178
|
+
/\b(?:I always|Ich immer)\s+(.{5,80})/gi,
|
|
179
|
+
/\b(?:I need|I want|Ich brauche|Ich will)\s+(.{5,80})/gi,
|
|
180
|
+
/\b(?:my timezone|my language|my email)\s+(.{3,60})/gi,
|
|
181
|
+
/\b(?:remember that|merke dir|vergiss nicht)\s+(.{5,120})/gi,
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (const msg of userMessages) {
|
|
185
|
+
const content = msg.content;
|
|
186
|
+
// Skip very long messages (likely code or documents)
|
|
187
|
+
if (content.length > 500) continue;
|
|
188
|
+
|
|
189
|
+
for (const pattern of patterns) {
|
|
190
|
+
pattern.lastIndex = 0;
|
|
191
|
+
let match;
|
|
192
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
193
|
+
// Clean up the match — take first sentence
|
|
194
|
+
let fact = match[0].trim();
|
|
195
|
+
const dotIdx = fact.indexOf(".");
|
|
196
|
+
if (dotIdx > 10) fact = fact.slice(0, dotIdx + 1);
|
|
197
|
+
// Remove trailing punctuation patterns
|
|
198
|
+
fact = fact.replace(/[,;:]\s*$/, "").trim();
|
|
199
|
+
if (fact.length >= 10 && fact.length <= 200) {
|
|
200
|
+
facts.add(fact);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return [...facts];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Main Import Function ───────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export interface ImportOptions {
|
|
212
|
+
mode: "memories" | "conversations" | "both";
|
|
213
|
+
extractFacts: boolean; // use heuristic extraction from conversations
|
|
214
|
+
maxConversations?: number; // limit for large exports
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function processImport(data: unknown, platform: ImportPlatform, options: ImportOptions): ImportResult {
|
|
218
|
+
const result: ImportResult = {
|
|
219
|
+
platform,
|
|
220
|
+
memories: [],
|
|
221
|
+
conversations: 0,
|
|
222
|
+
messagesProcessed: 0,
|
|
223
|
+
skipped: 0,
|
|
224
|
+
errors: [],
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Direct memories import (ChatGPT memories.json)
|
|
229
|
+
if (Array.isArray(data) && data.length > 0 && data[0].id?.startsWith?.("mem_")) {
|
|
230
|
+
const mems = parseChatGPTMemories(data);
|
|
231
|
+
result.memories.push(...mems);
|
|
232
|
+
result.platform = "chatgpt";
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Conversation-based import
|
|
237
|
+
let parsed: { conversations: number; messages: Array<{ role: string; content: string }> };
|
|
238
|
+
|
|
239
|
+
switch (platform) {
|
|
240
|
+
case "chatgpt":
|
|
241
|
+
parsed = parseChatGPTConversations(data as any[]);
|
|
242
|
+
break;
|
|
243
|
+
case "claude":
|
|
244
|
+
parsed = parseClaudeConversations(data as any[]);
|
|
245
|
+
break;
|
|
246
|
+
case "gemini":
|
|
247
|
+
parsed = parseGeminiConversations(data);
|
|
248
|
+
break;
|
|
249
|
+
default:
|
|
250
|
+
result.errors.push("Unknown platform format. Please select the correct platform.");
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
result.conversations = parsed.conversations;
|
|
255
|
+
result.messagesProcessed = parsed.messages.length;
|
|
256
|
+
|
|
257
|
+
// Limit conversations if needed
|
|
258
|
+
const maxMsgs = (options.maxConversations || 1000) * 20; // ~20 msgs per conversation
|
|
259
|
+
const msgs = parsed.messages.slice(0, maxMsgs);
|
|
260
|
+
if (parsed.messages.length > maxMsgs) {
|
|
261
|
+
result.skipped = parsed.messages.length - maxMsgs;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Extract facts from conversations
|
|
265
|
+
if (options.extractFacts || options.mode === "both") {
|
|
266
|
+
const facts = extractFactsHeuristic(msgs);
|
|
267
|
+
result.memories.push(...facts);
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
result.errors.push(err instanceof Error ? err.message : "Import failed");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Deduplicate memories
|
|
274
|
+
result.memories = [...new Set(result.memories)];
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { atomicWriteFileSync } from "../fs-utils.js";
|
|
5
|
+
|
|
6
|
+
export interface NewsSource {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
type: "rss" | "website" | "keyword";
|
|
10
|
+
url?: string; // for rss/website
|
|
11
|
+
keywords?: string[]; // for keyword monitoring
|
|
12
|
+
category: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
checkInterval: number; // minutes
|
|
15
|
+
lastChecked?: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface NewsItem {
|
|
20
|
+
id: string;
|
|
21
|
+
sourceId: string;
|
|
22
|
+
sourceName: string;
|
|
23
|
+
title: string;
|
|
24
|
+
summary: string;
|
|
25
|
+
url?: string;
|
|
26
|
+
category: string;
|
|
27
|
+
publishedAt: string;
|
|
28
|
+
fetchedAt: string;
|
|
29
|
+
read: boolean;
|
|
30
|
+
saved: boolean;
|
|
31
|
+
relevance?: number; // 0-100, AI-scored
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DATA_DIR = join(process.env.HEYHANK_HOME || join(process.env.HOME || "/root", ".heyhank"), "news");
|
|
35
|
+
const SOURCES_FILE = join(DATA_DIR, "sources.json");
|
|
36
|
+
const ITEMS_FILE = join(DATA_DIR, "items.json");
|
|
37
|
+
|
|
38
|
+
function ensureDir() {
|
|
39
|
+
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadSources(): NewsSource[] {
|
|
43
|
+
ensureDir();
|
|
44
|
+
if (!existsSync(SOURCES_FILE)) return [];
|
|
45
|
+
try { return JSON.parse(readFileSync(SOURCES_FILE, "utf-8")); }
|
|
46
|
+
catch { return []; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveSources(sources: NewsSource[]) {
|
|
50
|
+
ensureDir();
|
|
51
|
+
atomicWriteFileSync(SOURCES_FILE, JSON.stringify(sources, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadItems(): NewsItem[] {
|
|
55
|
+
ensureDir();
|
|
56
|
+
if (!existsSync(ITEMS_FILE)) return [];
|
|
57
|
+
try { return JSON.parse(readFileSync(ITEMS_FILE, "utf-8")); }
|
|
58
|
+
catch { return []; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveItems(items: NewsItem[]) {
|
|
62
|
+
ensureDir();
|
|
63
|
+
// Keep max 500 items
|
|
64
|
+
const trimmed = items.slice(0, 500);
|
|
65
|
+
atomicWriteFileSync(ITEMS_FILE, JSON.stringify(trimmed, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Sources
|
|
69
|
+
export function addSource(name: string, type: NewsSource["type"], category: string, url?: string, keywords?: string[], checkInterval?: number): NewsSource {
|
|
70
|
+
const sources = loadSources();
|
|
71
|
+
const source: NewsSource = {
|
|
72
|
+
id: randomUUID(),
|
|
73
|
+
name,
|
|
74
|
+
type,
|
|
75
|
+
url,
|
|
76
|
+
keywords,
|
|
77
|
+
category,
|
|
78
|
+
enabled: true,
|
|
79
|
+
checkInterval: checkInterval || 60,
|
|
80
|
+
createdAt: new Date().toISOString()
|
|
81
|
+
};
|
|
82
|
+
sources.push(source);
|
|
83
|
+
saveSources(sources);
|
|
84
|
+
return source;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function listSources(): NewsSource[] {
|
|
88
|
+
return loadSources();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function updateSource(id: string, patch: Partial<Pick<NewsSource, "name" | "enabled" | "checkInterval" | "keywords" | "url" | "category">>): NewsSource | null {
|
|
92
|
+
const sources = loadSources();
|
|
93
|
+
const idx = sources.findIndex(s => s.id === id);
|
|
94
|
+
if (idx === -1) return null;
|
|
95
|
+
Object.assign(sources[idx], patch);
|
|
96
|
+
saveSources(sources);
|
|
97
|
+
return sources[idx];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function deleteSource(id: string): boolean {
|
|
101
|
+
const sources = loadSources();
|
|
102
|
+
const idx = sources.findIndex(s => s.id === id);
|
|
103
|
+
if (idx === -1) return false;
|
|
104
|
+
sources.splice(idx, 1);
|
|
105
|
+
saveSources(sources);
|
|
106
|
+
// Also remove items from this source
|
|
107
|
+
const items = loadItems().filter(i => i.sourceId !== id);
|
|
108
|
+
saveItems(items);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function markSourceChecked(id: string): void {
|
|
113
|
+
const sources = loadSources();
|
|
114
|
+
const idx = sources.findIndex(s => s.id === id);
|
|
115
|
+
if (idx !== -1) {
|
|
116
|
+
sources[idx].lastChecked = new Date().toISOString();
|
|
117
|
+
saveSources(sources);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Items
|
|
122
|
+
export function addNewsItem(sourceId: string, sourceName: string, title: string, summary: string, category: string, url?: string, relevance?: number): NewsItem {
|
|
123
|
+
const items = loadItems();
|
|
124
|
+
// Dedup by title+source
|
|
125
|
+
if (items.some(i => i.sourceId === sourceId && i.title === title)) {
|
|
126
|
+
return items.find(i => i.sourceId === sourceId && i.title === title)!;
|
|
127
|
+
}
|
|
128
|
+
const item: NewsItem = {
|
|
129
|
+
id: randomUUID(),
|
|
130
|
+
sourceId,
|
|
131
|
+
sourceName,
|
|
132
|
+
title,
|
|
133
|
+
summary,
|
|
134
|
+
url,
|
|
135
|
+
category,
|
|
136
|
+
publishedAt: new Date().toISOString(),
|
|
137
|
+
fetchedAt: new Date().toISOString(),
|
|
138
|
+
read: false,
|
|
139
|
+
saved: false,
|
|
140
|
+
relevance
|
|
141
|
+
};
|
|
142
|
+
items.unshift(item);
|
|
143
|
+
saveItems(items);
|
|
144
|
+
return item;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function listNews(category?: string, unreadOnly?: boolean, savedOnly?: boolean, limit?: number): NewsItem[] {
|
|
148
|
+
let items = loadItems();
|
|
149
|
+
if (category) items = items.filter(i => i.category === category);
|
|
150
|
+
if (unreadOnly) items = items.filter(i => !i.read);
|
|
151
|
+
if (savedOnly) items = items.filter(i => i.saved);
|
|
152
|
+
return items.slice(0, limit || 50);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function markRead(id: string): boolean {
|
|
156
|
+
const items = loadItems();
|
|
157
|
+
const idx = items.findIndex(i => i.id === id);
|
|
158
|
+
if (idx === -1) return false;
|
|
159
|
+
items[idx].read = true;
|
|
160
|
+
saveItems(items);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function markAllRead(category?: string): number {
|
|
165
|
+
const items = loadItems();
|
|
166
|
+
let count = 0;
|
|
167
|
+
for (const item of items) {
|
|
168
|
+
if (!item.read && (!category || item.category === category)) {
|
|
169
|
+
item.read = true;
|
|
170
|
+
count++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
saveItems(items);
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function toggleSaved(id: string): NewsItem | null {
|
|
178
|
+
const items = loadItems();
|
|
179
|
+
const idx = items.findIndex(i => i.id === id);
|
|
180
|
+
if (idx === -1) return null;
|
|
181
|
+
items[idx].saved = !items[idx].saved;
|
|
182
|
+
saveItems(items);
|
|
183
|
+
return items[idx];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function searchNews(query: string): NewsItem[] {
|
|
187
|
+
const q = query.toLowerCase();
|
|
188
|
+
return loadItems().filter(i =>
|
|
189
|
+
i.title.toLowerCase().includes(q) ||
|
|
190
|
+
i.summary.toLowerCase().includes(q) ||
|
|
191
|
+
i.sourceName.toLowerCase().includes(q)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getNewsStats(): { total: number; unread: number; sources: number; byCategory: Record<string, number> } {
|
|
196
|
+
const items = loadItems();
|
|
197
|
+
const sources = loadSources();
|
|
198
|
+
const byCategory: Record<string, number> = {};
|
|
199
|
+
for (const item of items.filter(i => !i.read)) {
|
|
200
|
+
byCategory[item.category] = (byCategory[item.category] || 0) + 1;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
total: items.length,
|
|
204
|
+
unread: items.filter(i => !i.read).length,
|
|
205
|
+
sources: sources.filter(s => s.enabled).length,
|
|
206
|
+
byCategory
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { atomicWriteFileSync } from "../fs-utils.js";
|
|
5
|
+
|
|
6
|
+
export interface TemplateVariable {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Template {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
category: string; // email, contract, meeting, invoice, report, custom
|
|
17
|
+
content: string; // with {{variable}} placeholders
|
|
18
|
+
variables: TemplateVariable[];
|
|
19
|
+
tags: string[];
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
usageCount: number;
|
|
23
|
+
lastUsed?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DATA_DIR = join(process.env.HEYHANK_HOME || join(process.env.HOME || "/root", ".heyhank"), "templates");
|
|
27
|
+
const STORE_FILE = join(DATA_DIR, "templates.json");
|
|
28
|
+
|
|
29
|
+
function ensureDir() {
|
|
30
|
+
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function load(): Template[] {
|
|
34
|
+
ensureDir();
|
|
35
|
+
if (!existsSync(STORE_FILE)) return [];
|
|
36
|
+
try { return JSON.parse(readFileSync(STORE_FILE, "utf-8")); }
|
|
37
|
+
catch { return []; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function save(templates: Template[]) {
|
|
41
|
+
ensureDir();
|
|
42
|
+
atomicWriteFileSync(STORE_FILE, JSON.stringify(templates, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function listTemplates(category?: string): Template[] {
|
|
46
|
+
let templates = load();
|
|
47
|
+
if (category) templates = templates.filter(t => t.category === category);
|
|
48
|
+
return templates.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createTemplate(name: string, content: string, category: string, variables?: TemplateVariable[], tags?: string[]): Template {
|
|
52
|
+
const templates = load();
|
|
53
|
+
|
|
54
|
+
// Auto-detect variables from {{...}} placeholders if not provided
|
|
55
|
+
const detectedVars = [...content.matchAll(/\{\{(\w+)\}\}/g)].map(m => m[1]);
|
|
56
|
+
const vars: TemplateVariable[] = variables || detectedVars.map(v => ({ name: v, required: true }));
|
|
57
|
+
|
|
58
|
+
const template: Template = {
|
|
59
|
+
id: randomUUID(),
|
|
60
|
+
name,
|
|
61
|
+
category,
|
|
62
|
+
content,
|
|
63
|
+
variables: vars,
|
|
64
|
+
tags: tags || [],
|
|
65
|
+
createdAt: new Date().toISOString(),
|
|
66
|
+
updatedAt: new Date().toISOString(),
|
|
67
|
+
usageCount: 0
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
templates.push(template);
|
|
71
|
+
save(templates);
|
|
72
|
+
return template;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getTemplate(id: string): Template | null {
|
|
76
|
+
return load().find(t => t.id === id) || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function updateTemplate(id: string, patch: Partial<Pick<Template, "name" | "content" | "category" | "variables" | "tags">>): Template | null {
|
|
80
|
+
const templates = load();
|
|
81
|
+
const idx = templates.findIndex(t => t.id === id);
|
|
82
|
+
if (idx === -1) return null;
|
|
83
|
+
|
|
84
|
+
if (patch.name !== undefined) templates[idx].name = patch.name;
|
|
85
|
+
if (patch.content !== undefined) templates[idx].content = patch.content;
|
|
86
|
+
if (patch.category !== undefined) templates[idx].category = patch.category;
|
|
87
|
+
if (patch.variables !== undefined) templates[idx].variables = patch.variables;
|
|
88
|
+
if (patch.tags !== undefined) templates[idx].tags = patch.tags;
|
|
89
|
+
templates[idx].updatedAt = new Date().toISOString();
|
|
90
|
+
|
|
91
|
+
save(templates);
|
|
92
|
+
return templates[idx];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function deleteTemplate(id: string): boolean {
|
|
96
|
+
const templates = load();
|
|
97
|
+
const idx = templates.findIndex(t => t.id === id);
|
|
98
|
+
if (idx === -1) return false;
|
|
99
|
+
templates.splice(idx, 1);
|
|
100
|
+
save(templates);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function useTemplate(id: string, variables: Record<string, string>): { result: string; template: Template } | null {
|
|
105
|
+
const templates = load();
|
|
106
|
+
const idx = templates.findIndex(t => t.id === id);
|
|
107
|
+
if (idx === -1) return null;
|
|
108
|
+
|
|
109
|
+
let result = templates[idx].content;
|
|
110
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
111
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update usage stats
|
|
115
|
+
templates[idx].usageCount++;
|
|
116
|
+
templates[idx].lastUsed = new Date().toISOString();
|
|
117
|
+
save(templates);
|
|
118
|
+
|
|
119
|
+
return { result, template: templates[idx] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function searchTemplates(query: string): Template[] {
|
|
123
|
+
const q = query.toLowerCase();
|
|
124
|
+
return load().filter(t =>
|
|
125
|
+
t.name.toLowerCase().includes(q) ||
|
|
126
|
+
t.category.toLowerCase().includes(q) ||
|
|
127
|
+
t.tags.some(tag => tag.toLowerCase().includes(q)) ||
|
|
128
|
+
t.content.toLowerCase().includes(q)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function listCategories(): string[] {
|
|
133
|
+
return [...new Set(load().map(t => t.category))].sort();
|
|
134
|
+
}
|