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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -10
  3. package/bin/cli.ts +7 -7
  4. package/bin/ctl.ts +42 -42
  5. package/dist/assets/{AgentsPage-BPhirnCe.js → AgentsPage-B-AAmsMK.js} +3 -3
  6. package/dist/assets/AssistantPage-BV1Mfwdt.js +2 -0
  7. package/dist/assets/BusinessPage-tLpNEz19.js +1 -0
  8. package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-B-K_n3Jg.js} +1 -1
  9. package/dist/assets/HelpPage-Bhf_j6Xr.js +1 -0
  10. package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-DAMjs9tM.js} +1 -1
  11. package/dist/assets/JarvisHUD-C_TGXCCn.js +120 -0
  12. package/dist/assets/MediaPage-C48HTTrt.js +1 -0
  13. package/dist/assets/MemoryPage-JkC-qtgp.js +1 -0
  14. package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-AUo7tNnE.js} +1 -1
  15. package/dist/assets/{Playground-Fc5cdc5p.js → Playground-AzNMsRBL.js} +1 -1
  16. package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-DpE_2sX3.js} +1 -1
  17. package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-C2RQOs6p.js} +2 -2
  18. package/dist/assets/RunsPage-B9UOyO79.js +1 -0
  19. package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-jHvYjwfh.js} +1 -1
  20. package/dist/assets/SettingsPage-BBJax6gt.js +51 -0
  21. package/dist/assets/SkillsMarketplace-IjmjfdjD.js +1 -0
  22. package/dist/assets/SocialMediaPage-DoPZHhr2.js +10 -0
  23. package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DDEY7ckO.js} +1 -1
  24. package/dist/assets/TelephonyPage-OPNBZYKt.js +9 -0
  25. package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-BjMbHHW3.js} +1 -1
  26. package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
  27. package/dist/assets/{index-CEqZnThB.js → index-BgYM4wXw.js} +94 -93
  28. package/dist/assets/index-BkjSoVgn.css +32 -0
  29. package/dist/assets/sw-register-C7NOHtIu.js +1 -0
  30. package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
  31. package/dist/index.html +2 -2
  32. package/dist/sw.js +1 -1
  33. package/package.json +6 -1
  34. package/server/agent-executor.ts +37 -2
  35. package/server/agent-store.ts +3 -3
  36. package/server/agent-types.ts +11 -0
  37. package/server/assistant-store.ts +232 -6
  38. package/server/auth-manager.ts +9 -0
  39. package/server/cache-headers.ts +1 -1
  40. package/server/calendar-service.ts +10 -0
  41. package/server/ceo/document-store.ts +129 -0
  42. package/server/ceo/finance-store.ts +343 -0
  43. package/server/ceo/kpi-store.ts +208 -0
  44. package/server/ceo/memory-import.ts +277 -0
  45. package/server/ceo/news-store.ts +208 -0
  46. package/server/ceo/template-store.ts +134 -0
  47. package/server/ceo/time-tracking-store.ts +227 -0
  48. package/server/claude-auth-monitor.ts +128 -0
  49. package/server/claude-code-worker.ts +86 -0
  50. package/server/claude-session-discovery.ts +74 -1
  51. package/server/cli-launcher.ts +32 -10
  52. package/server/codex-adapter.ts +2 -2
  53. package/server/codex-ws-proxy.cjs +1 -1
  54. package/server/container-manager.ts +4 -4
  55. package/server/content-intelligence/content-engine.ts +1112 -0
  56. package/server/content-intelligence/platform-knowledge.ts +870 -0
  57. package/server/cron-store.ts +3 -3
  58. package/server/embedding-service.ts +49 -0
  59. package/server/event-bus-types.ts +13 -0
  60. package/server/federation/node-store.ts +5 -4
  61. package/server/fs-utils.ts +28 -1
  62. package/server/hank-notifications-store.ts +91 -0
  63. package/server/hank-tool-executor.ts +1835 -0
  64. package/server/hank-tools.ts +2107 -0
  65. package/server/image-pull-manager.ts +2 -2
  66. package/server/index.ts +25 -2
  67. package/server/llm-providers-streaming.ts +541 -0
  68. package/server/llm-providers.ts +12 -0
  69. package/server/marketplace.ts +249 -0
  70. package/server/mcp-registry.ts +158 -0
  71. package/server/memory-service.ts +296 -0
  72. package/server/obsidian-sync.ts +184 -0
  73. package/server/provider-manager.ts +5 -2
  74. package/server/provider-registry.ts +12 -0
  75. package/server/reminder-scheduler.ts +37 -1
  76. package/server/routes/agent-routes.ts +2 -1
  77. package/server/routes/assistant-routes.ts +198 -5
  78. package/server/routes/ceo-finance-kpi-routes.ts +167 -0
  79. package/server/routes/ceo-news-time-routes.ts +137 -0
  80. package/server/routes/ceo-routes.ts +99 -0
  81. package/server/routes/content-routes.ts +116 -0
  82. package/server/routes/email-routes.ts +147 -0
  83. package/server/routes/env-routes.ts +3 -3
  84. package/server/routes/fs-routes.ts +12 -9
  85. package/server/routes/hank-chat-routes.ts +592 -0
  86. package/server/routes/llm-routes.ts +12 -0
  87. package/server/routes/marketplace-routes.ts +63 -0
  88. package/server/routes/media-routes.ts +1 -1
  89. package/server/routes/memory-routes.ts +127 -0
  90. package/server/routes/platform-routes.ts +14 -675
  91. package/server/routes/sandbox-routes.ts +1 -1
  92. package/server/routes/settings-routes.ts +51 -1
  93. package/server/routes/socialmedia-routes.ts +152 -2
  94. package/server/routes/system-routes.ts +2 -2
  95. package/server/routes/team-routes.ts +71 -0
  96. package/server/routes/telephony-routes.ts +98 -18
  97. package/server/routes.ts +36 -9
  98. package/server/session-creation-service.ts +2 -2
  99. package/server/session-orchestrator.ts +54 -2
  100. package/server/session-types.ts +2 -0
  101. package/server/settings-manager.ts +50 -2
  102. package/server/skill-discovery.ts +68 -0
  103. package/server/socialmedia/adapters/browser-adapter.ts +179 -0
  104. package/server/socialmedia/adapters/postiz-adapter.ts +291 -14
  105. package/server/socialmedia/manager.ts +234 -15
  106. package/server/socialmedia/store.ts +51 -1
  107. package/server/socialmedia/types.ts +35 -2
  108. package/server/socialview/browser-manager.ts +150 -0
  109. package/server/socialview/extractors.ts +1298 -0
  110. package/server/socialview/image-describe.ts +188 -0
  111. package/server/socialview/library.ts +119 -0
  112. package/server/socialview/poster.ts +276 -0
  113. package/server/socialview/routes.ts +371 -0
  114. package/server/socialview/style-analyzer.ts +187 -0
  115. package/server/socialview/style-profiles.ts +67 -0
  116. package/server/socialview/types.ts +166 -0
  117. package/server/socialview/vision.ts +127 -0
  118. package/server/socialview/vnc-manager.ts +110 -0
  119. package/server/style-injector.ts +135 -0
  120. package/server/team-service.ts +239 -0
  121. package/server/team-store.ts +75 -0
  122. package/server/team-types.ts +52 -0
  123. package/server/telephony/audio-bridge.ts +281 -35
  124. package/server/telephony/audio-recorder.ts +132 -0
  125. package/server/telephony/call-manager.ts +803 -104
  126. package/server/telephony/call-types.ts +67 -1
  127. package/server/telephony/esl-client.ts +319 -0
  128. package/server/telephony/freeswitch-sync.ts +155 -0
  129. package/server/telephony/phone-utils.ts +63 -0
  130. package/server/telephony/telephony-store.ts +9 -8
  131. package/server/url-validator.ts +82 -0
  132. package/server/vault-markdown.ts +317 -0
  133. package/server/vault-migration.ts +121 -0
  134. package/server/vault-store.ts +466 -0
  135. package/server/vault-watcher.ts +59 -0
  136. package/server/vector-store.ts +210 -0
  137. package/server/voice-pipeline/gemini-live-adapter.ts +97 -0
  138. package/server/voice-pipeline/greeting-cache.ts +200 -0
  139. package/server/voice-pipeline/manager.ts +249 -0
  140. package/server/voice-pipeline/pipeline.ts +335 -0
  141. package/server/voice-pipeline/providers/index.ts +47 -0
  142. package/server/voice-pipeline/providers/llm-internal.ts +527 -0
  143. package/server/voice-pipeline/providers/stt-google.ts +157 -0
  144. package/server/voice-pipeline/providers/tts-google.ts +126 -0
  145. package/server/voice-pipeline/types.ts +247 -0
  146. package/server/ws-bridge-types.ts +6 -1
  147. package/dist/assets/AssistantPage-DJ-cMQfb.js +0 -1
  148. package/dist/assets/HelpPage-DMfkzERp.js +0 -1
  149. package/dist/assets/MediaPage-CE5rdvkC.js +0 -1
  150. package/dist/assets/RunsPage-C5BZF5Rx.js +0 -1
  151. package/dist/assets/SettingsPage-DirhjQrJ.js +0 -51
  152. package/dist/assets/SocialMediaPage-DBuM28vD.js +0 -1
  153. package/dist/assets/TelephonyPage-x0VV0fOo.js +0 -1
  154. package/dist/assets/index-C8M_PUmX.css +0 -32
  155. package/dist/assets/sw-register-LSSpj6RU.js +0 -1
  156. 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, writeFileSync, mkdirSync, existsSync } from "node:fs";
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
- writeFileSync(join(ASSISTANT_DIR, filename), JSON.stringify(data, null, 2), "utf-8");
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
- // ─── Gemini Conversations ──────��─────────────────────────────────────────────
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;
@@ -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;
@@ -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
+ }