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
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Calendar MCP Server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for calendar management via Claude Code. Uses iCalendar format (RFC 5545) for data storage.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **6 Calendar Tools**: Create, list, update, delete, find free time, and search events
|
|
8
|
+
- **Natural Language Dates**: Supports inputs like "tomorrow at 2pm" or "next Tuesday"
|
|
9
|
+
- **iCalendar Format**: Standard `.ics` format for portability
|
|
10
|
+
- **Git Integration**: Automatic commits for change tracking
|
|
11
|
+
- **Multi-Device Support**: Configure per-device vault paths
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
Add to your Claude Code MCP settings (`~/.claude/settings.json`):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"calendar": {
|
|
28
|
+
"command": "node",
|
|
29
|
+
"args": ["/path/to/calendar-mcp/dist/index.js"],
|
|
30
|
+
"env": {
|
|
31
|
+
"VAULT_PATH": "/path/to/your/obsidian-vault"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The server creates and manages a `Calendar/events.ics` file within your vault.
|
|
39
|
+
|
|
40
|
+
## Tools
|
|
41
|
+
|
|
42
|
+
### calendar_create_event
|
|
43
|
+
|
|
44
|
+
Create a new calendar event.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Parameters:
|
|
48
|
+
- title (required): Event title
|
|
49
|
+
- start (required): Start date/time (ISO 8601 or natural language)
|
|
50
|
+
- end: End date/time (defaults to start + 1 hour)
|
|
51
|
+
- location: Event location
|
|
52
|
+
- description: Event description
|
|
53
|
+
- allDay: Whether this is an all-day event
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### calendar_list_events
|
|
57
|
+
|
|
58
|
+
List calendar events with optional filtering.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Parameters:
|
|
62
|
+
- startDate: Filter events after this date
|
|
63
|
+
- endDate: Filter events before this date
|
|
64
|
+
- limit: Maximum events to return (default: 50)
|
|
65
|
+
- sortOrder: 'asc' or 'desc' (default: 'asc')
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### calendar_update_event
|
|
69
|
+
|
|
70
|
+
Update an existing event.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
Parameters:
|
|
74
|
+
- id (required): Event ID to update
|
|
75
|
+
- title: New title
|
|
76
|
+
- start: New start date/time
|
|
77
|
+
- end: New end date/time
|
|
78
|
+
- location: New location
|
|
79
|
+
- description: New description
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### calendar_delete_event
|
|
83
|
+
|
|
84
|
+
Delete an event by ID.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Parameters:
|
|
88
|
+
- id (required): Event ID to delete
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### calendar_find_free_time
|
|
92
|
+
|
|
93
|
+
Find available time slots.
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Parameters:
|
|
97
|
+
- duration (required): Duration in minutes
|
|
98
|
+
- startDate (required): Start of search range
|
|
99
|
+
- endDate (required): End of search range
|
|
100
|
+
- workingHoursOnly: Only 9am-5pm (default: true)
|
|
101
|
+
- maxResults: Maximum slots to return (default: 5)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### calendar_search_events
|
|
105
|
+
|
|
106
|
+
Search for events by text or criteria.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Parameters:
|
|
110
|
+
- query: Search text (searches title, description, location)
|
|
111
|
+
- startDate: Filter after this date
|
|
112
|
+
- endDate: Filter before this date
|
|
113
|
+
- location: Filter by location
|
|
114
|
+
- limit: Maximum results (default: 20)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Install dependencies
|
|
121
|
+
npm install
|
|
122
|
+
|
|
123
|
+
# Build TypeScript
|
|
124
|
+
npm run build
|
|
125
|
+
|
|
126
|
+
# Run tests
|
|
127
|
+
npm test
|
|
128
|
+
|
|
129
|
+
# Run tests with coverage
|
|
130
|
+
npm run test:coverage
|
|
131
|
+
|
|
132
|
+
# Watch mode for development
|
|
133
|
+
npm run dev
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Data Format
|
|
137
|
+
|
|
138
|
+
Calendar data is stored in standard iCalendar format:
|
|
139
|
+
|
|
140
|
+
```ics
|
|
141
|
+
BEGIN:VCALENDAR
|
|
142
|
+
VERSION:2.0
|
|
143
|
+
PRODID:-//Calendar MCP//EN
|
|
144
|
+
BEGIN:VEVENT
|
|
145
|
+
UID:uuid@calendar.local
|
|
146
|
+
DTSTAMP:20250125T120000Z
|
|
147
|
+
DTSTART:20250125T140000Z
|
|
148
|
+
DTEND:20250125T150000Z
|
|
149
|
+
SUMMARY:Event Title
|
|
150
|
+
LOCATION:Event Location
|
|
151
|
+
DESCRIPTION:Event Description
|
|
152
|
+
STATUS:CONFIRMED
|
|
153
|
+
END:VEVENT
|
|
154
|
+
END:VCALENDAR
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { createEvent } from './tools/create-event.js';
|
|
6
|
+
import { listEvents } from './tools/list-events.js';
|
|
7
|
+
import { updateEvent } from './tools/update-event.js';
|
|
8
|
+
import { deleteEvent } from './tools/delete-event.js';
|
|
9
|
+
import { findFreeTime } from './tools/find-free.js';
|
|
10
|
+
import { searchEvents } from './tools/search-events.js';
|
|
11
|
+
const server = new Server({
|
|
12
|
+
name: 'calendar-mcp',
|
|
13
|
+
version: '1.0.0',
|
|
14
|
+
}, {
|
|
15
|
+
capabilities: {
|
|
16
|
+
tools: {},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
// List available tools
|
|
20
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
21
|
+
return {
|
|
22
|
+
tools: [
|
|
23
|
+
{
|
|
24
|
+
name: 'calendar_create_event',
|
|
25
|
+
description: 'Create a new calendar event. Supports natural language dates like "tomorrow at 2pm" or "next Tuesday".',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
title: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Event title/summary (required)',
|
|
32
|
+
},
|
|
33
|
+
start: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Start date/time - ISO 8601 format or natural language (required)',
|
|
36
|
+
},
|
|
37
|
+
end: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'End date/time - ISO 8601 format or natural language (optional, defaults to start + 1 hour)',
|
|
40
|
+
},
|
|
41
|
+
location: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Event location (optional)',
|
|
44
|
+
},
|
|
45
|
+
description: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Event description/notes (optional)',
|
|
48
|
+
},
|
|
49
|
+
allDay: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
description: 'Whether this is an all-day event (optional, default: false)',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: ['title', 'start'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'calendar_list_events',
|
|
59
|
+
description: 'List calendar events with optional filtering by date range.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
startDate: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Filter events starting after this date (optional)',
|
|
66
|
+
},
|
|
67
|
+
endDate: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Filter events ending before this date (optional)',
|
|
70
|
+
},
|
|
71
|
+
limit: {
|
|
72
|
+
type: 'number',
|
|
73
|
+
description: 'Maximum number of events to return (default: 50)',
|
|
74
|
+
},
|
|
75
|
+
sortOrder: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
enum: ['asc', 'desc'],
|
|
78
|
+
description: 'Sort order by start date (default: asc)',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'calendar_update_event',
|
|
85
|
+
description: 'Update an existing calendar event. Only provided fields will be updated.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
id: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Event ID to update (required)',
|
|
92
|
+
},
|
|
93
|
+
title: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'New event title (optional)',
|
|
96
|
+
},
|
|
97
|
+
start: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'New start date/time (optional)',
|
|
100
|
+
},
|
|
101
|
+
end: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'New end date/time (optional)',
|
|
104
|
+
},
|
|
105
|
+
location: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'New location (optional)',
|
|
108
|
+
},
|
|
109
|
+
description: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
description: 'New description (optional)',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ['id'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'calendar_delete_event',
|
|
119
|
+
description: 'Delete a calendar event by ID.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
id: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Event ID to delete (required)',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: ['id'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'calendar_find_free_time',
|
|
133
|
+
description: 'Find available time slots in the calendar that match the specified duration.',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
duration: {
|
|
138
|
+
type: 'number',
|
|
139
|
+
description: 'Required duration in minutes',
|
|
140
|
+
},
|
|
141
|
+
startDate: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'Start of search range - ISO 8601 or natural language',
|
|
144
|
+
},
|
|
145
|
+
endDate: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'End of search range - ISO 8601 or natural language',
|
|
148
|
+
},
|
|
149
|
+
workingHoursOnly: {
|
|
150
|
+
type: 'boolean',
|
|
151
|
+
description: 'Only search during working hours 9am-5pm (default: true)',
|
|
152
|
+
},
|
|
153
|
+
maxResults: {
|
|
154
|
+
type: 'number',
|
|
155
|
+
description: 'Maximum number of slots to return (default: 5)',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
required: ['duration', 'startDate', 'endDate'],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'calendar_search_events',
|
|
163
|
+
description: 'Search for events by text, date range, or location.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
query: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'Search text - searches title, description, and location',
|
|
170
|
+
},
|
|
171
|
+
startDate: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'Filter events after this date (optional)',
|
|
174
|
+
},
|
|
175
|
+
endDate: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
description: 'Filter events before this date (optional)',
|
|
178
|
+
},
|
|
179
|
+
location: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
description: 'Filter by location (optional)',
|
|
182
|
+
},
|
|
183
|
+
limit: {
|
|
184
|
+
type: 'number',
|
|
185
|
+
description: 'Maximum results (default: 20)',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
// Handle tool calls
|
|
194
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
195
|
+
const { name, arguments: args } = request.params;
|
|
196
|
+
try {
|
|
197
|
+
switch (name) {
|
|
198
|
+
case 'calendar_create_event': {
|
|
199
|
+
const result = await createEvent(args);
|
|
200
|
+
return { content: [{ type: 'text', text: result }] };
|
|
201
|
+
}
|
|
202
|
+
case 'calendar_list_events': {
|
|
203
|
+
const result = await listEvents((args ?? {}));
|
|
204
|
+
return {
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: 'text',
|
|
208
|
+
text: JSON.stringify(result, null, 2),
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
case 'calendar_update_event': {
|
|
214
|
+
const result = await updateEvent(args);
|
|
215
|
+
return { content: [{ type: 'text', text: result }] };
|
|
216
|
+
}
|
|
217
|
+
case 'calendar_delete_event': {
|
|
218
|
+
const result = await deleteEvent(args);
|
|
219
|
+
return { content: [{ type: 'text', text: result }] };
|
|
220
|
+
}
|
|
221
|
+
case 'calendar_find_free_time': {
|
|
222
|
+
const result = await findFreeTime(args);
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: 'text',
|
|
227
|
+
text: JSON.stringify(result, null, 2),
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
case 'calendar_search_events': {
|
|
233
|
+
const result = await searchEvents((args ?? {}));
|
|
234
|
+
return {
|
|
235
|
+
content: [
|
|
236
|
+
{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: JSON.stringify(result, null, 2),
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
|
251
|
+
isError: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// Start the server
|
|
256
|
+
async function main() {
|
|
257
|
+
const transport = new StdioServerTransport();
|
|
258
|
+
await server.connect(transport);
|
|
259
|
+
console.error('Calendar MCP server running');
|
|
260
|
+
}
|
|
261
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { readCalendarFile, writeCalendarFile } from '../utils/ical.js';
|
|
3
|
+
import { parseNaturalDate, addHours, toISO, formatForDisplay } from '../utils/dates.js';
|
|
4
|
+
import { commitAndPush } from '../utils/git.js';
|
|
5
|
+
/**
|
|
6
|
+
* Create a new calendar event
|
|
7
|
+
* @param params - Event parameters
|
|
8
|
+
* @returns Success message with event details
|
|
9
|
+
*/
|
|
10
|
+
export async function createEvent(params) {
|
|
11
|
+
// Validate required fields
|
|
12
|
+
if (!params.title || params.title.trim() === '') {
|
|
13
|
+
throw new Error('Event title is required');
|
|
14
|
+
}
|
|
15
|
+
if (!params.start) {
|
|
16
|
+
throw new Error('Event start date/time is required');
|
|
17
|
+
}
|
|
18
|
+
// Parse dates
|
|
19
|
+
const startDate = parseNaturalDate(params.start);
|
|
20
|
+
let endDate;
|
|
21
|
+
if (params.end) {
|
|
22
|
+
endDate = parseNaturalDate(params.end);
|
|
23
|
+
}
|
|
24
|
+
else if (!params.allDay) {
|
|
25
|
+
// Default to 1 hour duration for non-all-day events
|
|
26
|
+
endDate = addHours(startDate, 1);
|
|
27
|
+
}
|
|
28
|
+
// Validate end is after start
|
|
29
|
+
if (endDate && endDate <= startDate) {
|
|
30
|
+
throw new Error('End date/time must be after start date/time');
|
|
31
|
+
}
|
|
32
|
+
// Read existing events
|
|
33
|
+
const events = await readCalendarFile();
|
|
34
|
+
// Create new event
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const newEvent = {
|
|
37
|
+
id: uuidv4(),
|
|
38
|
+
title: params.title.trim(),
|
|
39
|
+
start: toISO(startDate),
|
|
40
|
+
end: endDate ? toISO(endDate) : undefined,
|
|
41
|
+
location: params.location?.trim(),
|
|
42
|
+
description: params.description?.trim(),
|
|
43
|
+
allDay: params.allDay ?? false,
|
|
44
|
+
status: 'CONFIRMED',
|
|
45
|
+
created: toISO(now),
|
|
46
|
+
lastModified: toISO(now),
|
|
47
|
+
};
|
|
48
|
+
// Add to events list
|
|
49
|
+
events.push(newEvent);
|
|
50
|
+
// Write back to file
|
|
51
|
+
await writeCalendarFile(events);
|
|
52
|
+
// Commit changes
|
|
53
|
+
await commitAndPush(`Add event: ${newEvent.title}`);
|
|
54
|
+
// Return success message
|
|
55
|
+
const displayDate = formatForDisplay(startDate, newEvent.allDay);
|
|
56
|
+
return `Created event "${newEvent.title}" on ${displayDate}`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readCalendarFile, writeCalendarFile } from '../utils/ical.js';
|
|
2
|
+
import { commitAndPush } from '../utils/git.js';
|
|
3
|
+
/**
|
|
4
|
+
* Delete a calendar event
|
|
5
|
+
* @param params - Delete parameters (id required)
|
|
6
|
+
* @returns Success message
|
|
7
|
+
*/
|
|
8
|
+
export async function deleteEvent(params) {
|
|
9
|
+
// Validate required field
|
|
10
|
+
if (!params.id) {
|
|
11
|
+
throw new Error('Event ID is required');
|
|
12
|
+
}
|
|
13
|
+
// Read existing events
|
|
14
|
+
const events = await readCalendarFile();
|
|
15
|
+
// Find the event to delete
|
|
16
|
+
const eventIndex = events.findIndex(e => e.id === params.id);
|
|
17
|
+
if (eventIndex === -1) {
|
|
18
|
+
throw new Error(`Event not found: ${params.id}`);
|
|
19
|
+
}
|
|
20
|
+
const deletedEvent = events[eventIndex];
|
|
21
|
+
// Remove from array
|
|
22
|
+
events.splice(eventIndex, 1);
|
|
23
|
+
// Write back to file
|
|
24
|
+
await writeCalendarFile(events);
|
|
25
|
+
// Commit changes
|
|
26
|
+
await commitAndPush(`Delete event: ${deletedEvent.title}`);
|
|
27
|
+
// Return success message
|
|
28
|
+
return `Deleted event "${deletedEvent.title}"`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FindFreeTimeParams, FindFreeTimeResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Find available time slots in the calendar
|
|
4
|
+
* @param params - Search parameters
|
|
5
|
+
* @returns List of free time slots
|
|
6
|
+
*/
|
|
7
|
+
export declare function findFreeTime(params: FindFreeTimeParams): Promise<FindFreeTimeResult>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readCalendarFile } from '../utils/ical.js';
|
|
2
|
+
import { parseNaturalDate, addMinutes, toISO, rangesOverlap, } from '../utils/dates.js';
|
|
3
|
+
const DEFAULT_MAX_RESULTS = 5;
|
|
4
|
+
const WORKING_HOURS_START = 9; // 9 AM
|
|
5
|
+
const WORKING_HOURS_END = 17; // 5 PM
|
|
6
|
+
const SLOT_INCREMENT_MINUTES = 30; // Check every 30 minutes
|
|
7
|
+
/**
|
|
8
|
+
* Find available time slots in the calendar
|
|
9
|
+
* @param params - Search parameters
|
|
10
|
+
* @returns List of free time slots
|
|
11
|
+
*/
|
|
12
|
+
export async function findFreeTime(params) {
|
|
13
|
+
// Validate required fields
|
|
14
|
+
if (!params.duration || params.duration <= 0) {
|
|
15
|
+
throw new Error('Duration must be a positive number of minutes');
|
|
16
|
+
}
|
|
17
|
+
if (!params.startDate) {
|
|
18
|
+
throw new Error('Start date is required');
|
|
19
|
+
}
|
|
20
|
+
if (!params.endDate) {
|
|
21
|
+
throw new Error('End date is required');
|
|
22
|
+
}
|
|
23
|
+
const searchStart = parseNaturalDate(params.startDate);
|
|
24
|
+
const searchEnd = parseNaturalDate(params.endDate);
|
|
25
|
+
if (searchEnd <= searchStart) {
|
|
26
|
+
throw new Error('End date must be after start date');
|
|
27
|
+
}
|
|
28
|
+
const workingHoursOnly = params.workingHoursOnly ?? true;
|
|
29
|
+
const maxResults = params.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
30
|
+
// Read all events in the search range
|
|
31
|
+
const allEvents = await readCalendarFile();
|
|
32
|
+
// Filter events that overlap with search range
|
|
33
|
+
const events = allEvents.filter(event => {
|
|
34
|
+
const eventStart = new Date(event.start);
|
|
35
|
+
const eventEnd = event.end ? new Date(event.end) : addMinutes(eventStart, 60);
|
|
36
|
+
return rangesOverlap(eventStart, eventEnd, searchStart, searchEnd);
|
|
37
|
+
});
|
|
38
|
+
// Sort events by start time
|
|
39
|
+
events.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
40
|
+
// Find free slots
|
|
41
|
+
const freeSlots = [];
|
|
42
|
+
let currentTime = new Date(searchStart);
|
|
43
|
+
while (currentTime < searchEnd && freeSlots.length < maxResults) {
|
|
44
|
+
// Adjust for working hours if needed
|
|
45
|
+
if (workingHoursOnly) {
|
|
46
|
+
const adjustedTime = adjustForWorkingHours(currentTime);
|
|
47
|
+
if (!adjustedTime) {
|
|
48
|
+
// Move to next day
|
|
49
|
+
currentTime = getNextWorkingDayStart(currentTime);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
currentTime = adjustedTime;
|
|
53
|
+
}
|
|
54
|
+
// Check if this slot is free
|
|
55
|
+
const slotEnd = addMinutes(currentTime, params.duration);
|
|
56
|
+
// Make sure slot end doesn't exceed working hours
|
|
57
|
+
if (workingHoursOnly) {
|
|
58
|
+
const dayEnd = getWorkingHoursEnd(currentTime);
|
|
59
|
+
if (slotEnd > dayEnd) {
|
|
60
|
+
// Move to next day
|
|
61
|
+
currentTime = getNextWorkingDayStart(currentTime);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Make sure slot end doesn't exceed search range
|
|
66
|
+
if (slotEnd > searchEnd) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
// Check for conflicts with existing events
|
|
70
|
+
const hasConflict = events.some(event => {
|
|
71
|
+
const eventStart = new Date(event.start);
|
|
72
|
+
const eventEnd = event.end ? new Date(event.end) : addMinutes(eventStart, 60);
|
|
73
|
+
return rangesOverlap(currentTime, slotEnd, eventStart, eventEnd);
|
|
74
|
+
});
|
|
75
|
+
if (!hasConflict) {
|
|
76
|
+
freeSlots.push({
|
|
77
|
+
start: toISO(currentTime),
|
|
78
|
+
end: toISO(slotEnd),
|
|
79
|
+
durationMinutes: params.duration,
|
|
80
|
+
});
|
|
81
|
+
// Skip past this slot
|
|
82
|
+
currentTime = slotEnd;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Move to next slot increment
|
|
86
|
+
currentTime = addMinutes(currentTime, SLOT_INCREMENT_MINUTES);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { freeSlots };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Adjust a time to be within working hours
|
|
93
|
+
* Returns null if the time is after working hours for the day
|
|
94
|
+
*/
|
|
95
|
+
function adjustForWorkingHours(time) {
|
|
96
|
+
const hours = time.getUTCHours();
|
|
97
|
+
if (hours < WORKING_HOURS_START) {
|
|
98
|
+
// Before working hours - move to start of working hours
|
|
99
|
+
return new Date(Date.UTC(time.getUTCFullYear(), time.getUTCMonth(), time.getUTCDate(), WORKING_HOURS_START, 0, 0, 0));
|
|
100
|
+
}
|
|
101
|
+
if (hours >= WORKING_HOURS_END) {
|
|
102
|
+
// After working hours - return null to trigger next day
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return time;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the start of the next working day
|
|
109
|
+
*/
|
|
110
|
+
function getNextWorkingDayStart(time) {
|
|
111
|
+
const nextDay = new Date(Date.UTC(time.getUTCFullYear(), time.getUTCMonth(), time.getUTCDate() + 1, WORKING_HOURS_START, 0, 0, 0));
|
|
112
|
+
return nextDay;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get the end of working hours for a given day
|
|
116
|
+
*/
|
|
117
|
+
function getWorkingHoursEnd(time) {
|
|
118
|
+
return new Date(Date.UTC(time.getUTCFullYear(), time.getUTCMonth(), time.getUTCDate(), WORKING_HOURS_END, 0, 0, 0));
|
|
119
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ListEventsParams, ListEventsResult, CalendarEvent } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* List calendar events with optional filtering
|
|
4
|
+
* @param params - Filter parameters
|
|
5
|
+
* @returns List of events with count and hasMore indicator
|
|
6
|
+
*/
|
|
7
|
+
export declare function listEvents(params?: ListEventsParams): Promise<ListEventsResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Get all events without filtering (internal use)
|
|
10
|
+
*/
|
|
11
|
+
export declare function getAllEvents(): Promise<CalendarEvent[]>;
|