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/agent-executor.ts
CHANGED
|
@@ -18,11 +18,27 @@ const CLI_CONNECT_TIMEOUT_MS = 30_000;
|
|
|
18
18
|
/** Poll interval when waiting for CLI connection */
|
|
19
19
|
const CLI_CONNECT_POLL_MS = 500;
|
|
20
20
|
|
|
21
|
+
/** Skill-aware preamble for Claude agents (see skillRoutingEnabled below). */
|
|
22
|
+
const SKILL_ROUTING_PREAMBLE = [
|
|
23
|
+
"[Skill-aware mode]",
|
|
24
|
+
"You have access to user-installed Claude Code skills via the Skill tool. Skills are structured multi-stage workflows for specific tasks (content plans, post writing, audits, code review, etc.) — their descriptions are listed in your system context.",
|
|
25
|
+
"",
|
|
26
|
+
"BEFORE following the specialized instructions below, check whether the user's request matches a skill's description. If yes, invoke that skill via the Skill tool instead of executing custom logic.",
|
|
27
|
+
"",
|
|
28
|
+
"The specialized instructions below apply when no skill is a better fit.",
|
|
29
|
+
"",
|
|
30
|
+
"---",
|
|
31
|
+
"",
|
|
32
|
+
"",
|
|
33
|
+
].join("\n");
|
|
34
|
+
|
|
35
|
+
|
|
21
36
|
export interface ExecuteAgentOptions {
|
|
22
37
|
force?: boolean;
|
|
23
38
|
triggerType?: "manual" | "webhook" | "schedule";
|
|
24
39
|
additionalEnv?: Record<string, string>;
|
|
25
40
|
systemPrompt?: string;
|
|
41
|
+
cwdOverride?: string;
|
|
26
42
|
}
|
|
27
43
|
|
|
28
44
|
export class AgentExecutor {
|
|
@@ -155,7 +171,7 @@ export class AgentExecutor {
|
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
// Resolve working directory
|
|
158
|
-
let cwd = agent.cwd;
|
|
174
|
+
let cwd = opts?.cwdOverride || agent.cwd;
|
|
159
175
|
if (cwd === "temp" || !cwd) {
|
|
160
176
|
cwd = mkdtempSync(join(tmpdir(), `heyhank-agent-${agent.id}-`));
|
|
161
177
|
}
|
|
@@ -170,12 +186,24 @@ export class AgentExecutor {
|
|
|
170
186
|
`but agent sessions always run with bypassPermissions`,
|
|
171
187
|
);
|
|
172
188
|
}
|
|
189
|
+
// Skill-aware routing: when enabled (default for Claude backends),
|
|
190
|
+
// ensure the `Skill` tool is in allowedTools so the agent can invoke
|
|
191
|
+
// any user-installed skill from ~/.claude/skills/ when its description
|
|
192
|
+
// matches the user's request.
|
|
193
|
+
const skillRoutingEnabled = agent.backendType === "claude" && agent.skillRouting !== false;
|
|
194
|
+
let effectiveAllowedTools = agent.allowedTools;
|
|
195
|
+
if (skillRoutingEnabled && Array.isArray(agent.allowedTools) && agent.allowedTools.length > 0) {
|
|
196
|
+
if (!agent.allowedTools.includes("Skill")) {
|
|
197
|
+
effectiveAllowedTools = [...agent.allowedTools, "Skill"];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
173
201
|
const sessionInfo = this.launcher.launch({
|
|
174
202
|
model: agent.model,
|
|
175
203
|
permissionMode: "bypassPermissions",
|
|
176
204
|
cwd,
|
|
177
205
|
env: envVars,
|
|
178
|
-
allowedTools:
|
|
206
|
+
allowedTools: effectiveAllowedTools,
|
|
179
207
|
backendType: agent.backendType,
|
|
180
208
|
codexInternetAccess: agent.backendType === "codex" ? (agent.codexInternetAccess ?? true) : undefined,
|
|
181
209
|
codexSandbox: agent.backendType === "codex"
|
|
@@ -220,6 +248,13 @@ export class AgentExecutor {
|
|
|
220
248
|
resolvedPrompt = resolvedPrompt.replace(/\{\{input\}\}/g, "");
|
|
221
249
|
}
|
|
222
250
|
|
|
251
|
+
// Skill-aware preamble: routes attention to matching skills before
|
|
252
|
+
// following specialized instructions. Skill descriptions are already
|
|
253
|
+
// in the CLI system context; this just teaches the agent to consult them.
|
|
254
|
+
if (skillRoutingEnabled) {
|
|
255
|
+
resolvedPrompt = SKILL_ROUTING_PREAMBLE + resolvedPrompt;
|
|
256
|
+
}
|
|
257
|
+
|
|
223
258
|
// Send the prompt with agent prefix for traceability
|
|
224
259
|
const fullPrompt = `[agent:${agent.id} ${agent.name}]\n\n${resolvedPrompt}`;
|
|
225
260
|
this.wsBridge.injectUserMessage(sessionInfo.sessionId, fullPrompt);
|
|
@@ -316,6 +351,71 @@ export class AgentExecutor {
|
|
|
316
351
|
return this.executionStore.list(opts);
|
|
317
352
|
}
|
|
318
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Cancel a running execution. Marks the in-memory + persisted record as
|
|
356
|
+
* completed (success=false). Returns true if a matching in-memory execution
|
|
357
|
+
* was found and updated, false if only the on-disk record was touched (e.g.
|
|
358
|
+
* for zombie sessions whose CLI process is long gone).
|
|
359
|
+
*/
|
|
360
|
+
cancelExecution(sessionId: string, reason = "Stopped by user"): boolean {
|
|
361
|
+
for (const [, execs] of this.executions) {
|
|
362
|
+
const exec = execs.find((e) => e.sessionId === sessionId && !e.completedAt);
|
|
363
|
+
if (exec) {
|
|
364
|
+
exec.completedAt = Date.now();
|
|
365
|
+
exec.success = false;
|
|
366
|
+
exec.error = exec.error || reason;
|
|
367
|
+
this.executionStore.update(sessionId, {
|
|
368
|
+
completedAt: exec.completedAt,
|
|
369
|
+
success: exec.success,
|
|
370
|
+
error: exec.error,
|
|
371
|
+
});
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Fallback: touch the on-disk record directly (zombie cleanup).
|
|
376
|
+
this.executionStore.update(sessionId, {
|
|
377
|
+
completedAt: Date.now(),
|
|
378
|
+
success: false,
|
|
379
|
+
error: reason,
|
|
380
|
+
});
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Permanently delete execution records (single or bulk). Filters by status
|
|
386
|
+
* if provided, or by an explicit list of sessionIds. Returns the count of
|
|
387
|
+
* removed records. Refuses to delete records that are still running.
|
|
388
|
+
*/
|
|
389
|
+
deleteExecutions(opts: { sessionIds?: string[]; status?: "success" | "error" }): number {
|
|
390
|
+
let targets: string[] = [];
|
|
391
|
+
|
|
392
|
+
if (opts.sessionIds && opts.sessionIds.length > 0) {
|
|
393
|
+
// Filter out running ones — caller must cancel first.
|
|
394
|
+
const all = this.executionStore.list({ limit: 10000 }).executions;
|
|
395
|
+
const byId = new Map(all.map((e) => [e.sessionId, e] as const));
|
|
396
|
+
for (const sid of opts.sessionIds) {
|
|
397
|
+
const exec = byId.get(sid);
|
|
398
|
+
if (!exec || exec.completedAt) targets.push(sid);
|
|
399
|
+
}
|
|
400
|
+
} else if (opts.status) {
|
|
401
|
+
const matching = this.executionStore.list({ status: opts.status, limit: 10000 }).executions;
|
|
402
|
+
targets = matching.map((e) => e.sessionId);
|
|
403
|
+
} else {
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (targets.length === 0) return 0;
|
|
408
|
+
|
|
409
|
+
// Also drop them from each agent's in-memory list so getExecutions stays consistent.
|
|
410
|
+
const targetSet = new Set(targets);
|
|
411
|
+
for (const [agentId, list] of this.executions) {
|
|
412
|
+
const next = list.filter((e) => !targetSet.has(e.sessionId));
|
|
413
|
+
if (next.length !== list.length) this.executions.set(agentId, next);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return this.executionStore.deleteBySessionIds(targets);
|
|
417
|
+
}
|
|
418
|
+
|
|
319
419
|
/** Handle session exit: mark the corresponding execution as completed. */
|
|
320
420
|
handleSessionExited(sessionId: string, exitCode: number | null): void {
|
|
321
421
|
for (const [, execs] of this.executions) {
|
package/server/agent-store.ts
CHANGED
|
@@ -2,12 +2,12 @@ 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 { HEYHANK_HOME } from "./paths.js";
|
|
10
|
+
import { atomicWriteFileSync } from "./fs-utils.js";
|
|
11
11
|
import { randomBytes } from "node:crypto";
|
|
12
12
|
import type { AgentConfig, AgentConfigCreateInput } from "./agent-types.js";
|
|
13
13
|
|
|
@@ -115,7 +115,7 @@ export function createAgent(data: AgentConfigCreateInput): AgentConfig {
|
|
|
115
115
|
totalRuns: 0,
|
|
116
116
|
consecutiveFailures: 0,
|
|
117
117
|
};
|
|
118
|
-
|
|
118
|
+
atomicWriteFileSync(filePath(id), JSON.stringify(agent, null, 2));
|
|
119
119
|
return agent;
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -155,7 +155,7 @@ export function updateAgent(
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
atomicWriteFileSync(filePath(newId), JSON.stringify(agent, null, 2));
|
|
159
159
|
return agent;
|
|
160
160
|
}
|
|
161
161
|
|
package/server/agent-types.ts
CHANGED
|
@@ -49,6 +49,17 @@ export interface AgentConfig {
|
|
|
49
49
|
// ── Skills ──
|
|
50
50
|
/** Skill slugs to attach (from ~/.claude/skills/) */
|
|
51
51
|
skills?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Skill-aware routing. When true (default), the agent is prepended with a
|
|
54
|
+
* generic skill-discovery preamble and the `Skill` tool is added to its
|
|
55
|
+
* allowedTools, so it can invoke any user-installed skill from
|
|
56
|
+
* ~/.claude/skills/ when a skill description matches the user's request.
|
|
57
|
+
*
|
|
58
|
+
* Only applies to Claude backends (Codex/Ollama/etc. don't support skills).
|
|
59
|
+
* Set to false for agents that must follow strict, scripted instructions
|
|
60
|
+
* without skill detours (e.g. low-level coordinator agents).
|
|
61
|
+
*/
|
|
62
|
+
skillRouting?: boolean;
|
|
52
63
|
|
|
53
64
|
// ── Docker ──
|
|
54
65
|
/** Optional Docker container configuration */
|
|
@@ -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");
|