heyhank 0.1.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/README.md +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// ─── Calendar Service ─────────────────────────────────────────────────────────
|
|
2
|
+
// Multi-account CalDAV calendar integration for the personal assistant.
|
|
3
|
+
// Supports Google Calendar, iCloud, Nextcloud, and any CalDAV-compatible server.
|
|
4
|
+
|
|
5
|
+
import { DAVClient, DAVCalendar } from "tsdav";
|
|
6
|
+
import ICAL from "ical.js";
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { HEYHANK_HOME } from "./paths.js";
|
|
10
|
+
|
|
11
|
+
const CONFIG_PATH = join(HEYHANK_HOME, "assistant", "calendar-accounts.json");
|
|
12
|
+
|
|
13
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface CalendarAccount {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string; // display name, e.g. "Google" or "iCloud"
|
|
18
|
+
provider: "google" | "icloud" | "caldav"; // preset or custom
|
|
19
|
+
serverUrl: string; // CalDAV server URL
|
|
20
|
+
auth: {
|
|
21
|
+
user: string;
|
|
22
|
+
pass: string; // App Password for Google/iCloud
|
|
23
|
+
};
|
|
24
|
+
defaultCalendarId?: string; // preferred calendar for this account
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CalendarEvent {
|
|
28
|
+
uid: string;
|
|
29
|
+
summary: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
location?: string;
|
|
32
|
+
start: string; // ISO datetime
|
|
33
|
+
end: string; // ISO datetime
|
|
34
|
+
allDay: boolean;
|
|
35
|
+
recurrence?: string;
|
|
36
|
+
status?: string; // CONFIRMED, TENTATIVE, CANCELLED
|
|
37
|
+
organizer?: string;
|
|
38
|
+
attendees?: string[];
|
|
39
|
+
calendarName?: string;
|
|
40
|
+
accountId: string;
|
|
41
|
+
accountName: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CalendarInfo {
|
|
45
|
+
url: string;
|
|
46
|
+
displayName: string;
|
|
47
|
+
ctag?: string;
|
|
48
|
+
accountId: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Provider Presets ────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export const PROVIDER_PRESETS: Record<string, { serverUrl: string; label: string; helpText: string }> = {
|
|
54
|
+
google: {
|
|
55
|
+
serverUrl: "https://apidata.googleusercontent.com/caldav/v2/",
|
|
56
|
+
label: "Google Calendar",
|
|
57
|
+
helpText: "Use your Gmail address and an App Password (myaccount.google.com/apppasswords). 2FA must be enabled.",
|
|
58
|
+
},
|
|
59
|
+
icloud: {
|
|
60
|
+
serverUrl: "https://caldav.icloud.com/",
|
|
61
|
+
label: "iCloud Calendar",
|
|
62
|
+
helpText: "Use your Apple ID email and an App-Specific Password (appleid.apple.com → Sign-In and Security → App-Specific Passwords).",
|
|
63
|
+
},
|
|
64
|
+
outlook: {
|
|
65
|
+
serverUrl: "https://outlook.office365.com/caldav/",
|
|
66
|
+
label: "Outlook Calendar",
|
|
67
|
+
helpText: "Use your Outlook/Hotmail email and an App Password (account.microsoft.com → Security → App passwords). 2-Step Verification must be enabled.",
|
|
68
|
+
},
|
|
69
|
+
caldav: {
|
|
70
|
+
serverUrl: "",
|
|
71
|
+
label: "Custom CalDAV",
|
|
72
|
+
helpText: "Enter the full CalDAV URL of your server (e.g. https://cloud.example.com/remote.php/dav/ for Nextcloud).",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ─── Account Management ──────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function ensureDir(): void {
|
|
79
|
+
const dir = join(HEYHANK_HOME, "assistant");
|
|
80
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function loadAccounts(): CalendarAccount[] {
|
|
84
|
+
ensureDir();
|
|
85
|
+
try {
|
|
86
|
+
if (existsSync(CONFIG_PATH)) {
|
|
87
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as CalendarAccount[];
|
|
88
|
+
}
|
|
89
|
+
} catch { /* ignore */ }
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function saveAccounts(accounts: CalendarAccount[]): void {
|
|
94
|
+
ensureDir();
|
|
95
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(accounts, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function addAccount(data: Omit<CalendarAccount, "id">): CalendarAccount {
|
|
99
|
+
const accounts = loadAccounts();
|
|
100
|
+
const account: CalendarAccount = {
|
|
101
|
+
...data,
|
|
102
|
+
id: `cal-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
103
|
+
};
|
|
104
|
+
accounts.push(account);
|
|
105
|
+
saveAccounts(accounts);
|
|
106
|
+
return account;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function removeAccount(id: string): boolean {
|
|
110
|
+
const accounts = loadAccounts();
|
|
111
|
+
const idx = accounts.findIndex((a) => a.id === id);
|
|
112
|
+
if (idx === -1) return false;
|
|
113
|
+
accounts.splice(idx, 1);
|
|
114
|
+
saveAccounts(accounts);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fuzzy match for Gemini tool calls
|
|
119
|
+
function similarity(a: string, b: string): number {
|
|
120
|
+
a = a.toLowerCase().trim();
|
|
121
|
+
b = b.toLowerCase().trim();
|
|
122
|
+
if (a === b) return 1;
|
|
123
|
+
if (a.includes(b) || b.includes(a)) return 0.8;
|
|
124
|
+
// Dice coefficient
|
|
125
|
+
const bigrams = (s: string) => {
|
|
126
|
+
const set: string[] = [];
|
|
127
|
+
for (let i = 0; i < s.length - 1; i++) set.push(s.slice(i, i + 2));
|
|
128
|
+
return set;
|
|
129
|
+
};
|
|
130
|
+
const aBi = bigrams(a);
|
|
131
|
+
const bBi = bigrams(b);
|
|
132
|
+
let matches = 0;
|
|
133
|
+
for (const bi of aBi) if (bBi.includes(bi)) matches++;
|
|
134
|
+
return (2 * matches) / (aBi.length + bBi.length) || 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getAccount(idOrName: string): CalendarAccount | undefined {
|
|
138
|
+
const accounts = loadAccounts();
|
|
139
|
+
const exact = accounts.find((a) => a.id === idOrName || a.name.toLowerCase() === idOrName.toLowerCase());
|
|
140
|
+
if (exact) return exact;
|
|
141
|
+
let best: CalendarAccount | undefined;
|
|
142
|
+
let bestScore = 0;
|
|
143
|
+
for (const a of accounts) {
|
|
144
|
+
const score = Math.max(similarity(idOrName, a.name), similarity(idOrName, a.auth.user));
|
|
145
|
+
if (score > bestScore) { bestScore = score; best = a; }
|
|
146
|
+
}
|
|
147
|
+
return bestScore >= 0.4 ? best : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── CalDAV Client ───────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
async function createClient(account: CalendarAccount): Promise<DAVClient> {
|
|
153
|
+
const client = new DAVClient({
|
|
154
|
+
serverUrl: account.serverUrl,
|
|
155
|
+
credentials: {
|
|
156
|
+
username: account.auth.user,
|
|
157
|
+
password: account.auth.pass,
|
|
158
|
+
},
|
|
159
|
+
authMethod: "Basic",
|
|
160
|
+
defaultAccountType: "caldav",
|
|
161
|
+
});
|
|
162
|
+
await client.login();
|
|
163
|
+
return client;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Calendar Operations ─────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/** Filter to only VEVENT calendars (exclude VTODO/Reminders) */
|
|
169
|
+
function isEventCalendar(cal: DAVCalendar): boolean {
|
|
170
|
+
const components = (cal as unknown as { components?: string[] }).components;
|
|
171
|
+
if (!components || components.length === 0) return true; // assume event calendar if unknown
|
|
172
|
+
return components.includes("VEVENT");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** List all calendars for an account */
|
|
176
|
+
export async function listCalendars(account: CalendarAccount): Promise<CalendarInfo[]> {
|
|
177
|
+
const client = await createClient(account);
|
|
178
|
+
const calendars = await client.fetchCalendars();
|
|
179
|
+
return calendars.filter(isEventCalendar).map((cal) => ({
|
|
180
|
+
url: cal.url,
|
|
181
|
+
displayName: (typeof cal.displayName === "string" ? cal.displayName : null) || "Unnamed",
|
|
182
|
+
ctag: cal.ctag,
|
|
183
|
+
accountId: account.id,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Parse ICS data into CalendarEvent objects */
|
|
188
|
+
function parseICS(icsData: string, accountId: string, accountName: string, calendarName?: string): CalendarEvent[] {
|
|
189
|
+
const events: CalendarEvent[] = [];
|
|
190
|
+
try {
|
|
191
|
+
const jcal = ICAL.parse(icsData);
|
|
192
|
+
const comp = new ICAL.Component(jcal);
|
|
193
|
+
const vevents = comp.getAllSubcomponents("vevent");
|
|
194
|
+
|
|
195
|
+
for (const vevent of vevents) {
|
|
196
|
+
const event = new ICAL.Event(vevent);
|
|
197
|
+
const dtstart = vevent.getFirstPropertyValue("dtstart") as ICAL.Time | null;
|
|
198
|
+
const dtend = vevent.getFirstPropertyValue("dtend") as ICAL.Time | null;
|
|
199
|
+
|
|
200
|
+
const allDay = dtstart?.isDate || false;
|
|
201
|
+
const start = dtstart ? (allDay ? dtstart.toString() : dtstart.toJSDate().toISOString()) : "";
|
|
202
|
+
const end = dtend ? (allDay ? dtend.toString() : dtend.toJSDate().toISOString()) : start;
|
|
203
|
+
|
|
204
|
+
const attendeeProps = vevent.getAllProperties("attendee");
|
|
205
|
+
const attendees = attendeeProps.map((a) => {
|
|
206
|
+
const cn = a.getParameter("cn");
|
|
207
|
+
const val = (a.getFirstValue() as string || "").replace("mailto:", "");
|
|
208
|
+
return cn ? `${cn} <${val}>` : val;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
events.push({
|
|
212
|
+
uid: event.uid || "",
|
|
213
|
+
summary: event.summary || "(No title)",
|
|
214
|
+
description: event.description || undefined,
|
|
215
|
+
location: event.location || undefined,
|
|
216
|
+
start,
|
|
217
|
+
end,
|
|
218
|
+
allDay,
|
|
219
|
+
status: vevent.getFirstPropertyValue("status") as string || undefined,
|
|
220
|
+
organizer: (vevent.getFirstPropertyValue("organizer") as string || "").replace("mailto:", "") || undefined,
|
|
221
|
+
attendees: attendees.length > 0 ? attendees : undefined,
|
|
222
|
+
calendarName,
|
|
223
|
+
accountId,
|
|
224
|
+
accountName,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error("[calendar-service] ICS parse error:", err);
|
|
229
|
+
}
|
|
230
|
+
return events;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Fetch events from a specific date range */
|
|
234
|
+
export async function listEvents(
|
|
235
|
+
account: CalendarAccount,
|
|
236
|
+
opts: { from?: string; to?: string; calendarUrl?: string } = {},
|
|
237
|
+
): Promise<CalendarEvent[]> {
|
|
238
|
+
const client = await createClient(account);
|
|
239
|
+
const allCalendars = await client.fetchCalendars();
|
|
240
|
+
const calendars = allCalendars.filter(isEventCalendar);
|
|
241
|
+
|
|
242
|
+
// Filter to specific calendar if requested
|
|
243
|
+
const targetCalendars = opts.calendarUrl
|
|
244
|
+
? calendars.filter((c) => c.url === opts.calendarUrl)
|
|
245
|
+
: calendars;
|
|
246
|
+
|
|
247
|
+
if (targetCalendars.length === 0) return [];
|
|
248
|
+
|
|
249
|
+
// Default: today to 7 days ahead
|
|
250
|
+
const now = new Date();
|
|
251
|
+
const from = opts.from ? new Date(opts.from) : new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
252
|
+
const to = opts.to ? new Date(opts.to) : new Date(from.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
253
|
+
|
|
254
|
+
const allEvents: CalendarEvent[] = [];
|
|
255
|
+
|
|
256
|
+
for (const cal of targetCalendars) {
|
|
257
|
+
try {
|
|
258
|
+
const objects = await client.fetchCalendarObjects({
|
|
259
|
+
calendar: cal as DAVCalendar,
|
|
260
|
+
timeRange: {
|
|
261
|
+
start: from.toISOString(),
|
|
262
|
+
end: to.toISOString(),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
for (const obj of objects) {
|
|
267
|
+
if (obj.data) {
|
|
268
|
+
const calName = typeof cal.displayName === "string" ? cal.displayName : undefined;
|
|
269
|
+
const events = parseICS(obj.data, account.id, account.name, calName);
|
|
270
|
+
allEvents.push(...events);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error(`[calendar-service] Error fetching from calendar "${cal.displayName}":`, err);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Sort by start time
|
|
279
|
+
allEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
280
|
+
return allEvents;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Create a new calendar event */
|
|
284
|
+
export async function createEvent(
|
|
285
|
+
account: CalendarAccount,
|
|
286
|
+
event: {
|
|
287
|
+
summary: string;
|
|
288
|
+
description?: string;
|
|
289
|
+
location?: string;
|
|
290
|
+
start: string; // ISO datetime or YYYY-MM-DD for all-day
|
|
291
|
+
end: string; // ISO datetime or YYYY-MM-DD for all-day
|
|
292
|
+
allDay?: boolean;
|
|
293
|
+
calendarUrl?: string;
|
|
294
|
+
},
|
|
295
|
+
): Promise<{ success: boolean; uid: string }> {
|
|
296
|
+
const client = await createClient(account);
|
|
297
|
+
const allCalendars = await client.fetchCalendars();
|
|
298
|
+
const calendars = allCalendars.filter(isEventCalendar);
|
|
299
|
+
|
|
300
|
+
// Pick target calendar (only event calendars, never Reminders/VTODO)
|
|
301
|
+
let targetCal = event.calendarUrl
|
|
302
|
+
? calendars.find((c) => c.url === event.calendarUrl)
|
|
303
|
+
: (account.defaultCalendarId
|
|
304
|
+
? calendars.find((c) => c.url === account.defaultCalendarId)
|
|
305
|
+
: calendars[0]);
|
|
306
|
+
|
|
307
|
+
if (!targetCal) targetCal = calendars[0];
|
|
308
|
+
if (!targetCal) throw new Error("No calendar found on this account");
|
|
309
|
+
|
|
310
|
+
const uid = `${Date.now()}-${Math.random().toString(36).slice(2)}@heyhank`;
|
|
311
|
+
|
|
312
|
+
// Build ICS
|
|
313
|
+
const dtStart = event.allDay
|
|
314
|
+
? `;VALUE=DATE:${event.start.replace(/-/g, "")}`
|
|
315
|
+
: `:${new Date(event.start).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "")}`;
|
|
316
|
+
const dtEnd = event.allDay
|
|
317
|
+
? `;VALUE=DATE:${event.end.replace(/-/g, "")}`
|
|
318
|
+
: `:${new Date(event.end).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "")}`;
|
|
319
|
+
|
|
320
|
+
const lines = [
|
|
321
|
+
"BEGIN:VCALENDAR",
|
|
322
|
+
"VERSION:2.0",
|
|
323
|
+
"PRODID:-//HeyHank//Calendar//EN",
|
|
324
|
+
"BEGIN:VEVENT",
|
|
325
|
+
`UID:${uid}`,
|
|
326
|
+
`DTSTART${dtStart}`,
|
|
327
|
+
`DTEND${dtEnd}`,
|
|
328
|
+
`SUMMARY:${escapeICS(event.summary)}`,
|
|
329
|
+
];
|
|
330
|
+
if (event.description) lines.push(`DESCRIPTION:${escapeICS(event.description)}`);
|
|
331
|
+
if (event.location) lines.push(`LOCATION:${escapeICS(event.location)}`);
|
|
332
|
+
lines.push(`DTSTAMP:${new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "")}`);
|
|
333
|
+
lines.push("END:VEVENT", "END:VCALENDAR");
|
|
334
|
+
|
|
335
|
+
const icsData = lines.join("\r\n");
|
|
336
|
+
|
|
337
|
+
await client.createCalendarObject({
|
|
338
|
+
calendar: targetCal as DAVCalendar,
|
|
339
|
+
filename: `${uid}.ics`,
|
|
340
|
+
iCalString: icsData,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return { success: true, uid };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Delete a calendar event by UID */
|
|
347
|
+
export async function deleteEvent(
|
|
348
|
+
account: CalendarAccount,
|
|
349
|
+
uid: string,
|
|
350
|
+
): Promise<boolean> {
|
|
351
|
+
const client = await createClient(account);
|
|
352
|
+
const allCalendars = await client.fetchCalendars();
|
|
353
|
+
const calendars = allCalendars.filter(isEventCalendar);
|
|
354
|
+
|
|
355
|
+
for (const cal of calendars) {
|
|
356
|
+
try {
|
|
357
|
+
const objects = await client.fetchCalendarObjects({ calendar: cal as DAVCalendar });
|
|
358
|
+
for (const obj of objects) {
|
|
359
|
+
if (obj.data?.includes(uid) && obj.url) {
|
|
360
|
+
await client.deleteCalendarObject({ calendarObject: { url: obj.url, etag: obj.etag || "" } });
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch { /* continue searching */ }
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Search events by text query */
|
|
370
|
+
export async function searchEvents(
|
|
371
|
+
account: CalendarAccount,
|
|
372
|
+
query: string,
|
|
373
|
+
opts: { from?: string; to?: string } = {},
|
|
374
|
+
): Promise<CalendarEvent[]> {
|
|
375
|
+
// CalDAV text search isn't universally supported, so fetch and filter locally
|
|
376
|
+
const now = new Date();
|
|
377
|
+
const from = opts.from || new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
|
|
378
|
+
const to = opts.to || new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(); // 30 days
|
|
379
|
+
|
|
380
|
+
const events = await listEvents(account, { from, to });
|
|
381
|
+
const q = query.toLowerCase();
|
|
382
|
+
return events.filter((e) =>
|
|
383
|
+
e.summary.toLowerCase().includes(q) ||
|
|
384
|
+
(e.description || "").toLowerCase().includes(q) ||
|
|
385
|
+
(e.location || "").toLowerCase().includes(q),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Get a summary of upcoming events across all accounts */
|
|
390
|
+
export async function getUpcomingSummary(): Promise<{
|
|
391
|
+
account: string;
|
|
392
|
+
todayCount: number;
|
|
393
|
+
weekCount: number;
|
|
394
|
+
nextEvent?: { summary: string; start: string };
|
|
395
|
+
}[]> {
|
|
396
|
+
const accounts = loadAccounts();
|
|
397
|
+
const results = [];
|
|
398
|
+
const now = new Date();
|
|
399
|
+
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
400
|
+
const weekEnd = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
401
|
+
|
|
402
|
+
for (const account of accounts) {
|
|
403
|
+
try {
|
|
404
|
+
const weekEvents = await listEvents(account, {
|
|
405
|
+
from: now.toISOString(),
|
|
406
|
+
to: weekEnd.toISOString(),
|
|
407
|
+
});
|
|
408
|
+
const todayEvents = weekEvents.filter((e) => new Date(e.start) < todayEnd);
|
|
409
|
+
results.push({
|
|
410
|
+
account: account.name,
|
|
411
|
+
todayCount: todayEvents.length,
|
|
412
|
+
weekCount: weekEvents.length,
|
|
413
|
+
nextEvent: weekEvents[0] ? { summary: weekEvents[0].summary, start: weekEvents[0].start } : undefined,
|
|
414
|
+
});
|
|
415
|
+
} catch (err) {
|
|
416
|
+
results.push({
|
|
417
|
+
account: account.name,
|
|
418
|
+
todayCount: 0,
|
|
419
|
+
weekCount: 0,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
function escapeICS(text: string): string {
|
|
429
|
+
return text
|
|
430
|
+
.replace(/\\/g, "\\\\")
|
|
431
|
+
.replace(/;/g, "\\;")
|
|
432
|
+
.replace(/,/g, "\\,")
|
|
433
|
+
.replace(/\n/g, "\\n");
|
|
434
|
+
}
|