heyhank 0.1.0 → 0.3.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-DqjDAcIw.js} +3 -3
- package/dist/assets/AssistantPage-C50CQFSB.js +2 -0
- package/dist/assets/BusinessPage-AY70tf1k.js +1 -0
- package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-Dt7LLuRr.js} +1 -1
- package/dist/assets/HelpPage-tlGx7fQF.js +1 -0
- package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-B4XOuHXu.js} +1 -1
- package/dist/assets/JarvisHUD-BDvuRd0I.js +120 -0
- package/dist/assets/MediaPage-CofV9Rd-.js +1 -0
- package/dist/assets/MemoryPage-Cj7FeqmJ.js +1 -0
- package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-B9kXAlH1.js} +1 -1
- package/dist/assets/{Playground-Fc5cdc5p.js → Playground-Cka-pRkP.js} +1 -1
- package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-BqhQgfYj.js} +1 -1
- package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-VveKc9uX.js} +2 -2
- package/dist/assets/RunsPage-DXVEk0AZ.js +1 -0
- package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-DACcwfDF.js} +1 -1
- package/dist/assets/SettingsPage-jfuQh8Tu.js +51 -0
- package/dist/assets/SkillsMarketplace-DrigiApe.js +1 -0
- package/dist/assets/SocialMediaPage-DOh3IPe8.js +10 -0
- package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DLhJWATT.js} +1 -1
- package/dist/assets/TelephonyPage-9C4C3_ot.js +9 -0
- package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-ChX-8Wu7.js} +1 -1
- package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
- package/dist/assets/index-C6Q5UQHD.js +229 -0
- package/dist/assets/index-ZxGXgiV3.css +32 -0
- package/dist/assets/sw-register-BBYuk-kw.js +1 -0
- package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
- package/dist/assets/workbox-window.prod.es5-BBnX5xw4.js +2 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist/{workbox-d2a0910a.js → workbox-080c8b91.js} +1 -1
- package/package.json +6 -1
- package/server/agent-executor.ts +102 -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/execution-store.ts +54 -1
- 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 +44 -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/index-CEqZnThB.js +0 -204
- package/dist/assets/sw-register-LSSpj6RU.js +0 -1
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +0 -2
- package/server/socialmedia/adapters/ayrshare-adapter.ts +0 -169
package/server/cron-store.ts
CHANGED
|
@@ -2,13 +2,13 @@ import {
|
|
|
2
2
|
mkdirSync,
|
|
3
3
|
readdirSync,
|
|
4
4
|
readFileSync,
|
|
5
|
-
writeFileSync,
|
|
6
5
|
unlinkSync,
|
|
7
6
|
existsSync,
|
|
8
7
|
} from "node:fs";
|
|
9
8
|
import { join } from "node:path";
|
|
10
9
|
import type { CronJob, CronJobCreateInput } from "./cron-types.js";
|
|
11
10
|
import { HEYHANK_HOME } from "./paths.js";
|
|
11
|
+
import { atomicWriteFileSync } from "./fs-utils.js";
|
|
12
12
|
|
|
13
13
|
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
@@ -92,7 +92,7 @@ export function createJob(data: CronJobCreateInput): CronJob {
|
|
|
92
92
|
consecutiveFailures: 0,
|
|
93
93
|
totalRuns: 0,
|
|
94
94
|
};
|
|
95
|
-
|
|
95
|
+
atomicWriteFileSync(filePath(id), JSON.stringify(job, null, 2));
|
|
96
96
|
return job;
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -132,7 +132,7 @@ export function updateJob(
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
atomicWriteFileSync(filePath(newId), JSON.stringify(job, null, 2));
|
|
136
136
|
return job;
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ─── Embedding Service ──────────────────────────────────────────────────────
|
|
2
|
+
// Singleton: lazy-loads transformers.js pipeline for local embeddings.
|
|
3
|
+
// Model: Xenova/multilingual-e5-small (384 dims, ~118MB quantized, 100 languages)
|
|
4
|
+
|
|
5
|
+
type Pipeline = {
|
|
6
|
+
(texts: string[], options?: { pooling?: string; normalize?: boolean }): Promise<{ tolist(): number[][] }>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let pipeline: Pipeline | null = null;
|
|
10
|
+
let initPromise: Promise<Pipeline> | null = null;
|
|
11
|
+
|
|
12
|
+
async function ensureModel(): Promise<Pipeline> {
|
|
13
|
+
if (pipeline) return pipeline;
|
|
14
|
+
if (initPromise) return initPromise;
|
|
15
|
+
|
|
16
|
+
initPromise = (async () => {
|
|
17
|
+
console.log("[embedding] Loading multilingual-e5-small model (first run downloads ~118MB)...");
|
|
18
|
+
const { pipeline: createPipeline } = await import("@huggingface/transformers");
|
|
19
|
+
const pipe = await createPipeline("feature-extraction", "Xenova/multilingual-e5-small", {
|
|
20
|
+
dtype: "q8",
|
|
21
|
+
});
|
|
22
|
+
pipeline = pipe as unknown as Pipeline;
|
|
23
|
+
console.log("[embedding] Model loaded successfully.");
|
|
24
|
+
return pipeline;
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
initPromise.catch((err) => {
|
|
28
|
+
console.error("[embedding] Failed to load model:", err);
|
|
29
|
+
initPromise = null;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return initPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get embedding vector for text.
|
|
37
|
+
* @param text - The text to embed
|
|
38
|
+
* @param prefix - e5-specific prefix: "passage: " for documents, "query: " for search queries
|
|
39
|
+
*/
|
|
40
|
+
export async function getEmbedding(text: string, prefix = "passage: "): Promise<number[]> {
|
|
41
|
+
const pipe = await ensureModel();
|
|
42
|
+
const result = await pipe([`${prefix}${text}`], { pooling: "mean", normalize: true });
|
|
43
|
+
return result.tolist()[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Check if the embedding model is loaded and ready */
|
|
47
|
+
export function isEmbeddingReady(): boolean {
|
|
48
|
+
return pipeline !== null;
|
|
49
|
+
}
|
|
@@ -62,6 +62,19 @@ export interface HeyHankEventMap {
|
|
|
62
62
|
/** A result (turn completion) was processed and broadcast to browsers. */
|
|
63
63
|
"message:result": { sessionId: string; message: BrowserIncomingMessage };
|
|
64
64
|
|
|
65
|
+
// ── Telephony ──────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/** A phone call has just ended; a notification has been persisted for HankChat. */
|
|
68
|
+
"telephony:call-ended": {
|
|
69
|
+
notificationId: string;
|
|
70
|
+
callId: string;
|
|
71
|
+
phone: string;
|
|
72
|
+
contactName: string | null;
|
|
73
|
+
direction: "inbound" | "outbound";
|
|
74
|
+
durationSeconds: number;
|
|
75
|
+
summary: string | null;
|
|
76
|
+
};
|
|
77
|
+
|
|
65
78
|
// ── Federation ──────────────────────────────────────────────────────
|
|
66
79
|
|
|
67
80
|
/** A remote federation node connected. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Persists AgentExecution records to disk as JSONL (one file per day).
|
|
3
3
|
// Used by the Runs view to display execution history across server restarts.
|
|
4
4
|
|
|
5
|
-
import { mkdirSync, appendFileSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { mkdirSync, appendFileSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import type { AgentExecution } from "./agent-types.js";
|
|
@@ -72,6 +72,59 @@ export class ExecutionStore {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Permanently delete executions by sessionId. Rewrites any daily JSONL file
|
|
77
|
+
* that contained matching records and removes them from the in-memory cache.
|
|
78
|
+
* Returns the number of records that were removed (counts cache hits — the
|
|
79
|
+
* on-disk file may contain extra historical lines for the same sessionId
|
|
80
|
+
* which are all stripped too).
|
|
81
|
+
*/
|
|
82
|
+
deleteBySessionIds(sessionIds: string[]): number {
|
|
83
|
+
if (sessionIds.length === 0) return 0;
|
|
84
|
+
const toDelete = new Set(sessionIds);
|
|
85
|
+
|
|
86
|
+
// Rewrite each daily JSONL file, stripping any line whose sessionId matches.
|
|
87
|
+
try {
|
|
88
|
+
const files = readdirSync(this.dir)
|
|
89
|
+
.filter((f) => f.startsWith("executions-") && f.endsWith(".jsonl"));
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const filepath = join(this.dir, file);
|
|
92
|
+
const content = readFileSync(filepath, "utf-8");
|
|
93
|
+
const lines = content.split("\n").filter(Boolean);
|
|
94
|
+
const kept: string[] = [];
|
|
95
|
+
let touched = false;
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
try {
|
|
98
|
+
const exec = JSON.parse(line) as AgentExecution;
|
|
99
|
+
if (toDelete.has(exec.sessionId)) {
|
|
100
|
+
touched = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Keep malformed lines as-is rather than risk data loss.
|
|
105
|
+
}
|
|
106
|
+
kept.push(line);
|
|
107
|
+
}
|
|
108
|
+
if (touched) {
|
|
109
|
+
writeFileSync(filepath, kept.length ? kept.join("\n") + "\n" : "", "utf-8");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error("[execution-store] Failed to delete executions from disk:", err);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Remove from in-memory cache and count.
|
|
117
|
+
let removed = 0;
|
|
118
|
+
this.recentCache = this.recentCache.filter((e) => {
|
|
119
|
+
if (toDelete.has(e.sessionId)) {
|
|
120
|
+
removed++;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
return removed;
|
|
126
|
+
}
|
|
127
|
+
|
|
75
128
|
/** Query executions with pagination and filtering. */
|
|
76
129
|
list(opts?: ExecutionQuery): ExecutionListResult {
|
|
77
130
|
const limit = opts?.limit ?? 50;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// ─── Node Store ───────────────────────────────────────────────────────────────
|
|
2
2
|
// File-based CRUD for node configs. Stores identity + peer list in HEYHANK_HOME.
|
|
3
3
|
|
|
4
|
-
import { mkdirSync, readFileSync,
|
|
4
|
+
import { mkdirSync, readFileSync, existsSync } from "node:fs";
|
|
5
5
|
import { join, dirname } from "node:path";
|
|
6
6
|
import { randomUUID, randomBytes } from "node:crypto";
|
|
7
7
|
import { HEYHANK_HOME } from "../paths.js";
|
|
8
|
+
import { atomicWriteFileSync } from "../fs-utils.js";
|
|
8
9
|
import type { NodeIdentity, NodeConfig } from "./node-types.js";
|
|
9
10
|
|
|
10
11
|
const IDENTITY_FILE = join(HEYHANK_HOME, "node-identity.json");
|
|
@@ -32,14 +33,14 @@ export function getNodeIdentity(): NodeIdentity {
|
|
|
32
33
|
createdAt: new Date().toISOString(),
|
|
33
34
|
};
|
|
34
35
|
mkdirSync(dirname(IDENTITY_FILE), { recursive: true });
|
|
35
|
-
|
|
36
|
+
atomicWriteFileSync(IDENTITY_FILE, JSON.stringify(_identity, null, 2));
|
|
36
37
|
return _identity;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function updateNodeName(name: string): NodeIdentity {
|
|
40
41
|
const id = getNodeIdentity();
|
|
41
42
|
id.name = name;
|
|
42
|
-
|
|
43
|
+
atomicWriteFileSync(IDENTITY_FILE, JSON.stringify(id, null, 2));
|
|
43
44
|
return id;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -56,7 +57,7 @@ function readNodes(): NodeConfig[] {
|
|
|
56
57
|
|
|
57
58
|
function writeNodes(nodes: NodeConfig[]): void {
|
|
58
59
|
mkdirSync(dirname(NODES_FILE), { recursive: true });
|
|
59
|
-
|
|
60
|
+
atomicWriteFileSync(NODES_FILE, JSON.stringify(nodes, null, 2));
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function listNodes(): NodeConfig[] {
|
package/server/fs-utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync } from "node:fs";
|
|
2
2
|
|
|
3
3
|
/** Count newlines in a file. Fast: reads raw buffer, counts 0x0A bytes. */
|
|
4
4
|
export function countFileLines(path: string): number {
|
|
@@ -13,3 +13,30 @@ export function countFileLines(path: string): number {
|
|
|
13
13
|
return 0;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Atomic write: write to `path.tmp` then rename over the destination.
|
|
19
|
+
*
|
|
20
|
+
* Prevents corrupted/half-written files if the process crashes or two writers
|
|
21
|
+
* race each other — rename is atomic on POSIX & NTFS within the same filesystem.
|
|
22
|
+
* If the rename fails, the tmp file is removed so no `.tmp` junk is left behind.
|
|
23
|
+
*/
|
|
24
|
+
export function atomicWriteFileSync(
|
|
25
|
+
path: string,
|
|
26
|
+
content: string | Buffer | Uint8Array,
|
|
27
|
+
encoding: BufferEncoding = "utf-8",
|
|
28
|
+
): void {
|
|
29
|
+
const tmp = `${path}.tmp`;
|
|
30
|
+
try {
|
|
31
|
+
if (typeof content === "string") {
|
|
32
|
+
writeFileSync(tmp, content, encoding);
|
|
33
|
+
} else {
|
|
34
|
+
writeFileSync(tmp, content);
|
|
35
|
+
}
|
|
36
|
+
renameSync(tmp, path);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Best-effort cleanup of the temp file so we don't leave turds behind
|
|
39
|
+
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ─── Hank Notifications Store ───────────────────────────────────────────────
|
|
2
|
+
// Persistent queue of events that should be delivered to HankChat asynchronously
|
|
3
|
+
// (e.g. "call ended, please summarise"). Survives server restarts and HankChat
|
|
4
|
+
// being closed.
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import type { TranscriptEntry } from "./telephony/call-types.js";
|
|
11
|
+
|
|
12
|
+
export interface CallEndedNotification {
|
|
13
|
+
id: string;
|
|
14
|
+
type: "call-ended";
|
|
15
|
+
createdAt: number;
|
|
16
|
+
consumed: boolean;
|
|
17
|
+
callId: string;
|
|
18
|
+
phone: string;
|
|
19
|
+
contactName: string | null;
|
|
20
|
+
direction: "inbound" | "outbound";
|
|
21
|
+
durationSeconds: number;
|
|
22
|
+
summary: string | null;
|
|
23
|
+
transcript: TranscriptEntry[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type HankNotification = CallEndedNotification;
|
|
27
|
+
|
|
28
|
+
const DATA_DIR = join(homedir(), ".heyhank");
|
|
29
|
+
const FILE = join(DATA_DIR, "hank-notifications.json");
|
|
30
|
+
const MAX_KEEP = 100; // keep last N (consumed + unconsumed) to avoid unbounded growth
|
|
31
|
+
|
|
32
|
+
let cache: HankNotification[] | null = null;
|
|
33
|
+
|
|
34
|
+
function load(): HankNotification[] {
|
|
35
|
+
if (cache) return cache;
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(FILE)) return (cache = []);
|
|
38
|
+
const raw = readFileSync(FILE, "utf-8");
|
|
39
|
+
cache = JSON.parse(raw) as HankNotification[];
|
|
40
|
+
if (!Array.isArray(cache)) cache = [];
|
|
41
|
+
return cache;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error("[hank-notifications] Failed to load store:", err);
|
|
44
|
+
return (cache = []);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function persist(): void {
|
|
49
|
+
try {
|
|
50
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
51
|
+
const list = load();
|
|
52
|
+
// Trim to most recent MAX_KEEP
|
|
53
|
+
const trimmed = list.slice(-MAX_KEEP);
|
|
54
|
+
writeFileSync(FILE, JSON.stringify(trimmed, null, 2), "utf-8");
|
|
55
|
+
cache = trimmed;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error("[hank-notifications] Failed to persist store:", err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function addNotification(
|
|
62
|
+
n: Omit<HankNotification, "id" | "createdAt" | "consumed">,
|
|
63
|
+
): HankNotification {
|
|
64
|
+
const list = load();
|
|
65
|
+
const full: HankNotification = {
|
|
66
|
+
id: randomUUID(),
|
|
67
|
+
createdAt: Date.now(),
|
|
68
|
+
consumed: false,
|
|
69
|
+
...n,
|
|
70
|
+
};
|
|
71
|
+
list.push(full);
|
|
72
|
+
persist();
|
|
73
|
+
return full;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function listPending(): HankNotification[] {
|
|
77
|
+
return load().filter((n) => !n.consumed);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function markConsumed(id: string): boolean {
|
|
81
|
+
const list = load();
|
|
82
|
+
const n = list.find((x) => x.id === id);
|
|
83
|
+
if (!n) return false;
|
|
84
|
+
n.consumed = true;
|
|
85
|
+
persist();
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getById(id: string): HankNotification | null {
|
|
90
|
+
return load().find((n) => n.id === id) ?? null;
|
|
91
|
+
}
|