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
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
// ─── Assistant Store ──────────────────────────────────────────────────────────
|
|
2
2
|
// Persistent storage for personal assistant features: todos, notes, reminders.
|
|
3
3
|
// All data stored as JSON in ~/.heyhank/assistant/
|
|
4
|
+
// When an Obsidian vault is configured, delegates to vault-store.ts instead.
|
|
4
5
|
|
|
5
|
-
import { readFileSync,
|
|
6
|
+
import { readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { HEYHANK_HOME } from "./paths.js";
|
|
9
|
+
import { atomicWriteFileSync } from "./fs-utils.js";
|
|
10
|
+
import { getSettings } from "./settings-manager.js";
|
|
11
|
+
import * as vaultStore from "./vault-store.js";
|
|
12
|
+
|
|
13
|
+
function useVault(): boolean {
|
|
14
|
+
return !!getSettings().obsidianVaultPath;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
const ASSISTANT_DIR = join(HEYHANK_HOME, "assistant");
|
|
10
18
|
|
|
@@ -27,7 +35,7 @@ function readJson<T>(filename: string, fallback: T): T {
|
|
|
27
35
|
|
|
28
36
|
function writeJson(filename: string, data: unknown): void {
|
|
29
37
|
ensureDir();
|
|
30
|
-
|
|
38
|
+
atomicWriteFileSync(join(ASSISTANT_DIR, filename), JSON.stringify(data, null, 2));
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
// ─── Todos ────────────────────────────────────────────────────────────────────
|
|
@@ -40,6 +48,10 @@ export interface Todo {
|
|
|
40
48
|
createdAt: string;
|
|
41
49
|
doneAt?: string;
|
|
42
50
|
category?: string;
|
|
51
|
+
delegatedTo?: string;
|
|
52
|
+
dueDate?: string;
|
|
53
|
+
followUpDate?: string;
|
|
54
|
+
project?: string;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
function genId(): string {
|
|
@@ -47,6 +59,7 @@ function genId(): string {
|
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
export function listTodos(filter?: { done?: boolean; priority?: string; category?: string }): Todo[] {
|
|
62
|
+
if (useVault()) return vaultStore.listTodos(filter);
|
|
50
63
|
const todos = readJson<Todo[]>("todos.json", []);
|
|
51
64
|
return todos.filter((t) => {
|
|
52
65
|
if (filter?.done !== undefined && t.done !== filter.done) return false;
|
|
@@ -56,7 +69,8 @@ export function listTodos(filter?: { done?: boolean; priority?: string; category
|
|
|
56
69
|
});
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
export function addTodo(text: string, priority: string = "medium", category?: string): Todo {
|
|
72
|
+
export function addTodo(text: string, priority: string = "medium", category?: string, extra?: { delegatedTo?: string; dueDate?: string; followUpDate?: string; project?: string }): Todo {
|
|
73
|
+
if (useVault()) return vaultStore.addTodo(text, priority, category, extra);
|
|
60
74
|
const todos = readJson<Todo[]>("todos.json", []);
|
|
61
75
|
const todo: Todo = {
|
|
62
76
|
id: genId(),
|
|
@@ -65,6 +79,7 @@ export function addTodo(text: string, priority: string = "medium", category?: st
|
|
|
65
79
|
done: false,
|
|
66
80
|
createdAt: new Date().toISOString(),
|
|
67
81
|
category,
|
|
82
|
+
...extra,
|
|
68
83
|
};
|
|
69
84
|
todos.push(todo);
|
|
70
85
|
writeJson("todos.json", todos);
|
|
@@ -72,6 +87,7 @@ export function addTodo(text: string, priority: string = "medium", category?: st
|
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
export function completeTodo(id: string): Todo | null {
|
|
90
|
+
if (useVault()) return vaultStore.completeTodo(id);
|
|
75
91
|
const todos = readJson<Todo[]>("todos.json", []);
|
|
76
92
|
const todo = todos.find((t) => t.id === id);
|
|
77
93
|
if (!todo) return null;
|
|
@@ -82,6 +98,7 @@ export function completeTodo(id: string): Todo | null {
|
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
export function deleteTodo(id: string): boolean {
|
|
101
|
+
if (useVault()) return vaultStore.deleteTodo(id);
|
|
85
102
|
const todos = readJson<Todo[]>("todos.json", []);
|
|
86
103
|
const idx = todos.findIndex((t) => t.id === id);
|
|
87
104
|
if (idx === -1) return false;
|
|
@@ -90,7 +107,8 @@ export function deleteTodo(id: string): boolean {
|
|
|
90
107
|
return true;
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
export function updateTodo(id: string, patch: { text?: string; priority?: string; category?: string }): Todo | null {
|
|
110
|
+
export function updateTodo(id: string, patch: { text?: string; priority?: string; category?: string; delegatedTo?: string; dueDate?: string; followUpDate?: string; project?: string }): Todo | null {
|
|
111
|
+
if (useVault()) return vaultStore.updateTodo(id, patch);
|
|
94
112
|
const todos = readJson<Todo[]>("todos.json", []);
|
|
95
113
|
const todo = todos.find((t) => t.id === id);
|
|
96
114
|
if (!todo) return null;
|
|
@@ -99,10 +117,24 @@ export function updateTodo(id: string, patch: { text?: string; priority?: string
|
|
|
99
117
|
todo.priority = patch.priority as Todo["priority"];
|
|
100
118
|
}
|
|
101
119
|
if (patch.category !== undefined) todo.category = patch.category;
|
|
120
|
+
if (patch.delegatedTo !== undefined) todo.delegatedTo = patch.delegatedTo;
|
|
121
|
+
if (patch.dueDate !== undefined) todo.dueDate = patch.dueDate;
|
|
122
|
+
if (patch.followUpDate !== undefined) todo.followUpDate = patch.followUpDate;
|
|
123
|
+
if (patch.project !== undefined) todo.project = patch.project;
|
|
102
124
|
writeJson("todos.json", todos);
|
|
103
125
|
return todo;
|
|
104
126
|
}
|
|
105
127
|
|
|
128
|
+
export function listDelegations(person?: string): Todo[] {
|
|
129
|
+
const todos = listTodos();
|
|
130
|
+
const delegated = todos.filter((t) => t.delegatedTo && (!person || t.delegatedTo.toLowerCase() === person.toLowerCase()));
|
|
131
|
+
return delegated.sort((a, b) => {
|
|
132
|
+
if (!a.dueDate) return 1;
|
|
133
|
+
if (!b.dueDate) return -1;
|
|
134
|
+
return a.dueDate.localeCompare(b.dueDate);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
// ─── Notes ────────────────────────────────────────────────────────────────────
|
|
107
139
|
|
|
108
140
|
export interface Note {
|
|
@@ -115,6 +147,7 @@ export interface Note {
|
|
|
115
147
|
}
|
|
116
148
|
|
|
117
149
|
export function listNotes(search?: string): Note[] {
|
|
150
|
+
if (useVault()) return vaultStore.listNotes(search);
|
|
118
151
|
const notes = readJson<Note[]>("notes.json", []);
|
|
119
152
|
if (!search) return notes;
|
|
120
153
|
const q = search.toLowerCase();
|
|
@@ -126,6 +159,7 @@ export function listNotes(search?: string): Note[] {
|
|
|
126
159
|
}
|
|
127
160
|
|
|
128
161
|
export function addNote(title: string, content: string, tags: string[] = []): Note {
|
|
162
|
+
if (useVault()) return vaultStore.addNote(title, content, tags);
|
|
129
163
|
const notes = readJson<Note[]>("notes.json", []);
|
|
130
164
|
const note: Note = {
|
|
131
165
|
id: genId(),
|
|
@@ -141,11 +175,13 @@ export function addNote(title: string, content: string, tags: string[] = []): No
|
|
|
141
175
|
}
|
|
142
176
|
|
|
143
177
|
export function getNote(id: string): Note | null {
|
|
178
|
+
if (useVault()) return vaultStore.getNote(id);
|
|
144
179
|
const notes = readJson<Note[]>("notes.json", []);
|
|
145
180
|
return notes.find((n) => n.id === id) || null;
|
|
146
181
|
}
|
|
147
182
|
|
|
148
183
|
export function updateNote(id: string, patch: { title?: string; content?: string; tags?: string[] }): Note | null {
|
|
184
|
+
if (useVault()) return vaultStore.updateNote(id, patch);
|
|
149
185
|
const notes = readJson<Note[]>("notes.json", []);
|
|
150
186
|
const note = notes.find((n) => n.id === id);
|
|
151
187
|
if (!note) return null;
|
|
@@ -158,6 +194,7 @@ export function updateNote(id: string, patch: { title?: string; content?: string
|
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
export function deleteNote(id: string): boolean {
|
|
197
|
+
if (useVault()) return vaultStore.deleteNote(id);
|
|
161
198
|
const notes = readJson<Note[]>("notes.json", []);
|
|
162
199
|
const idx = notes.findIndex((n) => n.id === id);
|
|
163
200
|
if (idx === -1) return false;
|
|
@@ -174,14 +211,17 @@ export interface Reminder {
|
|
|
174
211
|
triggerAt: string; // ISO datetime
|
|
175
212
|
fired: boolean;
|
|
176
213
|
createdAt: string;
|
|
214
|
+
calendarEventUid?: string;
|
|
177
215
|
}
|
|
178
216
|
|
|
179
217
|
export function listReminders(includeFired = false): Reminder[] {
|
|
218
|
+
if (useVault()) return vaultStore.listReminders(includeFired);
|
|
180
219
|
const reminders = readJson<Reminder[]>("reminders.json", []);
|
|
181
220
|
return includeFired ? reminders : reminders.filter((r) => !r.fired);
|
|
182
221
|
}
|
|
183
222
|
|
|
184
|
-
export function addReminder(text: string, triggerAt: string): Reminder {
|
|
223
|
+
export function addReminder(text: string, triggerAt: string, calendarEventUid?: string): Reminder {
|
|
224
|
+
if (useVault()) return vaultStore.addReminder(text, triggerAt, calendarEventUid);
|
|
185
225
|
const reminders = readJson<Reminder[]>("reminders.json", []);
|
|
186
226
|
const reminder: Reminder = {
|
|
187
227
|
id: genId(),
|
|
@@ -189,13 +229,27 @@ export function addReminder(text: string, triggerAt: string): Reminder {
|
|
|
189
229
|
triggerAt,
|
|
190
230
|
fired: false,
|
|
191
231
|
createdAt: new Date().toISOString(),
|
|
232
|
+
...(calendarEventUid ? { calendarEventUid } : {}),
|
|
192
233
|
};
|
|
193
234
|
reminders.push(reminder);
|
|
194
235
|
writeJson("reminders.json", reminders);
|
|
195
236
|
return reminder;
|
|
196
237
|
}
|
|
197
238
|
|
|
239
|
+
export function updateReminder(id: string, updates: { text?: string; triggerAt?: string; calendarEventUid?: string }): Reminder | null {
|
|
240
|
+
if (useVault()) return vaultStore.updateReminder(id, updates);
|
|
241
|
+
const reminders = readJson<Reminder[]>("reminders.json", []);
|
|
242
|
+
const r = reminders.find((rem) => rem.id === id);
|
|
243
|
+
if (!r) return null;
|
|
244
|
+
if (updates.text !== undefined) r.text = updates.text;
|
|
245
|
+
if (updates.triggerAt !== undefined) r.triggerAt = updates.triggerAt;
|
|
246
|
+
if (updates.calendarEventUid !== undefined) r.calendarEventUid = updates.calendarEventUid;
|
|
247
|
+
writeJson("reminders.json", reminders);
|
|
248
|
+
return r;
|
|
249
|
+
}
|
|
250
|
+
|
|
198
251
|
export function fireReminder(id: string): Reminder | null {
|
|
252
|
+
if (useVault()) return vaultStore.fireReminder(id);
|
|
199
253
|
const reminders = readJson<Reminder[]>("reminders.json", []);
|
|
200
254
|
const r = reminders.find((rem) => rem.id === id);
|
|
201
255
|
if (!r) return null;
|
|
@@ -205,6 +259,7 @@ export function fireReminder(id: string): Reminder | null {
|
|
|
205
259
|
}
|
|
206
260
|
|
|
207
261
|
export function deleteReminder(id: string): boolean {
|
|
262
|
+
if (useVault()) return vaultStore.deleteReminder(id);
|
|
208
263
|
const reminders = readJson<Reminder[]>("reminders.json", []);
|
|
209
264
|
const idx = reminders.findIndex((r) => r.id === id);
|
|
210
265
|
if (idx === -1) return false;
|
|
@@ -215,12 +270,179 @@ export function deleteReminder(id: string): boolean {
|
|
|
215
270
|
|
|
216
271
|
/** Get all reminders that should have fired by now */
|
|
217
272
|
export function getDueReminders(): Reminder[] {
|
|
273
|
+
if (useVault()) return vaultStore.getDueReminders();
|
|
218
274
|
const now = new Date().toISOString();
|
|
219
275
|
const reminders = readJson<Reminder[]>("reminders.json", []);
|
|
220
276
|
return reminders.filter((r) => !r.fired && r.triggerAt <= now);
|
|
221
277
|
}
|
|
222
278
|
|
|
223
|
-
// ───
|
|
279
|
+
// ─── Contacts/CRM ────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
export interface ContactInteraction {
|
|
282
|
+
date: string;
|
|
283
|
+
type: "call" | "email" | "meeting" | "note";
|
|
284
|
+
summary: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface Contact {
|
|
288
|
+
id: string;
|
|
289
|
+
name: string;
|
|
290
|
+
company?: string;
|
|
291
|
+
email?: string;
|
|
292
|
+
phone?: string;
|
|
293
|
+
notes?: string;
|
|
294
|
+
tags: string[];
|
|
295
|
+
lastContactDate?: string;
|
|
296
|
+
interactions: ContactInteraction[];
|
|
297
|
+
createdAt: string;
|
|
298
|
+
updatedAt: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function listContacts(search?: string): Contact[] {
|
|
302
|
+
if (useVault()) return vaultStore.listContacts(search);
|
|
303
|
+
const contacts = readJson<Contact[]>("contacts.json", []);
|
|
304
|
+
if (!search) return contacts;
|
|
305
|
+
const q = search.toLowerCase();
|
|
306
|
+
return contacts.filter((c) =>
|
|
307
|
+
c.name.toLowerCase().includes(q) ||
|
|
308
|
+
(c.company && c.company.toLowerCase().includes(q)) ||
|
|
309
|
+
(c.email && c.email.toLowerCase().includes(q)) ||
|
|
310
|
+
(c.phone && c.phone.includes(q)) ||
|
|
311
|
+
c.tags.some((t) => t.toLowerCase().includes(q))
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function addContact(name: string, company?: string, email?: string, phone?: string, notes?: string, tags: string[] = []): Contact {
|
|
316
|
+
if (useVault()) return vaultStore.addContact(name, company, email, phone, notes, tags);
|
|
317
|
+
const contacts = readJson<Contact[]>("contacts.json", []);
|
|
318
|
+
const contact: Contact = {
|
|
319
|
+
id: genId(),
|
|
320
|
+
name,
|
|
321
|
+
company,
|
|
322
|
+
email,
|
|
323
|
+
phone,
|
|
324
|
+
notes,
|
|
325
|
+
tags,
|
|
326
|
+
interactions: [],
|
|
327
|
+
createdAt: new Date().toISOString(),
|
|
328
|
+
updatedAt: new Date().toISOString(),
|
|
329
|
+
};
|
|
330
|
+
contacts.push(contact);
|
|
331
|
+
writeJson("contacts.json", contacts);
|
|
332
|
+
return contact;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function getContact(id: string): Contact | null {
|
|
336
|
+
if (useVault()) return vaultStore.getContact(id);
|
|
337
|
+
const contacts = readJson<Contact[]>("contacts.json", []);
|
|
338
|
+
return contacts.find((c) => c.id === id) || null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function updateContact(id: string, patch: { name?: string; company?: string; email?: string; phone?: string; notes?: string; tags?: string[] }): Contact | null {
|
|
342
|
+
if (useVault()) return vaultStore.updateContact(id, patch);
|
|
343
|
+
const contacts = readJson<Contact[]>("contacts.json", []);
|
|
344
|
+
const contact = contacts.find((c) => c.id === id);
|
|
345
|
+
if (!contact) return null;
|
|
346
|
+
if (patch.name) contact.name = patch.name;
|
|
347
|
+
if (patch.company !== undefined) contact.company = patch.company;
|
|
348
|
+
if (patch.email !== undefined) contact.email = patch.email;
|
|
349
|
+
if (patch.phone !== undefined) contact.phone = patch.phone;
|
|
350
|
+
if (patch.notes !== undefined) contact.notes = patch.notes;
|
|
351
|
+
if (patch.tags) contact.tags = patch.tags;
|
|
352
|
+
contact.updatedAt = new Date().toISOString();
|
|
353
|
+
writeJson("contacts.json", contacts);
|
|
354
|
+
return contact;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function deleteContact(id: string): boolean {
|
|
358
|
+
if (useVault()) return vaultStore.deleteContact(id);
|
|
359
|
+
const contacts = readJson<Contact[]>("contacts.json", []);
|
|
360
|
+
const idx = contacts.findIndex((c) => c.id === id);
|
|
361
|
+
if (idx === -1) return false;
|
|
362
|
+
contacts.splice(idx, 1);
|
|
363
|
+
writeJson("contacts.json", contacts);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function logInteraction(contactId: string, interaction: Omit<ContactInteraction, "date">): Contact | null {
|
|
368
|
+
if (useVault()) return vaultStore.logInteraction(contactId, interaction);
|
|
369
|
+
const contacts = readJson<Contact[]>("contacts.json", []);
|
|
370
|
+
const contact = contacts.find((c) => c.id === contactId);
|
|
371
|
+
if (!contact) return null;
|
|
372
|
+
const entry: ContactInteraction = {
|
|
373
|
+
date: new Date().toISOString(),
|
|
374
|
+
type: interaction.type,
|
|
375
|
+
summary: interaction.summary,
|
|
376
|
+
};
|
|
377
|
+
contact.interactions.push(entry);
|
|
378
|
+
contact.lastContactDate = entry.date;
|
|
379
|
+
contact.updatedAt = new Date().toISOString();
|
|
380
|
+
writeJson("contacts.json", contacts);
|
|
381
|
+
return contact;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ─── Decisions ───────────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
export interface Decision {
|
|
387
|
+
id: string;
|
|
388
|
+
title: string;
|
|
389
|
+
context: string;
|
|
390
|
+
decision: string;
|
|
391
|
+
alternatives: string[];
|
|
392
|
+
reasoning: string;
|
|
393
|
+
tags: string[];
|
|
394
|
+
createdAt: string;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function listDecisions(search?: string): Decision[] {
|
|
398
|
+
if (useVault()) return vaultStore.listDecisions(search);
|
|
399
|
+
const decisions = readJson<Decision[]>("decisions.json", []);
|
|
400
|
+
if (!search) return decisions;
|
|
401
|
+
const q = search.toLowerCase();
|
|
402
|
+
return decisions.filter((d) =>
|
|
403
|
+
d.title.toLowerCase().includes(q) ||
|
|
404
|
+
d.context.toLowerCase().includes(q) ||
|
|
405
|
+
d.decision.toLowerCase().includes(q) ||
|
|
406
|
+
d.reasoning.toLowerCase().includes(q) ||
|
|
407
|
+
d.tags.some((t) => t.toLowerCase().includes(q))
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function addDecision(title: string, context: string, decision: string, alternatives: string[] = [], reasoning: string = "", tags: string[] = []): Decision {
|
|
412
|
+
if (useVault()) return vaultStore.addDecision(title, context, decision, alternatives, reasoning, tags);
|
|
413
|
+
const decisions = readJson<Decision[]>("decisions.json", []);
|
|
414
|
+
const entry: Decision = {
|
|
415
|
+
id: genId(),
|
|
416
|
+
title,
|
|
417
|
+
context,
|
|
418
|
+
decision,
|
|
419
|
+
alternatives,
|
|
420
|
+
reasoning,
|
|
421
|
+
tags,
|
|
422
|
+
createdAt: new Date().toISOString(),
|
|
423
|
+
};
|
|
424
|
+
decisions.push(entry);
|
|
425
|
+
writeJson("decisions.json", decisions);
|
|
426
|
+
return entry;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function getDecision(id: string): Decision | null {
|
|
430
|
+
if (useVault()) return vaultStore.getDecision(id);
|
|
431
|
+
const decisions = readJson<Decision[]>("decisions.json", []);
|
|
432
|
+
return decisions.find((d) => d.id === id) || null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function deleteDecision(id: string): boolean {
|
|
436
|
+
if (useVault()) return vaultStore.deleteDecision(id);
|
|
437
|
+
const decisions = readJson<Decision[]>("decisions.json", []);
|
|
438
|
+
const idx = decisions.findIndex((d) => d.id === id);
|
|
439
|
+
if (idx === -1) return false;
|
|
440
|
+
decisions.splice(idx, 1);
|
|
441
|
+
writeJson("decisions.json", decisions);
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ─── Gemini Conversations ────────────────────────────────────────────────────
|
|
224
446
|
|
|
225
447
|
export interface GeminiConversation {
|
|
226
448
|
id: string;
|
|
@@ -231,6 +453,7 @@ export interface GeminiConversation {
|
|
|
231
453
|
}
|
|
232
454
|
|
|
233
455
|
export function listGeminiConversations(): GeminiConversation[] {
|
|
456
|
+
if (useVault()) return vaultStore.listGeminiConversations();
|
|
234
457
|
return readJson<GeminiConversation[]>("gemini-conversations.json", [])
|
|
235
458
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
236
459
|
}
|
|
@@ -239,6 +462,7 @@ export function saveGeminiConversation(
|
|
|
239
462
|
messages: Array<{ role: "user" | "gemini" | "system"; text: string; ts: number }>,
|
|
240
463
|
duration?: number,
|
|
241
464
|
): GeminiConversation {
|
|
465
|
+
if (useVault()) return vaultStore.saveGeminiConversation(messages, duration);
|
|
242
466
|
const convos = readJson<GeminiConversation[]>("gemini-conversations.json", []);
|
|
243
467
|
// Generate title from first user message
|
|
244
468
|
const firstUser = messages.find((m) => m.role === "user");
|
|
@@ -258,11 +482,13 @@ export function saveGeminiConversation(
|
|
|
258
482
|
}
|
|
259
483
|
|
|
260
484
|
export function getGeminiConversation(id: string): GeminiConversation | null {
|
|
485
|
+
if (useVault()) return vaultStore.getGeminiConversation(id);
|
|
261
486
|
const convos = readJson<GeminiConversation[]>("gemini-conversations.json", []);
|
|
262
487
|
return convos.find((c) => c.id === id) || null;
|
|
263
488
|
}
|
|
264
489
|
|
|
265
490
|
export function deleteGeminiConversation(id: string): boolean {
|
|
491
|
+
if (useVault()) return vaultStore.deleteGeminiConversation(id);
|
|
266
492
|
const convos = readJson<GeminiConversation[]>("gemini-conversations.json", []);
|
|
267
493
|
const idx = convos.findIndex((c) => c.id === id);
|
|
268
494
|
if (idx === -1) return false;
|
package/server/auth-manager.ts
CHANGED
|
@@ -144,6 +144,15 @@ export function regenerateToken(): string {
|
|
|
144
144
|
return token;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Check whether a token file already exists on disk (i.e. not a fresh first-run).
|
|
149
|
+
* Used to decide whether to print the token to console on startup.
|
|
150
|
+
*/
|
|
151
|
+
export function isTokenPersisted(): boolean {
|
|
152
|
+
if (process.env.HEYHANK_AUTH_TOKEN || process.env.COMPANION_AUTH_TOKEN) return true;
|
|
153
|
+
return existsSync(AUTH_FILE);
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
/** Reset cached state — for testing only */
|
|
148
157
|
export function _resetForTest(): void {
|
|
149
158
|
cachedToken = null;
|
package/server/cache-headers.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function cacheControlMiddleware(): MiddlewareHandler {
|
|
|
31
31
|
|
|
32
32
|
// index.html (served for / and /index.html): must be fresh
|
|
33
33
|
if (path === "/" || path === "/index.html") {
|
|
34
|
-
c.header("Cache-Control", "no-cache");
|
|
34
|
+
c.header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -291,6 +291,7 @@ export async function createEvent(
|
|
|
291
291
|
end: string; // ISO datetime or YYYY-MM-DD for all-day
|
|
292
292
|
allDay?: boolean;
|
|
293
293
|
calendarUrl?: string;
|
|
294
|
+
alarm?: number; // minutes before event to trigger alarm (0 = at event time)
|
|
294
295
|
},
|
|
295
296
|
): Promise<{ success: boolean; uid: string }> {
|
|
296
297
|
const client = await createClient(account);
|
|
@@ -330,6 +331,15 @@ export async function createEvent(
|
|
|
330
331
|
if (event.description) lines.push(`DESCRIPTION:${escapeICS(event.description)}`);
|
|
331
332
|
if (event.location) lines.push(`LOCATION:${escapeICS(event.location)}`);
|
|
332
333
|
lines.push(`DTSTAMP:${new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "")}`);
|
|
334
|
+
if (event.alarm !== undefined) {
|
|
335
|
+
lines.push(
|
|
336
|
+
"BEGIN:VALARM",
|
|
337
|
+
"ACTION:DISPLAY",
|
|
338
|
+
`DESCRIPTION:${escapeICS(event.summary)}`,
|
|
339
|
+
`TRIGGER:${event.alarm === 0 ? "PT0S" : `-PT${event.alarm}M`}`,
|
|
340
|
+
"END:VALARM",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
333
343
|
lines.push("END:VEVENT", "END:VCALENDAR");
|
|
334
344
|
|
|
335
345
|
const icsData = lines.join("\r\n");
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync, statSync } from "fs";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { atomicWriteFileSync } from "../fs-utils.js";
|
|
5
|
+
|
|
6
|
+
export interface Document {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
fileType: string; // pdf, txt, md, docx, etc.
|
|
10
|
+
size: number; // bytes
|
|
11
|
+
path: string; // relative storage path
|
|
12
|
+
tags: string[];
|
|
13
|
+
folder: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
summary?: string; // AI-generated summary
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DocumentMetadata {
|
|
20
|
+
documents: Document[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DATA_DIR = join(process.env.HEYHANK_HOME || join(process.env.HOME || "/root", ".heyhank"), "documents");
|
|
24
|
+
const META_FILE = join(DATA_DIR, "_metadata.json");
|
|
25
|
+
|
|
26
|
+
function ensureDir() {
|
|
27
|
+
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadMeta(): DocumentMetadata {
|
|
31
|
+
ensureDir();
|
|
32
|
+
if (!existsSync(META_FILE)) return { documents: [] };
|
|
33
|
+
try { return JSON.parse(readFileSync(META_FILE, "utf-8")); }
|
|
34
|
+
catch { return { documents: [] }; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveMeta(meta: DocumentMetadata) {
|
|
38
|
+
ensureDir();
|
|
39
|
+
atomicWriteFileSync(META_FILE, JSON.stringify(meta, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function listDocuments(folder?: string, tag?: string): Document[] {
|
|
43
|
+
const meta = loadMeta();
|
|
44
|
+
let docs = meta.documents;
|
|
45
|
+
if (folder) docs = docs.filter(d => d.folder === folder);
|
|
46
|
+
if (tag) docs = docs.filter(d => d.tags.includes(tag));
|
|
47
|
+
return docs.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function addDocument(title: string, content: string, fileType: string, folder?: string, tags?: string[], summary?: string): Document {
|
|
51
|
+
const meta = loadMeta();
|
|
52
|
+
const id = randomUUID();
|
|
53
|
+
const filename = `${id}.${fileType}`;
|
|
54
|
+
const filePath = join(DATA_DIR, filename);
|
|
55
|
+
|
|
56
|
+
writeFileSync(filePath, content);
|
|
57
|
+
const stat = statSync(filePath);
|
|
58
|
+
|
|
59
|
+
const doc: Document = {
|
|
60
|
+
id,
|
|
61
|
+
title,
|
|
62
|
+
fileType,
|
|
63
|
+
size: stat.size,
|
|
64
|
+
path: filename,
|
|
65
|
+
tags: tags || [],
|
|
66
|
+
folder: folder || "General",
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
updatedAt: new Date().toISOString(),
|
|
69
|
+
summary
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
meta.documents.push(doc);
|
|
73
|
+
saveMeta(meta);
|
|
74
|
+
return doc;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getDocument(id: string): { meta: Document; content: string } | null {
|
|
78
|
+
const meta = loadMeta();
|
|
79
|
+
const doc = meta.documents.find(d => d.id === id);
|
|
80
|
+
if (!doc) return null;
|
|
81
|
+
const filePath = join(DATA_DIR, doc.path);
|
|
82
|
+
if (!existsSync(filePath)) return null;
|
|
83
|
+
const content = readFileSync(filePath, "utf-8");
|
|
84
|
+
return { meta: doc, content };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function updateDocument(id: string, patch: Partial<Pick<Document, "title" | "tags" | "folder" | "summary">>): Document | null {
|
|
88
|
+
const meta = loadMeta();
|
|
89
|
+
const idx = meta.documents.findIndex(d => d.id === id);
|
|
90
|
+
if (idx === -1) return null;
|
|
91
|
+
|
|
92
|
+
if (patch.title !== undefined) meta.documents[idx].title = patch.title;
|
|
93
|
+
if (patch.tags !== undefined) meta.documents[idx].tags = patch.tags;
|
|
94
|
+
if (patch.folder !== undefined) meta.documents[idx].folder = patch.folder;
|
|
95
|
+
if (patch.summary !== undefined) meta.documents[idx].summary = patch.summary;
|
|
96
|
+
meta.documents[idx].updatedAt = new Date().toISOString();
|
|
97
|
+
|
|
98
|
+
saveMeta(meta);
|
|
99
|
+
return meta.documents[idx];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function deleteDocument(id: string): boolean {
|
|
103
|
+
const meta = loadMeta();
|
|
104
|
+
const idx = meta.documents.findIndex(d => d.id === id);
|
|
105
|
+
if (idx === -1) return false;
|
|
106
|
+
|
|
107
|
+
const filePath = join(DATA_DIR, meta.documents[idx].path);
|
|
108
|
+
if (existsSync(filePath)) unlinkSync(filePath);
|
|
109
|
+
|
|
110
|
+
meta.documents.splice(idx, 1);
|
|
111
|
+
saveMeta(meta);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function searchDocuments(query: string): Document[] {
|
|
116
|
+
const meta = loadMeta();
|
|
117
|
+
const q = query.toLowerCase();
|
|
118
|
+
return meta.documents.filter(d =>
|
|
119
|
+
d.title.toLowerCase().includes(q) ||
|
|
120
|
+
d.tags.some(t => t.toLowerCase().includes(q)) ||
|
|
121
|
+
d.folder.toLowerCase().includes(q) ||
|
|
122
|
+
(d.summary && d.summary.toLowerCase().includes(q))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function listFolders(): string[] {
|
|
127
|
+
const meta = loadMeta();
|
|
128
|
+
return [...new Set(meta.documents.map(d => d.folder))].sort();
|
|
129
|
+
}
|