@zhin.js/plugin-calendar 0.0.0-stage → 0.1.1

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.
Files changed (101) hide show
  1. package/README.md +189 -2
  2. package/commands/calendar/auth/index.js +26 -0
  3. package/commands/calendar/auth/index.ts +29 -0
  4. package/commands/calendar/logout/index.js +18 -0
  5. package/commands/calendar/logout/index.ts +18 -0
  6. package/commands/calendar/status/index.js +10 -0
  7. package/commands/calendar/status/index.ts +10 -0
  8. package/lib/caldav.d.ts +18 -0
  9. package/lib/caldav.d.ts.map +1 -0
  10. package/lib/caldav.js +167 -0
  11. package/lib/caldav.js.map +1 -0
  12. package/lib/chat-auth.d.ts +49 -0
  13. package/lib/chat-auth.d.ts.map +1 -0
  14. package/lib/chat-auth.js +233 -0
  15. package/lib/chat-auth.js.map +1 -0
  16. package/lib/cli/auth.d.ts +3 -0
  17. package/lib/cli/auth.d.ts.map +1 -0
  18. package/lib/cli/auth.js +182 -0
  19. package/lib/cli/auth.js.map +1 -0
  20. package/lib/config.d.ts +87 -0
  21. package/lib/config.d.ts.map +1 -0
  22. package/lib/config.js +95 -0
  23. package/lib/config.js.map +1 -0
  24. package/lib/credential-store.d.ts +30 -0
  25. package/lib/credential-store.d.ts.map +1 -0
  26. package/lib/credential-store.js +42 -0
  27. package/lib/credential-store.js.map +1 -0
  28. package/lib/free-time.d.ts +13 -0
  29. package/lib/free-time.d.ts.map +1 -0
  30. package/lib/free-time.js +96 -0
  31. package/lib/free-time.js.map +1 -0
  32. package/lib/http-callback.d.ts +13 -0
  33. package/lib/http-callback.d.ts.map +1 -0
  34. package/lib/http-callback.js +26 -0
  35. package/lib/http-callback.js.map +1 -0
  36. package/lib/index.d.ts +32 -0
  37. package/lib/index.d.ts.map +1 -0
  38. package/lib/index.js +122 -0
  39. package/lib/index.js.map +1 -0
  40. package/lib/oauth-auth.d.ts +37 -0
  41. package/lib/oauth-auth.d.ts.map +1 -0
  42. package/lib/oauth-auth.js +91 -0
  43. package/lib/oauth-auth.js.map +1 -0
  44. package/lib/oauth-session.d.ts +27 -0
  45. package/lib/oauth-session.d.ts.map +1 -0
  46. package/lib/oauth-session.js +43 -0
  47. package/lib/oauth-session.js.map +1 -0
  48. package/lib/oauth.d.ts +28 -0
  49. package/lib/oauth.d.ts.map +1 -0
  50. package/lib/oauth.js +200 -0
  51. package/lib/oauth.js.map +1 -0
  52. package/lib/runtime.d.ts +11 -0
  53. package/lib/runtime.d.ts.map +1 -0
  54. package/lib/runtime.js +3 -0
  55. package/lib/runtime.js.map +1 -0
  56. package/lib/secrets.d.ts +3 -0
  57. package/lib/secrets.d.ts.map +1 -0
  58. package/lib/secrets.js +6 -0
  59. package/lib/secrets.js.map +1 -0
  60. package/lib/time.d.ts +37 -0
  61. package/lib/time.d.ts.map +1 -0
  62. package/lib/time.js +118 -0
  63. package/lib/time.js.map +1 -0
  64. package/lib/token-manager.d.ts +71 -0
  65. package/lib/token-manager.d.ts.map +1 -0
  66. package/lib/token-manager.js +239 -0
  67. package/lib/token-manager.js.map +1 -0
  68. package/lib/tool-errors.d.ts +6 -0
  69. package/lib/tool-errors.d.ts.map +1 -0
  70. package/lib/tool-errors.js +17 -0
  71. package/lib/tool-errors.js.map +1 -0
  72. package/lib/types.d.ts +28 -0
  73. package/lib/types.d.ts.map +1 -0
  74. package/lib/types.js +2 -0
  75. package/lib/types.js.map +1 -0
  76. package/package.json +112 -3
  77. package/plugin.js +72 -0
  78. package/skills/calendar-scheduling/SKILL.md +38 -0
  79. package/skills/calendar-scheduling/tools/create_event/index.js +38 -0
  80. package/skills/calendar-scheduling/tools/create_event/index.ts +41 -0
  81. package/skills/calendar-scheduling/tools/find_free_time/index.js +46 -0
  82. package/skills/calendar-scheduling/tools/find_free_time/index.ts +50 -0
  83. package/skills/calendar-scheduling/tools/list_events/index.js +32 -0
  84. package/skills/calendar-scheduling/tools/list_events/index.ts +42 -0
  85. package/src/caldav.ts +198 -0
  86. package/src/chat-auth.ts +336 -0
  87. package/src/cli/auth.ts +235 -0
  88. package/src/config.ts +108 -0
  89. package/src/credential-store.ts +79 -0
  90. package/src/free-time.ts +128 -0
  91. package/src/http-callback.ts +55 -0
  92. package/src/index.ts +193 -0
  93. package/src/oauth-auth.ts +145 -0
  94. package/src/oauth-session.ts +65 -0
  95. package/src/oauth.ts +298 -0
  96. package/src/runtime.ts +16 -0
  97. package/src/secrets.ts +5 -0
  98. package/src/time.ts +167 -0
  99. package/src/token-manager.ts +358 -0
  100. package/src/tool-errors.ts +18 -0
  101. package/src/types.ts +37 -0
@@ -0,0 +1,46 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from "@zhin.js/tool";
3
+ import { z } from "zod";
4
+ import { findFreeSlots } from "../../../../lib/free-time.js";
5
+ import { calendarRuntimeToken } from "../../../../lib/runtime.js";
6
+ import { runCalendarTool } from "../../../../lib/tool-errors.js";
7
+ import { parseDateTimeInZone } from "../../../../lib/time.js";
8
+ export default defineAgentTool({
9
+ description: "Find free slots of a given duration inside work hours between two datetimes.",
10
+ requiresApproval: "never",
11
+ tags: ["calendar"],
12
+ keywords: ["空闲", "free time", "有空"],
13
+ inputSchema: z.object({
14
+ from: z.string(),
15
+ to: z.string(),
16
+ duration_minutes: z
17
+ .number()
18
+ .int()
19
+ .min(5)
20
+ .max(24 * 60),
21
+ }),
22
+ async execute({ from, to, duration_minutes }, context) {
23
+ return runCalendarTool(async () => {
24
+ const runtime = context.use(calendarRuntimeToken);
25
+ if (!runtime.provider)
26
+ return "日历未启用:请先配置 provider 与凭据";
27
+ const timezone = runtime.config.timezone;
28
+ const fromDate = parseDateTimeInZone(from, timezone);
29
+ const toDate = parseDateTimeInZone(to, timezone);
30
+ const events = await runtime.provider.listEvents(fromDate, toDate, context.signal);
31
+ const slots = findFreeSlots({
32
+ from: fromDate,
33
+ to: toDate,
34
+ durationMinutes: duration_minutes,
35
+ workDayStart: runtime.config.work_day_start,
36
+ workDayEnd: runtime.config.work_day_end,
37
+ timezone,
38
+ events,
39
+ });
40
+ if (slots.length === 0) {
41
+ return `在工作时段(${runtime.config.work_day_start}-${runtime.config.work_day_end} ${timezone})没有足够的空闲`;
42
+ }
43
+ return slots.map((slot) => `• ${slot.start} → ${slot.end}`).join("\n");
44
+ });
45
+ },
46
+ });
@@ -0,0 +1,50 @@
1
+ import { defineAgentTool } from "@zhin.js/tool";
2
+ import { z } from "zod";
3
+ import { findFreeSlots } from "../../../../src/free-time.js";
4
+ import { calendarRuntimeToken } from "../../../../src/runtime.js";
5
+ import { runCalendarTool } from "../../../../src/tool-errors.js";
6
+ import { parseDateTimeInZone } from "../../../../src/time.js";
7
+
8
+ export default defineAgentTool({
9
+ description:
10
+ "Find free slots of a given duration inside work hours between two datetimes.",
11
+ requiresApproval: "never",
12
+ tags: ["calendar"],
13
+ keywords: ["空闲", "free time", "有空"],
14
+ inputSchema: z.object({
15
+ from: z.string(),
16
+ to: z.string(),
17
+ duration_minutes: z
18
+ .number()
19
+ .int()
20
+ .min(5)
21
+ .max(24 * 60),
22
+ }),
23
+ async execute({ from, to, duration_minutes }, context) {
24
+ return runCalendarTool(async () => {
25
+ const runtime = context.use(calendarRuntimeToken);
26
+ if (!runtime.provider) return "日历未启用:请先配置 provider 与凭据";
27
+ const timezone = runtime.config.timezone;
28
+ const fromDate = parseDateTimeInZone(from, timezone);
29
+ const toDate = parseDateTimeInZone(to, timezone);
30
+ const events = await runtime.provider.listEvents(
31
+ fromDate,
32
+ toDate,
33
+ context.signal
34
+ );
35
+ const slots = findFreeSlots({
36
+ from: fromDate,
37
+ to: toDate,
38
+ durationMinutes: duration_minutes,
39
+ workDayStart: runtime.config.work_day_start,
40
+ workDayEnd: runtime.config.work_day_end,
41
+ timezone,
42
+ events,
43
+ });
44
+ if (slots.length === 0) {
45
+ return `在工作时段(${runtime.config.work_day_start}-${runtime.config.work_day_end} ${timezone})没有足够的空闲`;
46
+ }
47
+ return slots.map((slot) => `• ${slot.start} → ${slot.end}`).join("\n");
48
+ });
49
+ },
50
+ });
@@ -0,0 +1,32 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from "@zhin.js/tool";
3
+ import { z } from "zod";
4
+ import { calendarRuntimeToken } from "../../../../lib/runtime.js";
5
+ import { runCalendarTool } from "../../../../lib/tool-errors.js";
6
+ import { parseDateTimeInZone } from "../../../../lib/time.js";
7
+ export default defineAgentTool({
8
+ description: "List calendar events between two datetimes in the configured timezone.",
9
+ requiresApproval: "never",
10
+ tags: ["calendar"],
11
+ keywords: ["日历", "日程", "events", "list events"],
12
+ inputSchema: z.object({
13
+ from: z
14
+ .string()
15
+ .describe("Start YYYY-MM-DDTHH:mm (configured timezone if no offset)"),
16
+ to: z.string().describe("End YYYY-MM-DDTHH:mm"),
17
+ }),
18
+ async execute({ from, to }, context) {
19
+ return runCalendarTool(async () => {
20
+ const runtime = context.use(calendarRuntimeToken);
21
+ if (!runtime.provider)
22
+ return "日历未启用:请在 plugins.calendar 配置 provider(caldav / google / microsoft)与凭据";
23
+ const timezone = runtime.config.timezone;
24
+ const events = await runtime.provider.listEvents(parseDateTimeInZone(from, timezone), parseDateTimeInZone(to, timezone), context.signal);
25
+ if (events.length === 0)
26
+ return `(${runtime.provider.name})${from} ~ ${to} 没有事件`;
27
+ return events
28
+ .map((event) => `• ${event.start} → ${event.end} ${event.title}${event.location ? ` @ ${event.location}` : ""} [${event.id}]`)
29
+ .join("\n");
30
+ });
31
+ },
32
+ });
@@ -0,0 +1,42 @@
1
+ import { defineAgentTool } from "@zhin.js/tool";
2
+ import { z } from "zod";
3
+ import { calendarRuntimeToken } from "../../../../src/runtime.js";
4
+ import { runCalendarTool } from "../../../../src/tool-errors.js";
5
+ import { parseDateTimeInZone } from "../../../../src/time.js";
6
+
7
+ export default defineAgentTool({
8
+ description:
9
+ "List calendar events between two datetimes in the configured timezone.",
10
+ requiresApproval: "never",
11
+ tags: ["calendar"],
12
+ keywords: ["日历", "日程", "events", "list events"],
13
+ inputSchema: z.object({
14
+ from: z
15
+ .string()
16
+ .describe("Start YYYY-MM-DDTHH:mm (configured timezone if no offset)"),
17
+ to: z.string().describe("End YYYY-MM-DDTHH:mm"),
18
+ }),
19
+ async execute({ from, to }, context) {
20
+ return runCalendarTool(async () => {
21
+ const runtime = context.use(calendarRuntimeToken);
22
+ if (!runtime.provider)
23
+ return "日历未启用:请在 plugins.calendar 配置 provider(caldav / google / microsoft)与凭据";
24
+ const timezone = runtime.config.timezone;
25
+ const events = await runtime.provider.listEvents(
26
+ parseDateTimeInZone(from, timezone),
27
+ parseDateTimeInZone(to, timezone),
28
+ context.signal
29
+ );
30
+ if (events.length === 0)
31
+ return `(${runtime.provider.name})${from} ~ ${to} 没有事件`;
32
+ return events
33
+ .map(
34
+ (event) =>
35
+ `• ${event.start} → ${event.end} ${event.title}${
36
+ event.location ? ` @ ${event.location}` : ""
37
+ } [${event.id}]`
38
+ )
39
+ .join("\n");
40
+ });
41
+ },
42
+ });
package/src/caldav.ts ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Minimal CalDAV client (RFC 4791) for listing and creating VEVENTs.
3
+ * Works with iCloud, Fastmail and Nextcloud calendars.
4
+ */
5
+ import type {
6
+ CalendarEvent,
7
+ CalendarProvider,
8
+ CreateEventInput,
9
+ } from "./types.js";
10
+ import { formatInZone } from "./time.js";
11
+
12
+ export interface CalDavOptions {
13
+ readonly calendarUrl: string;
14
+ readonly username: string;
15
+ readonly password: string;
16
+ readonly timezone: string;
17
+ readonly fetchImpl?: typeof fetch;
18
+ }
19
+
20
+ function basicAuth(username: string, password: string): string {
21
+ return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
22
+ }
23
+
24
+ function escapeXml(value: string): string {
25
+ return value
26
+ .replace(/&/g, "&")
27
+ .replace(/</g, "&lt;")
28
+ .replace(/>/g, "&gt;")
29
+ .replace(/"/g, "&quot;");
30
+ }
31
+
32
+ function toUtcStamp(date: Date): string {
33
+ return date
34
+ .toISOString()
35
+ .replace(/[-:]/g, "")
36
+ .replace(/\.\d{3}/, "");
37
+ }
38
+
39
+ function parseIcsDate(value: string): Date {
40
+ const match = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z)?)?/.exec(
41
+ value.trim()
42
+ );
43
+ if (!match) return new Date(NaN);
44
+ if (!match[4]) {
45
+ return new Date(
46
+ Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))
47
+ );
48
+ }
49
+ const utc = match[7] === "Z";
50
+ const parts = [
51
+ Number(match[1]),
52
+ Number(match[2]) - 1,
53
+ Number(match[3]),
54
+ Number(match[4]),
55
+ Number(match[5]),
56
+ Number(match[6] ?? 0),
57
+ ] as const;
58
+ return utc ? new Date(Date.UTC(...parts)) : new Date(...parts);
59
+ }
60
+
61
+ function extractField(block: string, name: string): string | undefined {
62
+ const pattern = new RegExp(`^${name}(?:;[^:]*)?:(.*)$`, "im");
63
+ const match = pattern.exec(block);
64
+ return match
65
+ ? match[1]!.trim().replace(/\\n/g, "\n").replace(/\\,/g, ",")
66
+ : undefined;
67
+ }
68
+
69
+ /** Parses VEVENT components from a CalDAV multistatus response. */
70
+ export function parseCalDavEvents(
71
+ xml: string,
72
+ timezone: string
73
+ ): CalendarEvent[] {
74
+ const events: CalendarEvent[] = [];
75
+ const hrefs = [
76
+ ...xml.matchAll(/<[^:>]*:?href[^>]*>([^<]+)<\/[^:>]*:?href>/gi),
77
+ ].map((m) => m[1]!.trim());
78
+ const calendars = [...xml.matchAll(/BEGIN:VEVENT([\s\S]*?)END:VEVENT/g)];
79
+ calendars.forEach((match, index) => {
80
+ const block = match[1]!;
81
+ const uid = extractField(block, "UID") ?? hrefs[index] ?? `event-${index}`;
82
+ const summary = extractField(block, "SUMMARY") ?? "(无标题)";
83
+ const startRaw = extractField(block, "DTSTART");
84
+ const endRaw = extractField(block, "DTEND") ?? startRaw;
85
+ if (!startRaw || !endRaw) return;
86
+ const start = parseIcsDate(startRaw);
87
+ const end = parseIcsDate(endRaw);
88
+ const allDay = !/T/.test(startRaw);
89
+ events.push({
90
+ id: uid,
91
+ title: summary,
92
+ start: formatInZone(start, timezone),
93
+ end: formatInZone(end, timezone),
94
+ location: extractField(block, "LOCATION"),
95
+ description: extractField(block, "DESCRIPTION"),
96
+ all_day: allDay,
97
+ });
98
+ });
99
+ return events;
100
+ }
101
+
102
+ function buildVevent(input: CreateEventInput, uid: string): string {
103
+ const lines = [
104
+ "BEGIN:VCALENDAR",
105
+ "VERSION:2.0",
106
+ "PRODID:-//zhin.js-assistant-toolkit//EN",
107
+ "BEGIN:VEVENT",
108
+ `UID:${uid}`,
109
+ `DTSTAMP:${toUtcStamp(new Date())}`,
110
+ input.all_day
111
+ ? `DTSTART;VALUE=DATE:${input.start
112
+ .toISOString()
113
+ .slice(0, 10)
114
+ .replace(/-/g, "")}`
115
+ : `DTSTART:${toUtcStamp(input.start)}`,
116
+ input.all_day
117
+ ? `DTEND;VALUE=DATE:${input.end
118
+ .toISOString()
119
+ .slice(0, 10)
120
+ .replace(/-/g, "")}`
121
+ : `DTEND:${toUtcStamp(input.end)}`,
122
+ `SUMMARY:${input.title.replace(/\n/g, " ")}`,
123
+ ];
124
+ if (input.location)
125
+ lines.push(`LOCATION:${input.location.replace(/\n/g, " ")}`);
126
+ if (input.description)
127
+ lines.push(`DESCRIPTION:${input.description.replace(/\n/g, "\\n")}`);
128
+ lines.push("END:VEVENT", "END:VCALENDAR");
129
+ return lines.join("\r\n");
130
+ }
131
+
132
+ export function createCalDavProvider(options: CalDavOptions): CalendarProvider {
133
+ const fetchImpl = options.fetchImpl ?? fetch;
134
+ const auth = basicAuth(options.username, options.password);
135
+ const base = options.calendarUrl.endsWith("/")
136
+ ? options.calendarUrl
137
+ : `${options.calendarUrl}/`;
138
+
139
+ return {
140
+ name: "caldav",
141
+ async listEvents(from, to, signal) {
142
+ const body = `<?xml version="1.0" encoding="utf-8" ?>
143
+ <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
144
+ <D:prop><D:getetag/><C:calendar-data/></D:prop>
145
+ <C:filter>
146
+ <C:comp-filter name="VCALENDAR">
147
+ <C:comp-filter name="VEVENT">
148
+ <C:time-range start="${toUtcStamp(from)}" end="${toUtcStamp(to)}"/>
149
+ </C:comp-filter>
150
+ </C:comp-filter>
151
+ </C:filter>
152
+ </C:calendar-query>`;
153
+ const response = await fetchImpl(base, {
154
+ method: "REPORT",
155
+ headers: {
156
+ authorization: auth,
157
+ depth: "1",
158
+ "content-type": "application/xml; charset=utf-8",
159
+ },
160
+ body,
161
+ signal,
162
+ });
163
+ if (!response.ok)
164
+ throw new Error(`CalDAV REPORT 失败:HTTP ${response.status}`);
165
+ return parseCalDavEvents(await response.text(), options.timezone);
166
+ },
167
+ async createEvent(input, signal) {
168
+ const uid = `${Date.now()}-${Math.random()
169
+ .toString(36)
170
+ .slice(2, 10)}@zhin`;
171
+ const url = new URL(`${uid}.ics`, base).toString();
172
+ const response = await fetchImpl(url, {
173
+ method: "PUT",
174
+ headers: {
175
+ authorization: auth,
176
+ "content-type": "text/calendar; charset=utf-8",
177
+ "if-none-match": "*",
178
+ },
179
+ body: buildVevent(input, uid),
180
+ signal,
181
+ });
182
+ if (response.status !== 201 && response.status !== 204) {
183
+ throw new Error(`CalDAV PUT 失败:HTTP ${response.status}`);
184
+ }
185
+ return {
186
+ id: uid,
187
+ title: input.title,
188
+ start: formatInZone(input.start, options.timezone),
189
+ end: formatInZone(input.end, options.timezone),
190
+ location: input.location,
191
+ description: input.description,
192
+ all_day: input.all_day,
193
+ };
194
+ },
195
+ };
196
+ }
197
+
198
+ export { escapeXml };