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,466 @@
|
|
|
1
|
+
// ─── Vault Store ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Filesystem-backed data store using Obsidian vault as primary storage.
|
|
3
|
+
// Each entity type maps to a subfolder in {vault}/HeyHank/.
|
|
4
|
+
// When obsidianVaultPath is configured, this replaces the JSON-based assistant-store.
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, renameSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { getSettings } from "./settings-manager.js";
|
|
9
|
+
import * as md from "./vault-markdown.js";
|
|
10
|
+
import type { Todo, Note, Reminder, GeminiConversation, Contact, ContactInteraction, Decision } from "./assistant-store.js";
|
|
11
|
+
import type { CallData } from "./vault-markdown.js";
|
|
12
|
+
|
|
13
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function getDir(subfolder: string): string | null {
|
|
16
|
+
const vaultPath = getSettings().obsidianVaultPath;
|
|
17
|
+
if (!vaultPath) return null;
|
|
18
|
+
const dir = join(vaultPath, "HeyHank", subfolder);
|
|
19
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readAllMd(dir: string, prefix: string): Array<{ filename: string; content: string }> {
|
|
24
|
+
return readdirSync(dir)
|
|
25
|
+
.filter((f) => f.startsWith(prefix) && f.endsWith(".md"))
|
|
26
|
+
.map((f) => ({ filename: f, content: readFileSync(join(dir, f), "utf-8") }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function genId(): string {
|
|
30
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Atomic write: write to .tmp then rename */
|
|
34
|
+
function atomicWrite(filepath: string, content: string): void {
|
|
35
|
+
const tmp = filepath + ".tmp";
|
|
36
|
+
writeFileSync(tmp, content, "utf-8");
|
|
37
|
+
renameSync(tmp, filepath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function filenameForId(prefix: string, id: string): string {
|
|
41
|
+
return `${prefix}${id}.md`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findFile(dir: string, prefix: string, id: string): string | null {
|
|
45
|
+
const filepath = join(dir, filenameForId(prefix, id));
|
|
46
|
+
return existsSync(filepath) ? filepath : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Todos ───────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export function listTodos(filter?: { done?: boolean; priority?: string; category?: string }): Todo[] {
|
|
52
|
+
const dir = getDir("Todos");
|
|
53
|
+
if (!dir) return [];
|
|
54
|
+
const files = readAllMd(dir, "todo-");
|
|
55
|
+
const todos = files.map((f) => md.markdownToTodo(f.content));
|
|
56
|
+
return todos.filter((t) => {
|
|
57
|
+
if (filter?.done !== undefined && t.done !== filter.done) return false;
|
|
58
|
+
if (filter?.priority && t.priority !== filter.priority) return false;
|
|
59
|
+
if (filter?.category && t.category !== filter.category) return false;
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function addTodo(text: string, priority: string = "medium", category?: string, extra?: { delegatedTo?: string; dueDate?: string; followUpDate?: string; project?: string }): Todo {
|
|
65
|
+
const dir = getDir("Todos");
|
|
66
|
+
if (!dir) throw new Error("Obsidian vault not configured");
|
|
67
|
+
const todo: Todo = {
|
|
68
|
+
id: genId(),
|
|
69
|
+
text,
|
|
70
|
+
priority: (["high", "medium", "low"].includes(priority) ? priority : "medium") as Todo["priority"],
|
|
71
|
+
done: false,
|
|
72
|
+
createdAt: new Date().toISOString(),
|
|
73
|
+
category,
|
|
74
|
+
...extra,
|
|
75
|
+
};
|
|
76
|
+
atomicWrite(join(dir, filenameForId("todo-", todo.id)), md.todoToMarkdown(todo));
|
|
77
|
+
return todo;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function completeTodo(id: string): Todo | null {
|
|
81
|
+
const dir = getDir("Todos");
|
|
82
|
+
if (!dir) return null;
|
|
83
|
+
const filepath = findFile(dir, "todo-", id);
|
|
84
|
+
if (!filepath) return null;
|
|
85
|
+
const todo = md.markdownToTodo(readFileSync(filepath, "utf-8"));
|
|
86
|
+
todo.done = true;
|
|
87
|
+
todo.doneAt = new Date().toISOString();
|
|
88
|
+
atomicWrite(filepath, md.todoToMarkdown(todo));
|
|
89
|
+
return todo;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function updateTodo(id: string, updates: { text?: string; priority?: string; category?: string; done?: boolean; delegatedTo?: string; dueDate?: string; followUpDate?: string; project?: string }): Todo | null {
|
|
93
|
+
const dir = getDir("Todos");
|
|
94
|
+
if (!dir) return null;
|
|
95
|
+
const filepath = findFile(dir, "todo-", id);
|
|
96
|
+
if (!filepath) return null;
|
|
97
|
+
const todo = md.markdownToTodo(readFileSync(filepath, "utf-8"));
|
|
98
|
+
if (updates.text) todo.text = updates.text;
|
|
99
|
+
if (updates.priority && ["high", "medium", "low"].includes(updates.priority)) {
|
|
100
|
+
todo.priority = updates.priority as Todo["priority"];
|
|
101
|
+
}
|
|
102
|
+
if (updates.category !== undefined) todo.category = updates.category;
|
|
103
|
+
if (updates.delegatedTo !== undefined) todo.delegatedTo = updates.delegatedTo;
|
|
104
|
+
if (updates.dueDate !== undefined) todo.dueDate = updates.dueDate;
|
|
105
|
+
if (updates.followUpDate !== undefined) todo.followUpDate = updates.followUpDate;
|
|
106
|
+
if (updates.project !== undefined) todo.project = updates.project;
|
|
107
|
+
if (updates.done === true && !todo.done) {
|
|
108
|
+
todo.done = true;
|
|
109
|
+
todo.doneAt = new Date().toISOString();
|
|
110
|
+
} else if (updates.done === false) {
|
|
111
|
+
todo.done = false;
|
|
112
|
+
todo.doneAt = undefined;
|
|
113
|
+
}
|
|
114
|
+
atomicWrite(filepath, md.todoToMarkdown(todo));
|
|
115
|
+
return todo;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function deleteTodo(id: string): boolean {
|
|
119
|
+
const dir = getDir("Todos");
|
|
120
|
+
if (!dir) return false;
|
|
121
|
+
const filepath = findFile(dir, "todo-", id);
|
|
122
|
+
if (!filepath) return false;
|
|
123
|
+
unlinkSync(filepath);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Notes ───────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export function listNotes(search?: string): Note[] {
|
|
130
|
+
const dir = getDir("Notes");
|
|
131
|
+
if (!dir) return [];
|
|
132
|
+
const files = readAllMd(dir, "note-");
|
|
133
|
+
const notes = files.map((f) => md.markdownToNote(f.content));
|
|
134
|
+
if (!search) return notes;
|
|
135
|
+
const q = search.toLowerCase();
|
|
136
|
+
return notes.filter((n) =>
|
|
137
|
+
n.title.toLowerCase().includes(q) ||
|
|
138
|
+
n.content.toLowerCase().includes(q) ||
|
|
139
|
+
n.tags.some((t) => t.toLowerCase().includes(q)),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function addNote(title: string, content: string = "", tags: string[] = []): Note {
|
|
144
|
+
const dir = getDir("Notes");
|
|
145
|
+
if (!dir) throw new Error("Obsidian vault not configured");
|
|
146
|
+
const note: Note = {
|
|
147
|
+
id: genId(),
|
|
148
|
+
title,
|
|
149
|
+
content,
|
|
150
|
+
tags,
|
|
151
|
+
createdAt: new Date().toISOString(),
|
|
152
|
+
updatedAt: new Date().toISOString(),
|
|
153
|
+
};
|
|
154
|
+
atomicWrite(join(dir, filenameForId("note-", note.id)), md.noteToMarkdown(note));
|
|
155
|
+
return note;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getNote(id: string): Note | null {
|
|
159
|
+
const dir = getDir("Notes");
|
|
160
|
+
if (!dir) return null;
|
|
161
|
+
const filepath = findFile(dir, "note-", id);
|
|
162
|
+
if (!filepath) return null;
|
|
163
|
+
return md.markdownToNote(readFileSync(filepath, "utf-8"));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function updateNote(id: string, updates: { title?: string; content?: string; tags?: string[] }): Note | null {
|
|
167
|
+
const dir = getDir("Notes");
|
|
168
|
+
if (!dir) return null;
|
|
169
|
+
const filepath = findFile(dir, "note-", id);
|
|
170
|
+
if (!filepath) return null;
|
|
171
|
+
const note = md.markdownToNote(readFileSync(filepath, "utf-8"));
|
|
172
|
+
if (updates.title) note.title = updates.title;
|
|
173
|
+
if (updates.content) note.content = updates.content;
|
|
174
|
+
if (updates.tags) note.tags = updates.tags;
|
|
175
|
+
note.updatedAt = new Date().toISOString();
|
|
176
|
+
atomicWrite(filepath, md.noteToMarkdown(note));
|
|
177
|
+
return note;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function deleteNote(id: string): boolean {
|
|
181
|
+
const dir = getDir("Notes");
|
|
182
|
+
if (!dir) return false;
|
|
183
|
+
const filepath = findFile(dir, "note-", id);
|
|
184
|
+
if (!filepath) return false;
|
|
185
|
+
unlinkSync(filepath);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Reminders ───────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
export function listReminders(includeFired = false): Reminder[] {
|
|
192
|
+
const dir = getDir("Reminders");
|
|
193
|
+
if (!dir) return [];
|
|
194
|
+
const files = readAllMd(dir, "reminder-");
|
|
195
|
+
const reminders = files.map((f) => md.markdownToReminder(f.content));
|
|
196
|
+
return includeFired ? reminders : reminders.filter((r) => !r.fired);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function addReminder(text: string, triggerAt: string, calendarEventUid?: string): Reminder {
|
|
200
|
+
const dir = getDir("Reminders");
|
|
201
|
+
if (!dir) throw new Error("Obsidian vault not configured");
|
|
202
|
+
const reminder: Reminder = {
|
|
203
|
+
id: genId(),
|
|
204
|
+
text,
|
|
205
|
+
triggerAt,
|
|
206
|
+
fired: false,
|
|
207
|
+
createdAt: new Date().toISOString(),
|
|
208
|
+
...(calendarEventUid ? { calendarEventUid } : {}),
|
|
209
|
+
};
|
|
210
|
+
atomicWrite(join(dir, filenameForId("reminder-", reminder.id)), md.reminderToMarkdown(reminder));
|
|
211
|
+
return reminder;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function updateReminder(id: string, updates: { text?: string; triggerAt?: string; calendarEventUid?: string }): Reminder | null {
|
|
215
|
+
const dir = getDir("Reminders");
|
|
216
|
+
if (!dir) return null;
|
|
217
|
+
const filepath = findFile(dir, "reminder-", id);
|
|
218
|
+
if (!filepath) return null;
|
|
219
|
+
const reminder = md.markdownToReminder(readFileSync(filepath, "utf-8"));
|
|
220
|
+
if (updates.text !== undefined) reminder.text = updates.text;
|
|
221
|
+
if (updates.triggerAt !== undefined) reminder.triggerAt = updates.triggerAt;
|
|
222
|
+
if (updates.calendarEventUid !== undefined) reminder.calendarEventUid = updates.calendarEventUid;
|
|
223
|
+
atomicWrite(filepath, md.reminderToMarkdown(reminder));
|
|
224
|
+
return reminder;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function fireReminder(id: string): Reminder | null {
|
|
228
|
+
const dir = getDir("Reminders");
|
|
229
|
+
if (!dir) return null;
|
|
230
|
+
const filepath = findFile(dir, "reminder-", id);
|
|
231
|
+
if (!filepath) return null;
|
|
232
|
+
const reminder = md.markdownToReminder(readFileSync(filepath, "utf-8"));
|
|
233
|
+
reminder.fired = true;
|
|
234
|
+
atomicWrite(filepath, md.reminderToMarkdown(reminder));
|
|
235
|
+
return reminder;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function deleteReminder(id: string): boolean {
|
|
239
|
+
const dir = getDir("Reminders");
|
|
240
|
+
if (!dir) return false;
|
|
241
|
+
const filepath = findFile(dir, "reminder-", id);
|
|
242
|
+
if (!filepath) return false;
|
|
243
|
+
unlinkSync(filepath);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function getDueReminders(): Reminder[] {
|
|
248
|
+
const now = new Date().toISOString();
|
|
249
|
+
const dir = getDir("Reminders");
|
|
250
|
+
if (!dir) return [];
|
|
251
|
+
const files = readAllMd(dir, "reminder-");
|
|
252
|
+
const reminders = files.map((f) => md.markdownToReminder(f.content));
|
|
253
|
+
return reminders.filter((r) => !r.fired && r.triggerAt <= now);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── Conversations ───────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export function listGeminiConversations(): GeminiConversation[] {
|
|
259
|
+
const dir = getDir("Conversations");
|
|
260
|
+
if (!dir) return [];
|
|
261
|
+
const files = readAllMd(dir, "convo-");
|
|
262
|
+
return files
|
|
263
|
+
.map((f) => md.markdownToConversation(f.content))
|
|
264
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function saveGeminiConversation(
|
|
268
|
+
messages: Array<{ role: string; text: string; ts: number }>,
|
|
269
|
+
duration?: number,
|
|
270
|
+
): GeminiConversation {
|
|
271
|
+
const dir = getDir("Conversations");
|
|
272
|
+
if (!dir) throw new Error("Obsidian vault not configured");
|
|
273
|
+
const firstUser = messages.find((m) => m.role === "user");
|
|
274
|
+
const title = firstUser ? firstUser.text.slice(0, 50) : "Gemini conversation";
|
|
275
|
+
const convo: GeminiConversation = {
|
|
276
|
+
id: genId(),
|
|
277
|
+
title,
|
|
278
|
+
messages: messages
|
|
279
|
+
.filter((m) => m.role !== "system")
|
|
280
|
+
.map((m) => ({ role: m.role as "user" | "gemini" | "system", text: m.text, ts: m.ts })),
|
|
281
|
+
createdAt: new Date().toISOString(),
|
|
282
|
+
duration,
|
|
283
|
+
};
|
|
284
|
+
atomicWrite(join(dir, filenameForId("convo-", convo.id)), md.conversationToMarkdown(convo));
|
|
285
|
+
|
|
286
|
+
// Enforce limit of 100 conversations: delete oldest if over
|
|
287
|
+
const allFiles = readdirSync(dir)
|
|
288
|
+
.filter((f) => f.startsWith("convo-") && f.endsWith(".md"))
|
|
289
|
+
.sort(); // lexicographic sort — oldest first since filenames contain timestamp-based IDs
|
|
290
|
+
if (allFiles.length > 100) {
|
|
291
|
+
const toRemove = allFiles.slice(0, allFiles.length - 100);
|
|
292
|
+
for (const f of toRemove) {
|
|
293
|
+
try {
|
|
294
|
+
unlinkSync(join(dir, f));
|
|
295
|
+
} catch { /* ignore cleanup errors */ }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return convo;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function getGeminiConversation(id: string): GeminiConversation | null {
|
|
303
|
+
const dir = getDir("Conversations");
|
|
304
|
+
if (!dir) return null;
|
|
305
|
+
const filepath = findFile(dir, "convo-", id);
|
|
306
|
+
if (!filepath) return null;
|
|
307
|
+
return md.markdownToConversation(readFileSync(filepath, "utf-8"));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function deleteGeminiConversation(id: string): boolean {
|
|
311
|
+
const dir = getDir("Conversations");
|
|
312
|
+
if (!dir) return false;
|
|
313
|
+
const filepath = findFile(dir, "convo-", id);
|
|
314
|
+
if (!filepath) return false;
|
|
315
|
+
unlinkSync(filepath);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── Calls ───────────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
export function syncCallToVault(call: CallData): void {
|
|
322
|
+
const dir = getDir("Calls");
|
|
323
|
+
if (!dir) return;
|
|
324
|
+
atomicWrite(join(dir, filenameForId("call-", call.id)), md.callToMarkdown(call));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Contacts ─────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
export function listContacts(search?: string): Contact[] {
|
|
330
|
+
const dir = getDir("Contacts");
|
|
331
|
+
if (!dir) return [];
|
|
332
|
+
const files = readAllMd(dir, "contact-");
|
|
333
|
+
const contacts = files.map((f) => md.markdownToContact(f.content));
|
|
334
|
+
if (!search) return contacts;
|
|
335
|
+
const q = search.toLowerCase();
|
|
336
|
+
return contacts.filter((c) =>
|
|
337
|
+
c.name.toLowerCase().includes(q) ||
|
|
338
|
+
(c.company && c.company.toLowerCase().includes(q)) ||
|
|
339
|
+
(c.email && c.email.toLowerCase().includes(q)) ||
|
|
340
|
+
(c.phone && c.phone.includes(q)) ||
|
|
341
|
+
c.tags.some((t) => t.toLowerCase().includes(q)),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function addContact(name: string, company?: string, email?: string, phone?: string, notes?: string, tags: string[] = []): Contact {
|
|
346
|
+
const dir = getDir("Contacts");
|
|
347
|
+
if (!dir) throw new Error("Obsidian vault not configured");
|
|
348
|
+
const contact: Contact = {
|
|
349
|
+
id: genId(),
|
|
350
|
+
name,
|
|
351
|
+
company,
|
|
352
|
+
email,
|
|
353
|
+
phone,
|
|
354
|
+
notes,
|
|
355
|
+
tags,
|
|
356
|
+
interactions: [],
|
|
357
|
+
createdAt: new Date().toISOString(),
|
|
358
|
+
updatedAt: new Date().toISOString(),
|
|
359
|
+
};
|
|
360
|
+
atomicWrite(join(dir, filenameForId("contact-", contact.id)), md.contactToMarkdown(contact));
|
|
361
|
+
return contact;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function getContact(id: string): Contact | null {
|
|
365
|
+
const dir = getDir("Contacts");
|
|
366
|
+
if (!dir) return null;
|
|
367
|
+
const filepath = findFile(dir, "contact-", id);
|
|
368
|
+
if (!filepath) return null;
|
|
369
|
+
return md.markdownToContact(readFileSync(filepath, "utf-8"));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function updateContact(id: string, updates: { name?: string; company?: string; email?: string; phone?: string; notes?: string; tags?: string[] }): Contact | null {
|
|
373
|
+
const dir = getDir("Contacts");
|
|
374
|
+
if (!dir) return null;
|
|
375
|
+
const filepath = findFile(dir, "contact-", id);
|
|
376
|
+
if (!filepath) return null;
|
|
377
|
+
const contact = md.markdownToContact(readFileSync(filepath, "utf-8"));
|
|
378
|
+
if (updates.name) contact.name = updates.name;
|
|
379
|
+
if (updates.company !== undefined) contact.company = updates.company;
|
|
380
|
+
if (updates.email !== undefined) contact.email = updates.email;
|
|
381
|
+
if (updates.phone !== undefined) contact.phone = updates.phone;
|
|
382
|
+
if (updates.notes !== undefined) contact.notes = updates.notes;
|
|
383
|
+
if (updates.tags) contact.tags = updates.tags;
|
|
384
|
+
contact.updatedAt = new Date().toISOString();
|
|
385
|
+
atomicWrite(filepath, md.contactToMarkdown(contact));
|
|
386
|
+
return contact;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function deleteContact(id: string): boolean {
|
|
390
|
+
const dir = getDir("Contacts");
|
|
391
|
+
if (!dir) return false;
|
|
392
|
+
const filepath = findFile(dir, "contact-", id);
|
|
393
|
+
if (!filepath) return false;
|
|
394
|
+
unlinkSync(filepath);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function logInteraction(contactId: string, interaction: Omit<ContactInteraction, "date">): Contact | null {
|
|
399
|
+
const dir = getDir("Contacts");
|
|
400
|
+
if (!dir) return null;
|
|
401
|
+
const filepath = findFile(dir, "contact-", contactId);
|
|
402
|
+
if (!filepath) return null;
|
|
403
|
+
const contact = md.markdownToContact(readFileSync(filepath, "utf-8"));
|
|
404
|
+
const entry: ContactInteraction = {
|
|
405
|
+
date: new Date().toISOString(),
|
|
406
|
+
type: interaction.type,
|
|
407
|
+
summary: interaction.summary,
|
|
408
|
+
};
|
|
409
|
+
contact.interactions.push(entry);
|
|
410
|
+
contact.lastContactDate = entry.date;
|
|
411
|
+
contact.updatedAt = new Date().toISOString();
|
|
412
|
+
atomicWrite(filepath, md.contactToMarkdown(contact));
|
|
413
|
+
return contact;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ─── Decisions ────────────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
export function listDecisions(search?: string): Decision[] {
|
|
419
|
+
const dir = getDir("Decisions");
|
|
420
|
+
if (!dir) return [];
|
|
421
|
+
const files = readAllMd(dir, "decision-");
|
|
422
|
+
const decisions = files.map((f) => md.markdownToDecision(f.content));
|
|
423
|
+
if (!search) return decisions;
|
|
424
|
+
const q = search.toLowerCase();
|
|
425
|
+
return decisions.filter((d) =>
|
|
426
|
+
d.title.toLowerCase().includes(q) ||
|
|
427
|
+
d.context.toLowerCase().includes(q) ||
|
|
428
|
+
d.decision.toLowerCase().includes(q) ||
|
|
429
|
+
d.reasoning.toLowerCase().includes(q) ||
|
|
430
|
+
d.tags.some((t) => t.toLowerCase().includes(q)),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function addDecision(title: string, context: string, decision: string, alternatives: string[] = [], reasoning: string = "", tags: string[] = []): Decision {
|
|
435
|
+
const dir = getDir("Decisions");
|
|
436
|
+
if (!dir) throw new Error("Obsidian vault not configured");
|
|
437
|
+
const entry: Decision = {
|
|
438
|
+
id: genId(),
|
|
439
|
+
title,
|
|
440
|
+
context,
|
|
441
|
+
decision,
|
|
442
|
+
alternatives,
|
|
443
|
+
reasoning,
|
|
444
|
+
tags,
|
|
445
|
+
createdAt: new Date().toISOString(),
|
|
446
|
+
};
|
|
447
|
+
atomicWrite(join(dir, filenameForId("decision-", entry.id)), md.decisionToMarkdown(entry));
|
|
448
|
+
return entry;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function getDecision(id: string): Decision | null {
|
|
452
|
+
const dir = getDir("Decisions");
|
|
453
|
+
if (!dir) return null;
|
|
454
|
+
const filepath = findFile(dir, "decision-", id);
|
|
455
|
+
if (!filepath) return null;
|
|
456
|
+
return md.markdownToDecision(readFileSync(filepath, "utf-8"));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function deleteDecision(id: string): boolean {
|
|
460
|
+
const dir = getDir("Decisions");
|
|
461
|
+
if (!dir) return false;
|
|
462
|
+
const filepath = findFile(dir, "decision-", id);
|
|
463
|
+
if (!filepath) return false;
|
|
464
|
+
unlinkSync(filepath);
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ─── Vault Watcher ───────────────────────────────────────────────────────────
|
|
2
|
+
// Watches the Memory/ subfolder for external edits (e.g. from Obsidian)
|
|
3
|
+
// and re-indexes changed files in Vectra.
|
|
4
|
+
|
|
5
|
+
import { watch, existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import type { FSWatcher } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { parseFrontmatter } from "./vault-markdown.js";
|
|
9
|
+
|
|
10
|
+
const pendingWrites = new Set<string>();
|
|
11
|
+
let watcher: FSWatcher | null = null;
|
|
12
|
+
|
|
13
|
+
export function markPendingWrite(filename: string): void {
|
|
14
|
+
pendingWrites.add(filename);
|
|
15
|
+
setTimeout(() => pendingWrites.delete(filename), 500);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function startMemoryWatcher(
|
|
19
|
+
memoryDir: string,
|
|
20
|
+
onEdit: (id: string, content: string) => Promise<void>,
|
|
21
|
+
onDelete: (id: string) => Promise<void>,
|
|
22
|
+
): void {
|
|
23
|
+
stopMemoryWatcher();
|
|
24
|
+
if (!existsSync(memoryDir)) return;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
watcher = watch(memoryDir, (eventType, filename) => {
|
|
28
|
+
if (!filename || !filename.startsWith("memory-") || !filename.endsWith(".md")) return;
|
|
29
|
+
if (pendingWrites.has(filename)) return;
|
|
30
|
+
|
|
31
|
+
const id = filename.replace(/^memory-/, "").replace(/\.md$/, "");
|
|
32
|
+
const filepath = join(memoryDir, filename);
|
|
33
|
+
|
|
34
|
+
setTimeout(async () => {
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(filepath)) {
|
|
37
|
+
const raw = readFileSync(filepath, "utf-8");
|
|
38
|
+
const { body } = parseFrontmatter(raw);
|
|
39
|
+
if (body) await onEdit(id, body);
|
|
40
|
+
} else {
|
|
41
|
+
await onDelete(id);
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.warn("[vault-watcher] Error:", err instanceof Error ? err.message : err);
|
|
45
|
+
}
|
|
46
|
+
}, 300);
|
|
47
|
+
});
|
|
48
|
+
console.log(`[vault-watcher] Watching ${memoryDir}`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn("[vault-watcher] Failed to start:", err instanceof Error ? err.message : err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function stopMemoryWatcher(): void {
|
|
55
|
+
if (watcher) {
|
|
56
|
+
watcher.close();
|
|
57
|
+
watcher = null;
|
|
58
|
+
}
|
|
59
|
+
}
|