calendar-mcp 1.0.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 +159 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +261 -0
- package/dist/tools/create-event.d.ts +7 -0
- package/dist/tools/create-event.js +57 -0
- package/dist/tools/delete-event.d.ts +7 -0
- package/dist/tools/delete-event.js +29 -0
- package/dist/tools/find-free.d.ts +7 -0
- package/dist/tools/find-free.js +119 -0
- package/dist/tools/list-events.d.ts +11 -0
- package/dist/tools/list-events.js +46 -0
- package/dist/tools/search-events.d.ts +7 -0
- package/dist/tools/search-events.js +43 -0
- package/dist/tools/update-event.d.ts +7 -0
- package/dist/tools/update-event.js +64 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +1 -0
- package/dist/utils/dates.d.ts +58 -0
- package/dist/utils/dates.js +145 -0
- package/dist/utils/git.d.ts +35 -0
- package/dist/utils/git.js +88 -0
- package/dist/utils/ical.d.ts +47 -0
- package/dist/utils/ical.js +249 -0
- package/dist/utils/paths.d.ts +21 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +60 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readCalendarFile } from '../utils/ical.js';
|
|
2
|
+
import { parseNaturalDate } from '../utils/dates.js';
|
|
3
|
+
const DEFAULT_LIMIT = 50;
|
|
4
|
+
/**
|
|
5
|
+
* List calendar events with optional filtering
|
|
6
|
+
* @param params - Filter parameters
|
|
7
|
+
* @returns List of events with count and hasMore indicator
|
|
8
|
+
*/
|
|
9
|
+
export async function listEvents(params = {}) {
|
|
10
|
+
// Read all events
|
|
11
|
+
let events = await readCalendarFile();
|
|
12
|
+
// Filter by start date
|
|
13
|
+
if (params.startDate) {
|
|
14
|
+
const startFilter = parseNaturalDate(params.startDate);
|
|
15
|
+
events = events.filter(event => new Date(event.start) >= startFilter);
|
|
16
|
+
}
|
|
17
|
+
// Filter by end date
|
|
18
|
+
if (params.endDate) {
|
|
19
|
+
const endFilter = parseNaturalDate(params.endDate);
|
|
20
|
+
events = events.filter(event => new Date(event.start) <= endFilter);
|
|
21
|
+
}
|
|
22
|
+
// Sort by start date
|
|
23
|
+
const sortOrder = params.sortOrder ?? 'asc';
|
|
24
|
+
events.sort((a, b) => {
|
|
25
|
+
const dateA = new Date(a.start).getTime();
|
|
26
|
+
const dateB = new Date(b.start).getTime();
|
|
27
|
+
return sortOrder === 'asc' ? dateA - dateB : dateB - dateA;
|
|
28
|
+
});
|
|
29
|
+
// Calculate total count before limiting
|
|
30
|
+
const totalCount = events.length;
|
|
31
|
+
// Apply limit
|
|
32
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
33
|
+
const hasMore = events.length > limit;
|
|
34
|
+
events = events.slice(0, limit);
|
|
35
|
+
return {
|
|
36
|
+
events,
|
|
37
|
+
count: events.length,
|
|
38
|
+
hasMore,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get all events without filtering (internal use)
|
|
43
|
+
*/
|
|
44
|
+
export async function getAllEvents() {
|
|
45
|
+
return readCalendarFile();
|
|
46
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SearchEventsParams, CalendarEvent } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Search for events matching criteria
|
|
4
|
+
* @param params - Search parameters
|
|
5
|
+
* @returns List of matching events
|
|
6
|
+
*/
|
|
7
|
+
export declare function searchEvents(params?: SearchEventsParams): Promise<CalendarEvent[]>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readCalendarFile } from '../utils/ical.js';
|
|
2
|
+
import { parseNaturalDate } from '../utils/dates.js';
|
|
3
|
+
const DEFAULT_LIMIT = 20;
|
|
4
|
+
/**
|
|
5
|
+
* Search for events matching criteria
|
|
6
|
+
* @param params - Search parameters
|
|
7
|
+
* @returns List of matching events
|
|
8
|
+
*/
|
|
9
|
+
export async function searchEvents(params = {}) {
|
|
10
|
+
// Read all events
|
|
11
|
+
let events = await readCalendarFile();
|
|
12
|
+
// Text search (case-insensitive)
|
|
13
|
+
if (params.query) {
|
|
14
|
+
const query = params.query.toLowerCase();
|
|
15
|
+
events = events.filter(event => {
|
|
16
|
+
const titleMatch = event.title.toLowerCase().includes(query);
|
|
17
|
+
const descriptionMatch = event.description?.toLowerCase().includes(query) ?? false;
|
|
18
|
+
const locationMatch = event.location?.toLowerCase().includes(query) ?? false;
|
|
19
|
+
return titleMatch || descriptionMatch || locationMatch;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Filter by start date
|
|
23
|
+
if (params.startDate) {
|
|
24
|
+
const startFilter = parseNaturalDate(params.startDate);
|
|
25
|
+
events = events.filter(event => new Date(event.start) >= startFilter);
|
|
26
|
+
}
|
|
27
|
+
// Filter by end date
|
|
28
|
+
if (params.endDate) {
|
|
29
|
+
const endFilter = parseNaturalDate(params.endDate);
|
|
30
|
+
events = events.filter(event => new Date(event.start) <= endFilter);
|
|
31
|
+
}
|
|
32
|
+
// Filter by location (case-insensitive contains)
|
|
33
|
+
if (params.location) {
|
|
34
|
+
const locationQuery = params.location.toLowerCase();
|
|
35
|
+
events = events.filter(event => event.location?.toLowerCase().includes(locationQuery) ?? false);
|
|
36
|
+
}
|
|
37
|
+
// Sort by start date (most recent first for search results)
|
|
38
|
+
events.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
39
|
+
// Apply limit
|
|
40
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
41
|
+
events = events.slice(0, limit);
|
|
42
|
+
return events;
|
|
43
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UpdateEventParams } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Update an existing calendar event
|
|
4
|
+
* @param params - Update parameters (id required, other fields optional)
|
|
5
|
+
* @returns Success message
|
|
6
|
+
*/
|
|
7
|
+
export declare function updateEvent(params: UpdateEventParams): Promise<string>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readCalendarFile, writeCalendarFile } from '../utils/ical.js';
|
|
2
|
+
import { parseNaturalDate, toISO, formatForDisplay } from '../utils/dates.js';
|
|
3
|
+
import { commitAndPush } from '../utils/git.js';
|
|
4
|
+
/**
|
|
5
|
+
* Update an existing calendar event
|
|
6
|
+
* @param params - Update parameters (id required, other fields optional)
|
|
7
|
+
* @returns Success message
|
|
8
|
+
*/
|
|
9
|
+
export async function updateEvent(params) {
|
|
10
|
+
// Validate required field
|
|
11
|
+
if (!params.id) {
|
|
12
|
+
throw new Error('Event ID is required');
|
|
13
|
+
}
|
|
14
|
+
// Read existing events
|
|
15
|
+
const events = await readCalendarFile();
|
|
16
|
+
// Find the event to update
|
|
17
|
+
const eventIndex = events.findIndex(e => e.id === params.id);
|
|
18
|
+
if (eventIndex === -1) {
|
|
19
|
+
throw new Error(`Event not found: ${params.id}`);
|
|
20
|
+
}
|
|
21
|
+
const event = events[eventIndex];
|
|
22
|
+
const originalTitle = event.title;
|
|
23
|
+
// Update fields if provided
|
|
24
|
+
if (params.title !== undefined) {
|
|
25
|
+
if (params.title.trim() === '') {
|
|
26
|
+
throw new Error('Event title cannot be empty');
|
|
27
|
+
}
|
|
28
|
+
event.title = params.title.trim();
|
|
29
|
+
}
|
|
30
|
+
if (params.start !== undefined) {
|
|
31
|
+
const startDate = parseNaturalDate(params.start);
|
|
32
|
+
event.start = toISO(startDate);
|
|
33
|
+
}
|
|
34
|
+
if (params.end !== undefined) {
|
|
35
|
+
const endDate = parseNaturalDate(params.end);
|
|
36
|
+
event.end = toISO(endDate);
|
|
37
|
+
}
|
|
38
|
+
if (params.location !== undefined) {
|
|
39
|
+
event.location = params.location.trim() || undefined;
|
|
40
|
+
}
|
|
41
|
+
if (params.description !== undefined) {
|
|
42
|
+
event.description = params.description.trim() || undefined;
|
|
43
|
+
}
|
|
44
|
+
// Validate end is after start if both are set
|
|
45
|
+
if (event.end) {
|
|
46
|
+
const startDate = new Date(event.start);
|
|
47
|
+
const endDate = new Date(event.end);
|
|
48
|
+
if (endDate <= startDate) {
|
|
49
|
+
throw new Error('End date/time must be after start date/time');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Update lastModified
|
|
53
|
+
event.lastModified = toISO(new Date());
|
|
54
|
+
// Update in array
|
|
55
|
+
events[eventIndex] = event;
|
|
56
|
+
// Write back to file
|
|
57
|
+
await writeCalendarFile(events);
|
|
58
|
+
// Commit changes
|
|
59
|
+
await commitAndPush(`Update event: ${event.title}`);
|
|
60
|
+
// Return success message
|
|
61
|
+
const startDate = new Date(event.start);
|
|
62
|
+
const displayDate = formatForDisplay(startDate, event.allDay);
|
|
63
|
+
return `Updated "${originalTitle}" - now "${event.title}" on ${displayDate}`;
|
|
64
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar event representation (internal format)
|
|
3
|
+
*/
|
|
4
|
+
export interface CalendarEvent {
|
|
5
|
+
/** Unique identifier (UID from iCal, without domain suffix) */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Event title/summary */
|
|
8
|
+
title: string;
|
|
9
|
+
/** Start date/time in ISO 8601 format */
|
|
10
|
+
start: string;
|
|
11
|
+
/** End date/time in ISO 8601 format (optional) */
|
|
12
|
+
end?: string;
|
|
13
|
+
/** Event location (optional) */
|
|
14
|
+
location?: string;
|
|
15
|
+
/** Event description (optional) */
|
|
16
|
+
description?: string;
|
|
17
|
+
/** Whether this is an all-day event */
|
|
18
|
+
allDay: boolean;
|
|
19
|
+
/** Event status: TENTATIVE, CONFIRMED, or CANCELLED */
|
|
20
|
+
status: EventStatus;
|
|
21
|
+
/** Creation timestamp in ISO 8601 format */
|
|
22
|
+
created?: string;
|
|
23
|
+
/** Last modification timestamp in ISO 8601 format */
|
|
24
|
+
lastModified?: string;
|
|
25
|
+
}
|
|
26
|
+
export type EventStatus = 'TENTATIVE' | 'CONFIRMED' | 'CANCELLED';
|
|
27
|
+
/**
|
|
28
|
+
* iCalendar VEVENT component fields
|
|
29
|
+
*/
|
|
30
|
+
export interface ICalEvent {
|
|
31
|
+
uid: string;
|
|
32
|
+
dtstamp: Date;
|
|
33
|
+
dtstart: Date;
|
|
34
|
+
dtend?: Date;
|
|
35
|
+
summary: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
location?: string;
|
|
38
|
+
status?: EventStatus;
|
|
39
|
+
created?: Date;
|
|
40
|
+
lastModified?: Date;
|
|
41
|
+
allDay: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parsed iCalendar file structure
|
|
45
|
+
*/
|
|
46
|
+
export interface ICalendar {
|
|
47
|
+
prodId: string;
|
|
48
|
+
version: string;
|
|
49
|
+
calName?: string;
|
|
50
|
+
timezone?: string;
|
|
51
|
+
events: ICalEvent[];
|
|
52
|
+
}
|
|
53
|
+
export interface CreateEventParams {
|
|
54
|
+
/** Event title/summary (required) */
|
|
55
|
+
title: string;
|
|
56
|
+
/** Start date/time - ISO 8601 or natural language (required) */
|
|
57
|
+
start: string;
|
|
58
|
+
/** End date/time - ISO 8601 or natural language (optional) */
|
|
59
|
+
end?: string;
|
|
60
|
+
/** Event location (optional) */
|
|
61
|
+
location?: string;
|
|
62
|
+
/** Event description (optional) */
|
|
63
|
+
description?: string;
|
|
64
|
+
/** Whether this is an all-day event (optional, default: false) */
|
|
65
|
+
allDay?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface ListEventsParams {
|
|
68
|
+
/** Filter events starting after this date (optional) */
|
|
69
|
+
startDate?: string;
|
|
70
|
+
/** Filter events ending before this date (optional) */
|
|
71
|
+
endDate?: string;
|
|
72
|
+
/** Maximum number of events to return (default: 50) */
|
|
73
|
+
limit?: number;
|
|
74
|
+
/** Sort order by start date (default: 'asc') */
|
|
75
|
+
sortOrder?: 'asc' | 'desc';
|
|
76
|
+
}
|
|
77
|
+
export interface UpdateEventParams {
|
|
78
|
+
/** Event ID to update (required) */
|
|
79
|
+
id: string;
|
|
80
|
+
/** New title (optional) */
|
|
81
|
+
title?: string;
|
|
82
|
+
/** New start date/time (optional) */
|
|
83
|
+
start?: string;
|
|
84
|
+
/** New end date/time (optional) */
|
|
85
|
+
end?: string;
|
|
86
|
+
/** New location (optional) */
|
|
87
|
+
location?: string;
|
|
88
|
+
/** New description (optional) */
|
|
89
|
+
description?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface DeleteEventParams {
|
|
92
|
+
/** Event ID to delete (required) */
|
|
93
|
+
id: string;
|
|
94
|
+
}
|
|
95
|
+
export interface FindFreeTimeParams {
|
|
96
|
+
/** Required duration in minutes */
|
|
97
|
+
duration: number;
|
|
98
|
+
/** Start of search range - ISO 8601 or natural language */
|
|
99
|
+
startDate: string;
|
|
100
|
+
/** End of search range - ISO 8601 or natural language */
|
|
101
|
+
endDate: string;
|
|
102
|
+
/** Only search during working hours 9am-5pm (default: true) */
|
|
103
|
+
workingHoursOnly?: boolean;
|
|
104
|
+
/** Maximum number of slots to return (default: 5) */
|
|
105
|
+
maxResults?: number;
|
|
106
|
+
}
|
|
107
|
+
export interface SearchEventsParams {
|
|
108
|
+
/** Search text - searches title, description, location */
|
|
109
|
+
query?: string;
|
|
110
|
+
/** Filter events after this date (optional) */
|
|
111
|
+
startDate?: string;
|
|
112
|
+
/** Filter events before this date (optional) */
|
|
113
|
+
endDate?: string;
|
|
114
|
+
/** Filter by location (optional) */
|
|
115
|
+
location?: string;
|
|
116
|
+
/** Maximum results (default: 20) */
|
|
117
|
+
limit?: number;
|
|
118
|
+
}
|
|
119
|
+
export interface ListEventsResult {
|
|
120
|
+
events: CalendarEvent[];
|
|
121
|
+
count: number;
|
|
122
|
+
hasMore: boolean;
|
|
123
|
+
}
|
|
124
|
+
export interface FreeSlot {
|
|
125
|
+
/** Start of free slot in ISO 8601 format */
|
|
126
|
+
start: string;
|
|
127
|
+
/** End of free slot in ISO 8601 format */
|
|
128
|
+
end: string;
|
|
129
|
+
/** Duration in minutes */
|
|
130
|
+
durationMinutes: number;
|
|
131
|
+
}
|
|
132
|
+
export interface FindFreeTimeResult {
|
|
133
|
+
freeSlots: FreeSlot[];
|
|
134
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a date string that can be either ISO 8601 or natural language
|
|
3
|
+
* @param input - Date string (e.g., "2025-01-25T14:00:00Z" or "tomorrow at 2pm")
|
|
4
|
+
* @param referenceDate - Reference date for natural language parsing (defaults to now)
|
|
5
|
+
* @returns Parsed Date object
|
|
6
|
+
* @throws Error if the date cannot be parsed
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseNaturalDate(input: string, referenceDate?: Date): Date;
|
|
9
|
+
/**
|
|
10
|
+
* Format a Date object to iCalendar DATE-TIME format (UTC)
|
|
11
|
+
* Format: YYYYMMDDTHHMMSSZ
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatICalDateTime(date: Date): string;
|
|
14
|
+
/**
|
|
15
|
+
* Format a Date object to iCalendar DATE format (for all-day events)
|
|
16
|
+
* Format: YYYYMMDD
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatICalDate(date: Date): string;
|
|
19
|
+
/**
|
|
20
|
+
* Parse an iCalendar DATE-TIME or DATE string to a Date object
|
|
21
|
+
* Supports: YYYYMMDDTHHMMSSZ (UTC) or YYYYMMDD (date only)
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseICalDate(icalDate: string): {
|
|
24
|
+
date: Date;
|
|
25
|
+
allDay: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Add a duration in minutes to a date
|
|
29
|
+
*/
|
|
30
|
+
export declare function addMinutes(date: Date, minutes: number): Date;
|
|
31
|
+
/**
|
|
32
|
+
* Add hours to a date
|
|
33
|
+
*/
|
|
34
|
+
export declare function addHours(date: Date, hours: number): Date;
|
|
35
|
+
/**
|
|
36
|
+
* Add days to a date
|
|
37
|
+
*/
|
|
38
|
+
export declare function addDays(date: Date, days: number): Date;
|
|
39
|
+
/**
|
|
40
|
+
* Format a Date to ISO 8601 string
|
|
41
|
+
*/
|
|
42
|
+
export declare function toISO(date: Date): string;
|
|
43
|
+
/**
|
|
44
|
+
* Get the start of a day (midnight UTC)
|
|
45
|
+
*/
|
|
46
|
+
export declare function startOfDay(date: Date): Date;
|
|
47
|
+
/**
|
|
48
|
+
* Get the end of a day (23:59:59.999 UTC)
|
|
49
|
+
*/
|
|
50
|
+
export declare function endOfDay(date: Date): Date;
|
|
51
|
+
/**
|
|
52
|
+
* Check if two time ranges overlap
|
|
53
|
+
*/
|
|
54
|
+
export declare function rangesOverlap(start1: Date, end1: Date, start2: Date, end2: Date): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Format a date for human-readable display
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatForDisplay(date: Date, allDay: boolean): string;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as chrono from 'chrono-node';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a date string that can be either ISO 8601 or natural language
|
|
4
|
+
* @param input - Date string (e.g., "2025-01-25T14:00:00Z" or "tomorrow at 2pm")
|
|
5
|
+
* @param referenceDate - Reference date for natural language parsing (defaults to now)
|
|
6
|
+
* @returns Parsed Date object
|
|
7
|
+
* @throws Error if the date cannot be parsed
|
|
8
|
+
*/
|
|
9
|
+
export function parseNaturalDate(input, referenceDate) {
|
|
10
|
+
// First try parsing as ISO 8601
|
|
11
|
+
const isoDate = new Date(input);
|
|
12
|
+
if (!isNaN(isoDate.getTime()) && input.includes('-')) {
|
|
13
|
+
return isoDate;
|
|
14
|
+
}
|
|
15
|
+
// Try natural language parsing with chrono
|
|
16
|
+
const ref = referenceDate ?? new Date();
|
|
17
|
+
const results = chrono.parse(input, ref, { forwardDate: true });
|
|
18
|
+
if (results.length === 0) {
|
|
19
|
+
throw new Error(`Could not parse date: "${input}"`);
|
|
20
|
+
}
|
|
21
|
+
const parsed = results[0].start.date();
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Format a Date object to iCalendar DATE-TIME format (UTC)
|
|
26
|
+
* Format: YYYYMMDDTHHMMSSZ
|
|
27
|
+
*/
|
|
28
|
+
export function formatICalDateTime(date) {
|
|
29
|
+
const year = date.getUTCFullYear();
|
|
30
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
31
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
32
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
33
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
34
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
35
|
+
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Format a Date object to iCalendar DATE format (for all-day events)
|
|
39
|
+
* Format: YYYYMMDD
|
|
40
|
+
*/
|
|
41
|
+
export function formatICalDate(date) {
|
|
42
|
+
const year = date.getUTCFullYear();
|
|
43
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
44
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
45
|
+
return `${year}${month}${day}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse an iCalendar DATE-TIME or DATE string to a Date object
|
|
49
|
+
* Supports: YYYYMMDDTHHMMSSZ (UTC) or YYYYMMDD (date only)
|
|
50
|
+
*/
|
|
51
|
+
export function parseICalDate(icalDate) {
|
|
52
|
+
// Remove any VALUE=DATE: prefix
|
|
53
|
+
const cleanDate = icalDate.replace(/^VALUE=DATE:?/, '');
|
|
54
|
+
// All-day format: YYYYMMDD
|
|
55
|
+
if (cleanDate.length === 8 && !cleanDate.includes('T')) {
|
|
56
|
+
const year = parseInt(cleanDate.slice(0, 4), 10);
|
|
57
|
+
const month = parseInt(cleanDate.slice(4, 6), 10) - 1;
|
|
58
|
+
const day = parseInt(cleanDate.slice(6, 8), 10);
|
|
59
|
+
return { date: new Date(Date.UTC(year, month, day)), allDay: true };
|
|
60
|
+
}
|
|
61
|
+
// DateTime format: YYYYMMDDTHHMMSSZ or YYYYMMDDTHHMMSS
|
|
62
|
+
if (cleanDate.includes('T')) {
|
|
63
|
+
const datePart = cleanDate.slice(0, 8);
|
|
64
|
+
const timePart = cleanDate.slice(9, 15);
|
|
65
|
+
const isUtc = cleanDate.endsWith('Z');
|
|
66
|
+
const year = parseInt(datePart.slice(0, 4), 10);
|
|
67
|
+
const month = parseInt(datePart.slice(4, 6), 10) - 1;
|
|
68
|
+
const day = parseInt(datePart.slice(6, 8), 10);
|
|
69
|
+
const hours = parseInt(timePart.slice(0, 2), 10);
|
|
70
|
+
const minutes = parseInt(timePart.slice(2, 4), 10);
|
|
71
|
+
const seconds = parseInt(timePart.slice(4, 6), 10);
|
|
72
|
+
if (isUtc) {
|
|
73
|
+
return { date: new Date(Date.UTC(year, month, day, hours, minutes, seconds)), allDay: false };
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
return { date: new Date(year, month, day, hours, minutes, seconds), allDay: false };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Invalid iCal date format: "${icalDate}"`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Add a duration in minutes to a date
|
|
83
|
+
*/
|
|
84
|
+
export function addMinutes(date, minutes) {
|
|
85
|
+
return new Date(date.getTime() + minutes * 60 * 1000);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Add hours to a date
|
|
89
|
+
*/
|
|
90
|
+
export function addHours(date, hours) {
|
|
91
|
+
return addMinutes(date, hours * 60);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Add days to a date
|
|
95
|
+
*/
|
|
96
|
+
export function addDays(date, days) {
|
|
97
|
+
return addMinutes(date, days * 24 * 60);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Format a Date to ISO 8601 string
|
|
101
|
+
*/
|
|
102
|
+
export function toISO(date) {
|
|
103
|
+
return date.toISOString();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get the start of a day (midnight UTC)
|
|
107
|
+
*/
|
|
108
|
+
export function startOfDay(date) {
|
|
109
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get the end of a day (23:59:59.999 UTC)
|
|
113
|
+
*/
|
|
114
|
+
export function endOfDay(date) {
|
|
115
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 23, 59, 59, 999));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if two time ranges overlap
|
|
119
|
+
*/
|
|
120
|
+
export function rangesOverlap(start1, end1, start2, end2) {
|
|
121
|
+
return start1 < end2 && end1 > start2;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Format a date for human-readable display
|
|
125
|
+
*/
|
|
126
|
+
export function formatForDisplay(date, allDay) {
|
|
127
|
+
if (allDay) {
|
|
128
|
+
return date.toLocaleDateString('en-US', {
|
|
129
|
+
weekday: 'long',
|
|
130
|
+
year: 'numeric',
|
|
131
|
+
month: 'long',
|
|
132
|
+
day: 'numeric',
|
|
133
|
+
timeZone: 'UTC'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return date.toLocaleString('en-US', {
|
|
137
|
+
weekday: 'long',
|
|
138
|
+
year: 'numeric',
|
|
139
|
+
month: 'long',
|
|
140
|
+
day: 'numeric',
|
|
141
|
+
hour: 'numeric',
|
|
142
|
+
minute: '2-digit',
|
|
143
|
+
timeZoneName: 'short'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SimpleGit } from 'simple-git';
|
|
2
|
+
/**
|
|
3
|
+
* Get the SimpleGit instance for the vault
|
|
4
|
+
* Uses singleton pattern to reuse the same instance
|
|
5
|
+
*/
|
|
6
|
+
export declare function getGit(): SimpleGit;
|
|
7
|
+
/**
|
|
8
|
+
* Reset the git instance (useful for testing)
|
|
9
|
+
*/
|
|
10
|
+
export declare function resetGit(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Commit changes and push to remote
|
|
13
|
+
* @param message - Commit message
|
|
14
|
+
* @returns Success status and optional error message
|
|
15
|
+
*/
|
|
16
|
+
export declare function commitAndPush(message: string): Promise<{
|
|
17
|
+
success: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Get the current git status
|
|
22
|
+
*/
|
|
23
|
+
export declare function getStatus(): Promise<{
|
|
24
|
+
isRepo: boolean;
|
|
25
|
+
hasChanges: boolean;
|
|
26
|
+
ahead: number;
|
|
27
|
+
behind: number;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Pull latest changes from remote
|
|
31
|
+
*/
|
|
32
|
+
export declare function pull(): Promise<{
|
|
33
|
+
success: boolean;
|
|
34
|
+
error?: string;
|
|
35
|
+
}>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { simpleGit } from 'simple-git';
|
|
2
|
+
import { getVaultPath } from './paths.js';
|
|
3
|
+
let gitInstance = null;
|
|
4
|
+
/**
|
|
5
|
+
* Get the SimpleGit instance for the vault
|
|
6
|
+
* Uses singleton pattern to reuse the same instance
|
|
7
|
+
*/
|
|
8
|
+
export function getGit() {
|
|
9
|
+
if (!gitInstance) {
|
|
10
|
+
gitInstance = simpleGit(getVaultPath());
|
|
11
|
+
}
|
|
12
|
+
return gitInstance;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Reset the git instance (useful for testing)
|
|
16
|
+
*/
|
|
17
|
+
export function resetGit() {
|
|
18
|
+
gitInstance = null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Commit changes and push to remote
|
|
22
|
+
* @param message - Commit message
|
|
23
|
+
* @returns Success status and optional error message
|
|
24
|
+
*/
|
|
25
|
+
export async function commitAndPush(message) {
|
|
26
|
+
try {
|
|
27
|
+
const git = getGit();
|
|
28
|
+
// Check if there are changes to commit
|
|
29
|
+
const status = await git.status();
|
|
30
|
+
if (status.files.length === 0) {
|
|
31
|
+
return { success: true }; // Nothing to commit
|
|
32
|
+
}
|
|
33
|
+
// Add all changes in the Calendar folder
|
|
34
|
+
await git.add('Calendar/*');
|
|
35
|
+
// Commit with message
|
|
36
|
+
await git.commit(message);
|
|
37
|
+
// Try to push (may fail if no remote or offline)
|
|
38
|
+
try {
|
|
39
|
+
await git.push();
|
|
40
|
+
}
|
|
41
|
+
catch (pushError) {
|
|
42
|
+
// Push failed, but commit succeeded - that's okay for offline use
|
|
43
|
+
console.error('Push failed (may be offline):', pushError);
|
|
44
|
+
}
|
|
45
|
+
return { success: true };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
49
|
+
return { success: false, error: errorMessage };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get the current git status
|
|
54
|
+
*/
|
|
55
|
+
export async function getStatus() {
|
|
56
|
+
try {
|
|
57
|
+
const git = getGit();
|
|
58
|
+
const status = await git.status();
|
|
59
|
+
return {
|
|
60
|
+
isRepo: true,
|
|
61
|
+
hasChanges: status.files.length > 0,
|
|
62
|
+
ahead: status.ahead,
|
|
63
|
+
behind: status.behind,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return {
|
|
68
|
+
isRepo: false,
|
|
69
|
+
hasChanges: false,
|
|
70
|
+
ahead: 0,
|
|
71
|
+
behind: 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Pull latest changes from remote
|
|
77
|
+
*/
|
|
78
|
+
export async function pull() {
|
|
79
|
+
try {
|
|
80
|
+
const git = getGit();
|
|
81
|
+
await git.pull();
|
|
82
|
+
return { success: true };
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
86
|
+
return { success: false, error: errorMessage };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CalendarEvent } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a unique event ID
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateUID(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Extract the ID part from a UID (without domain)
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractId(uid: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Create a full UID from an ID
|
|
12
|
+
*/
|
|
13
|
+
export declare function createUID(id: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Escape special characters for iCal text fields
|
|
16
|
+
*/
|
|
17
|
+
export declare function escapeICalText(text: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Unescape special characters from iCal text fields
|
|
20
|
+
*/
|
|
21
|
+
export declare function unescapeICalText(text: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Fold long lines according to iCal spec (max 75 chars per line)
|
|
24
|
+
*/
|
|
25
|
+
export declare function foldLine(line: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Unfold lines that were folded according to iCal spec
|
|
28
|
+
*/
|
|
29
|
+
export declare function unfoldLines(content: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Parse iCalendar content to events
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseICalendar(content: string): CalendarEvent[];
|
|
34
|
+
/**
|
|
35
|
+
* Generate iCalendar content from events
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateICalendar(events: CalendarEvent[]): string;
|
|
38
|
+
/**
|
|
39
|
+
* Read the calendar file and return parsed events
|
|
40
|
+
* Returns empty array if file doesn't exist
|
|
41
|
+
*/
|
|
42
|
+
export declare function readCalendarFile(): Promise<CalendarEvent[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Write events to the calendar file
|
|
45
|
+
* Creates the Calendar directory if it doesn't exist
|
|
46
|
+
*/
|
|
47
|
+
export declare function writeCalendarFile(events: CalendarEvent[]): Promise<void>;
|