cc-lark 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 (298) hide show
  1. package/.github/workflows/ci.yml +47 -0
  2. package/.github/workflows/release.yml +47 -0
  3. package/.github/workflows/sync-upstream.yml +127 -0
  4. package/.prettierrc.json +7 -0
  5. package/README.md +214 -0
  6. package/dist/core/api-error.d.ts +193 -0
  7. package/dist/core/api-error.d.ts.map +1 -0
  8. package/dist/core/api-error.js +263 -0
  9. package/dist/core/api-error.js.map +1 -0
  10. package/dist/core/auth-errors.d.ts +13 -0
  11. package/dist/core/auth-errors.d.ts.map +1 -0
  12. package/dist/core/auth-errors.js +14 -0
  13. package/dist/core/auth-errors.js.map +1 -0
  14. package/dist/core/config.d.ts +60 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +115 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/device-flow.d.ts +80 -0
  19. package/dist/core/device-flow.d.ts.map +1 -0
  20. package/dist/core/device-flow.js +231 -0
  21. package/dist/core/device-flow.js.map +1 -0
  22. package/dist/core/index.d.ts +16 -0
  23. package/dist/core/index.d.ts.map +1 -0
  24. package/dist/core/index.js +16 -0
  25. package/dist/core/index.js.map +1 -0
  26. package/dist/core/lark-client.d.ts +136 -0
  27. package/dist/core/lark-client.d.ts.map +1 -0
  28. package/dist/core/lark-client.js +315 -0
  29. package/dist/core/lark-client.js.map +1 -0
  30. package/dist/core/token-store.d.ts +67 -0
  31. package/dist/core/token-store.d.ts.map +1 -0
  32. package/dist/core/token-store.js +215 -0
  33. package/dist/core/token-store.js.map +1 -0
  34. package/dist/core/types.d.ts +286 -0
  35. package/dist/core/types.d.ts.map +1 -0
  36. package/dist/core/types.js +11 -0
  37. package/dist/core/types.js.map +1 -0
  38. package/dist/core/uat-client.d.ts +64 -0
  39. package/dist/core/uat-client.d.ts.map +1 -0
  40. package/dist/core/uat-client.js +227 -0
  41. package/dist/core/uat-client.js.map +1 -0
  42. package/dist/core/version.d.ts +26 -0
  43. package/dist/core/version.d.ts.map +1 -0
  44. package/dist/core/version.js +50 -0
  45. package/dist/core/version.js.map +1 -0
  46. package/dist/index.d.ts +12 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +116 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/tools/bitable/app.d.ts +20 -0
  51. package/dist/tools/bitable/app.d.ts.map +1 -0
  52. package/dist/tools/bitable/app.js +301 -0
  53. package/dist/tools/bitable/app.js.map +1 -0
  54. package/dist/tools/bitable/field.d.ts +19 -0
  55. package/dist/tools/bitable/field.d.ts.map +1 -0
  56. package/dist/tools/bitable/field.js +315 -0
  57. package/dist/tools/bitable/field.js.map +1 -0
  58. package/dist/tools/bitable/index.d.ts +21 -0
  59. package/dist/tools/bitable/index.d.ts.map +1 -0
  60. package/dist/tools/bitable/index.js +39 -0
  61. package/dist/tools/bitable/index.js.map +1 -0
  62. package/dist/tools/bitable/record.d.ts +22 -0
  63. package/dist/tools/bitable/record.d.ts.map +1 -0
  64. package/dist/tools/bitable/record.js +434 -0
  65. package/dist/tools/bitable/record.js.map +1 -0
  66. package/dist/tools/bitable/table.d.ts +21 -0
  67. package/dist/tools/bitable/table.d.ts.map +1 -0
  68. package/dist/tools/bitable/table.js +361 -0
  69. package/dist/tools/bitable/table.js.map +1 -0
  70. package/dist/tools/calendar/calendar.d.ts +18 -0
  71. package/dist/tools/calendar/calendar.d.ts.map +1 -0
  72. package/dist/tools/calendar/calendar.js +192 -0
  73. package/dist/tools/calendar/calendar.js.map +1 -0
  74. package/dist/tools/calendar/event.d.ts +20 -0
  75. package/dist/tools/calendar/event.d.ts.map +1 -0
  76. package/dist/tools/calendar/event.js +465 -0
  77. package/dist/tools/calendar/event.js.map +1 -0
  78. package/dist/tools/calendar/index.d.ts +19 -0
  79. package/dist/tools/calendar/index.d.ts.map +1 -0
  80. package/dist/tools/calendar/index.js +37 -0
  81. package/dist/tools/calendar/index.js.map +1 -0
  82. package/dist/tools/chat/chat.d.ts +11 -0
  83. package/dist/tools/chat/chat.d.ts.map +1 -0
  84. package/dist/tools/chat/chat.js +106 -0
  85. package/dist/tools/chat/chat.js.map +1 -0
  86. package/dist/tools/chat/index.d.ts +11 -0
  87. package/dist/tools/chat/index.d.ts.map +1 -0
  88. package/dist/tools/chat/index.js +20 -0
  89. package/dist/tools/chat/index.js.map +1 -0
  90. package/dist/tools/chat/members.d.ts +9 -0
  91. package/dist/tools/chat/members.d.ts.map +1 -0
  92. package/dist/tools/chat/members.js +80 -0
  93. package/dist/tools/chat/members.js.map +1 -0
  94. package/dist/tools/common/get-user.d.ts +11 -0
  95. package/dist/tools/common/get-user.d.ts.map +1 -0
  96. package/dist/tools/common/get-user.js +112 -0
  97. package/dist/tools/common/get-user.js.map +1 -0
  98. package/dist/tools/common/index.d.ts +11 -0
  99. package/dist/tools/common/index.d.ts.map +1 -0
  100. package/dist/tools/common/index.js +20 -0
  101. package/dist/tools/common/index.js.map +1 -0
  102. package/dist/tools/common/search-user.d.ts +9 -0
  103. package/dist/tools/common/search-user.d.ts.map +1 -0
  104. package/dist/tools/common/search-user.js +88 -0
  105. package/dist/tools/common/search-user.js.map +1 -0
  106. package/dist/tools/doc/create.d.ts +17 -0
  107. package/dist/tools/doc/create.d.ts.map +1 -0
  108. package/dist/tools/doc/create.js +159 -0
  109. package/dist/tools/doc/create.js.map +1 -0
  110. package/dist/tools/doc/fetch.d.ts +17 -0
  111. package/dist/tools/doc/fetch.d.ts.map +1 -0
  112. package/dist/tools/doc/fetch.js +123 -0
  113. package/dist/tools/doc/fetch.js.map +1 -0
  114. package/dist/tools/doc/index.d.ts +21 -0
  115. package/dist/tools/doc/index.d.ts.map +1 -0
  116. package/dist/tools/doc/index.js +33 -0
  117. package/dist/tools/doc/index.js.map +1 -0
  118. package/dist/tools/doc/shared.d.ts +69 -0
  119. package/dist/tools/doc/shared.d.ts.map +1 -0
  120. package/dist/tools/doc/shared.js +172 -0
  121. package/dist/tools/doc/shared.js.map +1 -0
  122. package/dist/tools/doc/update.d.ts +25 -0
  123. package/dist/tools/doc/update.d.ts.map +1 -0
  124. package/dist/tools/doc/update.js +208 -0
  125. package/dist/tools/doc/update.js.map +1 -0
  126. package/dist/tools/drive/file.d.ts +13 -0
  127. package/dist/tools/drive/file.d.ts.map +1 -0
  128. package/dist/tools/drive/file.js +212 -0
  129. package/dist/tools/drive/file.js.map +1 -0
  130. package/dist/tools/drive/index.d.ts +12 -0
  131. package/dist/tools/drive/index.d.ts.map +1 -0
  132. package/dist/tools/drive/index.js +25 -0
  133. package/dist/tools/drive/index.js.map +1 -0
  134. package/dist/tools/im/format-messages.d.ts +99 -0
  135. package/dist/tools/im/format-messages.d.ts.map +1 -0
  136. package/dist/tools/im/format-messages.js +277 -0
  137. package/dist/tools/im/format-messages.js.map +1 -0
  138. package/dist/tools/im/helpers.d.ts +53 -0
  139. package/dist/tools/im/helpers.d.ts.map +1 -0
  140. package/dist/tools/im/helpers.js +85 -0
  141. package/dist/tools/im/helpers.js.map +1 -0
  142. package/dist/tools/im/index.d.ts +25 -0
  143. package/dist/tools/im/index.d.ts.map +1 -0
  144. package/dist/tools/im/index.js +44 -0
  145. package/dist/tools/im/index.js.map +1 -0
  146. package/dist/tools/im/message-read.d.ts +19 -0
  147. package/dist/tools/im/message-read.d.ts.map +1 -0
  148. package/dist/tools/im/message-read.js +526 -0
  149. package/dist/tools/im/message-read.js.map +1 -0
  150. package/dist/tools/im/message.d.ts +22 -0
  151. package/dist/tools/im/message.d.ts.map +1 -0
  152. package/dist/tools/im/message.js +233 -0
  153. package/dist/tools/im/message.js.map +1 -0
  154. package/dist/tools/im/resource.d.ts +19 -0
  155. package/dist/tools/im/resource.d.ts.map +1 -0
  156. package/dist/tools/im/resource.js +185 -0
  157. package/dist/tools/im/resource.js.map +1 -0
  158. package/dist/tools/im/time-utils.d.ts +70 -0
  159. package/dist/tools/im/time-utils.d.ts.map +1 -0
  160. package/dist/tools/im/time-utils.js +277 -0
  161. package/dist/tools/im/time-utils.js.map +1 -0
  162. package/dist/tools/index.d.ts +85 -0
  163. package/dist/tools/index.d.ts.map +1 -0
  164. package/dist/tools/index.js +135 -0
  165. package/dist/tools/index.js.map +1 -0
  166. package/dist/tools/oauth.d.ts +15 -0
  167. package/dist/tools/oauth.d.ts.map +1 -0
  168. package/dist/tools/oauth.js +379 -0
  169. package/dist/tools/oauth.js.map +1 -0
  170. package/dist/tools/search/doc-search.d.ts +9 -0
  171. package/dist/tools/search/doc-search.d.ts.map +1 -0
  172. package/dist/tools/search/doc-search.js +219 -0
  173. package/dist/tools/search/doc-search.js.map +1 -0
  174. package/dist/tools/search/index.d.ts +11 -0
  175. package/dist/tools/search/index.d.ts.map +1 -0
  176. package/dist/tools/search/index.js +18 -0
  177. package/dist/tools/search/index.js.map +1 -0
  178. package/dist/tools/sheets/index.d.ts +11 -0
  179. package/dist/tools/sheets/index.d.ts.map +1 -0
  180. package/dist/tools/sheets/index.js +18 -0
  181. package/dist/tools/sheets/index.js.map +1 -0
  182. package/dist/tools/sheets/sheet.d.ts +11 -0
  183. package/dist/tools/sheets/sheet.d.ts.map +1 -0
  184. package/dist/tools/sheets/sheet.js +332 -0
  185. package/dist/tools/sheets/sheet.js.map +1 -0
  186. package/dist/tools/task/index.d.ts +12 -0
  187. package/dist/tools/task/index.d.ts.map +1 -0
  188. package/dist/tools/task/index.js +30 -0
  189. package/dist/tools/task/index.js.map +1 -0
  190. package/dist/tools/task/task.d.ts +13 -0
  191. package/dist/tools/task/task.d.ts.map +1 -0
  192. package/dist/tools/task/task.js +225 -0
  193. package/dist/tools/task/task.js.map +1 -0
  194. package/dist/tools/task/tasklist.d.ts +13 -0
  195. package/dist/tools/task/tasklist.d.ts.map +1 -0
  196. package/dist/tools/task/tasklist.js +206 -0
  197. package/dist/tools/task/tasklist.js.map +1 -0
  198. package/dist/tools/wiki/index.d.ts +11 -0
  199. package/dist/tools/wiki/index.d.ts.map +1 -0
  200. package/dist/tools/wiki/index.js +20 -0
  201. package/dist/tools/wiki/index.js.map +1 -0
  202. package/dist/tools/wiki/node.d.ts +11 -0
  203. package/dist/tools/wiki/node.d.ts.map +1 -0
  204. package/dist/tools/wiki/node.js +112 -0
  205. package/dist/tools/wiki/node.js.map +1 -0
  206. package/dist/tools/wiki/space.d.ts +11 -0
  207. package/dist/tools/wiki/space.d.ts.map +1 -0
  208. package/dist/tools/wiki/space.js +125 -0
  209. package/dist/tools/wiki/space.js.map +1 -0
  210. package/dist/utils/index.d.ts +8 -0
  211. package/dist/utils/index.d.ts.map +1 -0
  212. package/dist/utils/index.js +8 -0
  213. package/dist/utils/index.js.map +1 -0
  214. package/dist/utils/logger.d.ts +36 -0
  215. package/dist/utils/logger.d.ts.map +1 -0
  216. package/dist/utils/logger.js +101 -0
  217. package/dist/utils/logger.js.map +1 -0
  218. package/eslint.config.js +13 -0
  219. package/package.json +54 -0
  220. package/skills/feishu-bitable/SKILL.md +248 -0
  221. package/skills/feishu-bitable/references/examples.md +813 -0
  222. package/skills/feishu-bitable/references/field-properties.md +763 -0
  223. package/skills/feishu-bitable/references/record-values.md +911 -0
  224. package/skills/feishu-calendar/SKILL.md +244 -0
  225. package/skills/feishu-channel-rules/SKILL.md +18 -0
  226. package/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
  227. package/skills/feishu-create-doc/SKILL.md +719 -0
  228. package/skills/feishu-fetch-doc/SKILL.md +93 -0
  229. package/skills/feishu-im-read/SKILL.md +163 -0
  230. package/skills/feishu-task/SKILL.md +293 -0
  231. package/skills/feishu-troubleshoot/SKILL.md +70 -0
  232. package/skills/feishu-update-doc/SKILL.md +285 -0
  233. package/src/core/api-error.ts +342 -0
  234. package/src/core/auth-errors.ts +27 -0
  235. package/src/core/config.ts +134 -0
  236. package/src/core/device-flow.ts +314 -0
  237. package/src/core/index.ts +16 -0
  238. package/src/core/lark-client.ts +391 -0
  239. package/src/core/token-store.ts +249 -0
  240. package/src/core/types.ts +302 -0
  241. package/src/core/uat-client.ts +298 -0
  242. package/src/core/version.ts +53 -0
  243. package/src/index.ts +138 -0
  244. package/src/tools/bitable/app.ts +390 -0
  245. package/src/tools/bitable/field.ts +406 -0
  246. package/src/tools/bitable/index.ts +43 -0
  247. package/src/tools/bitable/record.ts +559 -0
  248. package/src/tools/bitable/table.ts +472 -0
  249. package/src/tools/calendar/calendar.ts +254 -0
  250. package/src/tools/calendar/event.ts +606 -0
  251. package/src/tools/calendar/index.ts +41 -0
  252. package/src/tools/chat/chat.ts +127 -0
  253. package/src/tools/chat/index.ts +24 -0
  254. package/src/tools/chat/members.ts +93 -0
  255. package/src/tools/common/get-user.ts +127 -0
  256. package/src/tools/common/index.ts +24 -0
  257. package/src/tools/common/search-user.ts +99 -0
  258. package/src/tools/doc/create.ts +184 -0
  259. package/src/tools/doc/fetch.ts +149 -0
  260. package/src/tools/doc/index.ts +38 -0
  261. package/src/tools/doc/shared.ts +228 -0
  262. package/src/tools/doc/update.ts +240 -0
  263. package/src/tools/drive/file.ts +265 -0
  264. package/src/tools/drive/index.ts +29 -0
  265. package/src/tools/im/format-messages.ts +391 -0
  266. package/src/tools/im/helpers.ts +109 -0
  267. package/src/tools/im/index.ts +49 -0
  268. package/src/tools/im/message-read.ts +676 -0
  269. package/src/tools/im/message.ts +303 -0
  270. package/src/tools/im/resource.ts +225 -0
  271. package/src/tools/im/time-utils.ts +347 -0
  272. package/src/tools/index.ts +205 -0
  273. package/src/tools/oauth.ts +460 -0
  274. package/src/tools/search/doc-search.ts +250 -0
  275. package/src/tools/search/index.ts +22 -0
  276. package/src/tools/sheets/index.ts +22 -0
  277. package/src/tools/sheets/sheet.ts +382 -0
  278. package/src/tools/task/index.ts +34 -0
  279. package/src/tools/task/task.ts +265 -0
  280. package/src/tools/task/tasklist.ts +262 -0
  281. package/src/tools/wiki/index.ts +24 -0
  282. package/src/tools/wiki/node.ts +131 -0
  283. package/src/tools/wiki/space.ts +152 -0
  284. package/src/utils/index.ts +8 -0
  285. package/src/utils/logger.ts +132 -0
  286. package/tests/core/config.test.ts +238 -0
  287. package/tests/core/device-flow.test.ts +490 -0
  288. package/tests/core/lark-client.test.ts +378 -0
  289. package/tests/core/token-store.test.ts +438 -0
  290. package/tests/index.test.ts +360 -0
  291. package/tests/tools/doc/create.test.ts +224 -0
  292. package/tests/tools/doc/fetch.test.ts +182 -0
  293. package/tests/tools/doc/shared.test.ts +183 -0
  294. package/tests/tools/doc/update.test.ts +330 -0
  295. package/tests/tools/im/format-messages.test.ts +184 -0
  296. package/tests/tools/im/time-utils.test.ts +178 -0
  297. package/tests/utils/logger.test.ts +140 -0
  298. package/tsconfig.json +20 -0
@@ -0,0 +1,606 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * feishu_calendar_event tool - Manage Feishu calendar events.
6
+ *
7
+ * Actions: create, list, get, patch, delete
8
+ *
9
+ * Uses the Feishu Calendar API:
10
+ * - create: POST /open-apis/calendar/v4/calendars/:calendar_id/events
11
+ * - list: GET /open-apis/calendar/v4/calendars/:calendar_id/events/instance_view
12
+ * - get: GET /open-apis/calendar/v4/calendars/:calendar_id/events/:event_id
13
+ * - patch: PATCH /open-apis/calendar/v4/calendars/:calendar_id/events/:event_id
14
+ * - delete: DELETE /open-apis/calendar/v4/calendars/:calendar_id/events/:event_id
15
+ *
16
+ * Adapted from openclaw-lark for MCP Server architecture.
17
+ */
18
+
19
+ import { z } from 'zod';
20
+ import type { ToolRegistry } from '../index.js';
21
+ import { LarkClient } from '../../core/lark-client.js';
22
+ import { getValidAccessToken, NeedAuthorizationError } from '../../core/uat-client.js';
23
+ import { assertLarkOk } from '../../core/api-error.js';
24
+ import { json, jsonError, type ToolResult } from '../im/helpers.js';
25
+ import { logger } from '../../utils/logger.js';
26
+
27
+ const log = logger('tools:calendar:event');
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Helpers
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Parse time string to Unix timestamp (seconds).
35
+ * Supports ISO 8601 with timezone or Beijing time (UTC+8) without timezone.
36
+ */
37
+ function parseTimeToTimestamp(input: string): string | null {
38
+ try {
39
+ const trimmed = input.trim();
40
+ const hasTimezone = /[Zz]$|[+-]\d{2}:\d{2}$/.test(trimmed);
41
+
42
+ if (hasTimezone) {
43
+ const date = new Date(trimmed);
44
+ if (isNaN(date.getTime())) return null;
45
+ return Math.floor(date.getTime() / 1000).toString();
46
+ }
47
+
48
+ // No timezone - treat as Beijing time (UTC+8)
49
+ const normalized = trimmed.replace('T', ' ');
50
+ const match = normalized.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})(?::(\d{2}))?$/);
51
+
52
+ if (!match) {
53
+ const date = new Date(trimmed);
54
+ if (isNaN(date.getTime())) return null;
55
+ return Math.floor(date.getTime() / 1000).toString();
56
+ }
57
+
58
+ const [, year, month, day, hour, minute, second] = match;
59
+ const utcDate = new Date(
60
+ Date.UTC(
61
+ parseInt(year),
62
+ parseInt(month) - 1,
63
+ parseInt(day),
64
+ parseInt(hour) - 8, // Beijing time - 8 hours = UTC
65
+ parseInt(minute),
66
+ parseInt(second ?? '0'),
67
+ ),
68
+ );
69
+
70
+ return Math.floor(utcDate.getTime() / 1000).toString();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Convert Unix timestamp to ISO 8601 string in Shanghai timezone.
78
+ */
79
+ function unixTimestampToISO8601(raw: string | number | undefined): string | null {
80
+ if (raw === undefined || raw === null) return null;
81
+
82
+ const text = typeof raw === 'number' ? String(raw) : String(raw).trim();
83
+ if (!/^-?\d+$/.test(text)) return null;
84
+
85
+ const num = Number(text);
86
+ if (!Number.isFinite(num)) return null;
87
+
88
+ const utcMs = Math.abs(num) >= 1e12 ? num : num * 1000;
89
+ const beijingDate = new Date(utcMs + 8 * 60 * 60 * 1000);
90
+ if (Number.isNaN(beijingDate.getTime())) return null;
91
+
92
+ const year = beijingDate.getUTCFullYear();
93
+ const month = String(beijingDate.getUTCMonth() + 1).padStart(2, '0');
94
+ const day = String(beijingDate.getUTCDate()).padStart(2, '0');
95
+ const hour = String(beijingDate.getUTCHours()).padStart(2, '0');
96
+ const minute = String(beijingDate.getUTCMinutes()).padStart(2, '0');
97
+ const second = String(beijingDate.getUTCSeconds()).padStart(2, '0');
98
+
99
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}+08:00`;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Input schemas
104
+ // ---------------------------------------------------------------------------
105
+
106
+ const createActionSchema = {
107
+ action: z.literal('create').describe('Create a calendar event'),
108
+ summary: z.string().optional().describe('Event title (recommended)'),
109
+ start_time: z.string().describe('Start time (ISO 8601 with timezone, e.g., 2024-01-01T00:00:00+08:00)'),
110
+ end_time: z.string().describe('End time (ISO 8601 with timezone, e.g., 2024-01-01T01:00:00+08:00)'),
111
+ calendar_id: z.string().optional().describe('Calendar ID (optional, uses primary if not provided)'),
112
+ description: z.string().optional().describe('Event description'),
113
+ location_name: z.string().optional().describe('Location name'),
114
+ };
115
+
116
+ const listActionSchema = {
117
+ action: z.literal('list').describe('List calendar events in a time range'),
118
+ start_time: z.string().describe('Start time (ISO 8601 with timezone, max 40 days range)'),
119
+ end_time: z.string().describe('End time (ISO 8601 with timezone, max 40 days range)'),
120
+ calendar_id: z.string().optional().describe('Calendar ID (optional, uses primary if not provided)'),
121
+ };
122
+
123
+ const getActionSchema = {
124
+ action: z.literal('get').describe('Get a calendar event by ID'),
125
+ event_id: z.string().describe('Event ID'),
126
+ calendar_id: z.string().optional().describe('Calendar ID (optional, uses primary if not provided)'),
127
+ };
128
+
129
+ const patchActionSchema = {
130
+ action: z.literal('patch').describe('Update a calendar event'),
131
+ event_id: z.string().describe('Event ID'),
132
+ calendar_id: z.string().optional().describe('Calendar ID (optional, uses primary if not provided)'),
133
+ summary: z.string().optional().describe('New event title'),
134
+ description: z.string().optional().describe('New event description'),
135
+ start_time: z.string().optional().describe('New start time (ISO 8601 with timezone)'),
136
+ end_time: z.string().optional().describe('New end time (ISO 8601 with timezone)'),
137
+ };
138
+
139
+ const deleteActionSchema = {
140
+ action: z.literal('delete').describe('Delete a calendar event'),
141
+ event_id: z.string().describe('Event ID'),
142
+ calendar_id: z.string().optional().describe('Calendar ID (optional, uses primary if not provided)'),
143
+ need_notification: z.boolean().optional().describe('Whether to notify attendees (default true)'),
144
+ };
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Tool registration
148
+ // ---------------------------------------------------------------------------
149
+
150
+ export function registerCalendarEventTool(registry: ToolRegistry): void {
151
+ registry.register({
152
+ name: 'feishu_calendar_event_create',
153
+ description: [
154
+ 'Create a Feishu calendar event.',
155
+ '',
156
+ 'Parameters:',
157
+ '- summary: Event title (recommended)',
158
+ '- start_time: Start time (ISO 8601 with timezone)',
159
+ '- end_time: End time (ISO 8601 with timezone)',
160
+ '- calendar_id: Calendar ID (optional)',
161
+ '- description: Event description (optional)',
162
+ '- location_name: Location name (optional)',
163
+ '',
164
+ 'Returns the created event.',
165
+ 'Requires OAuth authorization.',
166
+ ].join('\n'),
167
+ inputSchema: createActionSchema,
168
+ handler: async (args, context) => handleCreate(args, context),
169
+ });
170
+
171
+ registry.register({
172
+ name: 'feishu_calendar_event_list',
173
+ description: [
174
+ 'List Feishu calendar events in a time range.',
175
+ '',
176
+ 'Parameters:',
177
+ '- start_time: Start time (ISO 8601 with timezone, max 40 days range)',
178
+ '- end_time: End time (ISO 8601 with timezone, max 40 days range)',
179
+ '- calendar_id: Calendar ID (optional)',
180
+ '',
181
+ 'Returns list of events.',
182
+ 'Requires OAuth authorization.',
183
+ ].join('\n'),
184
+ inputSchema: listActionSchema,
185
+ handler: async (args, context) => handleList(args, context),
186
+ });
187
+
188
+ registry.register({
189
+ name: 'feishu_calendar_event_get',
190
+ description: [
191
+ 'Get a Feishu calendar event by ID.',
192
+ '',
193
+ 'Parameters:',
194
+ '- event_id: Event ID',
195
+ '- calendar_id: Calendar ID (optional)',
196
+ '',
197
+ 'Returns the event info.',
198
+ 'Requires OAuth authorization.',
199
+ ].join('\n'),
200
+ inputSchema: getActionSchema,
201
+ handler: async (args, context) => handleGet(args, context),
202
+ });
203
+
204
+ registry.register({
205
+ name: 'feishu_calendar_event_patch',
206
+ description: [
207
+ 'Update a Feishu calendar event.',
208
+ '',
209
+ 'Parameters:',
210
+ '- event_id: Event ID',
211
+ '- calendar_id: Calendar ID (optional)',
212
+ '- summary: New event title (optional)',
213
+ '- description: New event description (optional)',
214
+ '- start_time: New start time (optional)',
215
+ '- end_time: New end time (optional)',
216
+ '',
217
+ 'Returns the updated event.',
218
+ 'Requires OAuth authorization.',
219
+ ].join('\n'),
220
+ inputSchema: patchActionSchema,
221
+ handler: async (args, context) => handlePatch(args, context),
222
+ });
223
+
224
+ registry.register({
225
+ name: 'feishu_calendar_event_delete',
226
+ description: [
227
+ 'Delete a Feishu calendar event.',
228
+ '',
229
+ 'Parameters:',
230
+ '- event_id: Event ID',
231
+ '- calendar_id: Calendar ID (optional)',
232
+ '- need_notification: Whether to notify attendees (default true)',
233
+ '',
234
+ 'Returns success status.',
235
+ 'Requires OAuth authorization.',
236
+ ].join('\n'),
237
+ inputSchema: deleteActionSchema,
238
+ handler: async (args, context) => handleDelete(args, context),
239
+ });
240
+
241
+ log.debug('feishu_calendar_event tools registered');
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Helpers
246
+ // ---------------------------------------------------------------------------
247
+
248
+ async function getAccessToken(context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }): Promise<string | ToolResult> {
249
+ const { larkClient, config } = context;
250
+ if (!larkClient) {
251
+ return jsonError('LarkClient not initialized. Check FEISHU_APP_ID and FEISHU_APP_SECRET.');
252
+ }
253
+ const { appId, appSecret, brand } = config;
254
+ if (!appId || !appSecret) {
255
+ return jsonError('Missing FEISHU_APP_ID or FEISHU_APP_SECRET.');
256
+ }
257
+
258
+ const { listStoredTokens } = await import('../../core/token-store.js');
259
+ const tokens = await listStoredTokens(appId);
260
+ if (tokens.length === 0) {
261
+ return jsonError(
262
+ 'No user authorization found. Please use the feishu_oauth tool with action="authorize" to authorize a user first.'
263
+ );
264
+ }
265
+
266
+ const userOpenId = tokens[0].userOpenId;
267
+
268
+ try {
269
+ return await getValidAccessToken({
270
+ userOpenId,
271
+ appId,
272
+ appSecret,
273
+ domain: brand ?? 'feishu',
274
+ });
275
+ } catch (err) {
276
+ if (err instanceof NeedAuthorizationError) {
277
+ return jsonError(
278
+ `User authorization required or expired. Please use feishu_oauth tool with action="authorize" to re-authorize.`,
279
+ { userOpenId }
280
+ );
281
+ }
282
+ throw err;
283
+ }
284
+ }
285
+
286
+ async function resolveCalendarId(
287
+ calendarId: string | undefined,
288
+ larkClient: LarkClient,
289
+ accessToken: string
290
+ ): Promise<string> {
291
+ if (calendarId) return calendarId;
292
+
293
+ const Lark = await import('@larksuiteoapi/node-sdk');
294
+ const opts = Lark.withUserAccessToken(accessToken);
295
+
296
+ const res = await larkClient.sdk.calendar.calendar.primary({}, opts);
297
+ assertLarkOk(res);
298
+
299
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
300
+ const data = res.data as any;
301
+ const cid = data?.calendars?.[0]?.calendar?.calendar_id;
302
+
303
+ if (!cid) {
304
+ throw new Error('Could not determine primary calendar');
305
+ }
306
+
307
+ log.info(`resolveCalendarId: primary() returned calendar_id=${cid}`);
308
+ return cid;
309
+ }
310
+
311
+ function normalizeEventTimeFields(event: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
312
+ if (!event) return event;
313
+
314
+ const normalized: Record<string, unknown> = { ...event };
315
+
316
+ const startTime = event.start_time;
317
+ if (startTime && typeof startTime === 'object' && 'timestamp' in startTime) {
318
+ const ts = (startTime as { timestamp?: unknown }).timestamp;
319
+ const iso = unixTimestampToISO8601(ts as string | number | undefined);
320
+ if (iso) {
321
+ normalized.start_time = iso;
322
+ }
323
+ }
324
+
325
+ const endTime = event.end_time;
326
+ if (endTime && typeof endTime === 'object' && 'timestamp' in endTime) {
327
+ const ts = (endTime as { timestamp?: unknown }).timestamp;
328
+ const iso = unixTimestampToISO8601(ts as string | number | undefined);
329
+ if (iso) {
330
+ normalized.end_time = iso;
331
+ }
332
+ }
333
+
334
+ return normalized;
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // Handlers
339
+ // ---------------------------------------------------------------------------
340
+
341
+ async function handleCreate(
342
+ args: unknown,
343
+ context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }
344
+ ): Promise<ToolResult> {
345
+ const p = args as z.infer<ReturnType<typeof z.object<typeof createActionSchema>>>;
346
+ const { larkClient } = context;
347
+
348
+ if (!p.start_time || !p.end_time) {
349
+ return jsonError('start_time and end_time are required');
350
+ }
351
+
352
+ const startTs = parseTimeToTimestamp(p.start_time);
353
+ const endTs = parseTimeToTimestamp(p.end_time);
354
+
355
+ if (!startTs || !endTs) {
356
+ return jsonError(
357
+ "Invalid time format. Must use ISO 8601 with timezone, e.g., '2024-01-01T00:00:00+08:00'",
358
+ { received_start: p.start_time, received_end: p.end_time }
359
+ );
360
+ }
361
+
362
+ const accessTokenResult = await getAccessToken(context);
363
+ if (typeof accessTokenResult === 'object' && 'content' in accessTokenResult) {
364
+ return accessTokenResult;
365
+ }
366
+ const accessToken = accessTokenResult;
367
+
368
+ const calendarId = await resolveCalendarId(p.calendar_id, larkClient!, accessToken);
369
+
370
+ log.info(`create: summary=${p.summary ?? '(none)'}, start_time=${startTs}, end_time=${endTs}, calendar_id=${calendarId}`);
371
+
372
+ const Lark = await import('@larksuiteoapi/node-sdk');
373
+ const opts = Lark.withUserAccessToken(accessToken);
374
+
375
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
376
+ const eventData: any = {
377
+ summary: p.summary,
378
+ start_time: { timestamp: startTs },
379
+ end_time: { timestamp: endTs },
380
+ need_notification: true,
381
+ };
382
+ if (p.description) eventData.description = p.description;
383
+ if (p.location_name) eventData.location = { name: p.location_name };
384
+
385
+ const res = await larkClient!.sdk.calendar.calendarEvent.create(
386
+ {
387
+ path: { calendar_id: calendarId },
388
+ data: eventData,
389
+ },
390
+ opts
391
+ );
392
+ assertLarkOk(res);
393
+
394
+ log.info(`create: event created, event_id=${res.data?.event?.event_id}`);
395
+
396
+ return json({
397
+ event: normalizeEventTimeFields(res.data?.event as Record<string, unknown> | undefined),
398
+ calendar_id: calendarId,
399
+ });
400
+ }
401
+
402
+ async function handleList(
403
+ args: unknown,
404
+ context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }
405
+ ): Promise<ToolResult> {
406
+ const p = args as z.infer<ReturnType<typeof z.object<typeof listActionSchema>>>;
407
+ const { larkClient } = context;
408
+
409
+ if (!p.start_time || !p.end_time) {
410
+ return jsonError('start_time and end_time are required');
411
+ }
412
+
413
+ const startTs = parseTimeToTimestamp(p.start_time);
414
+ const endTs = parseTimeToTimestamp(p.end_time);
415
+
416
+ if (!startTs || !endTs) {
417
+ return jsonError(
418
+ "Invalid time format. Must use ISO 8601 with timezone, e.g., '2024-01-01T00:00:00+08:00'",
419
+ { received_start: p.start_time, received_end: p.end_time }
420
+ );
421
+ }
422
+
423
+ const accessTokenResult = await getAccessToken(context);
424
+ if (typeof accessTokenResult === 'object' && 'content' in accessTokenResult) {
425
+ return accessTokenResult;
426
+ }
427
+ const accessToken = accessTokenResult;
428
+
429
+ const calendarId = await resolveCalendarId(p.calendar_id, larkClient!, accessToken);
430
+
431
+ log.info(`list: calendar_id=${calendarId}, start_time=${startTs}, end_time=${endTs}`);
432
+
433
+ const Lark = await import('@larksuiteoapi/node-sdk');
434
+ const opts = Lark.withUserAccessToken(accessToken);
435
+
436
+ const res = await larkClient!.sdk.calendar.calendarEvent.instanceView(
437
+ {
438
+ path: { calendar_id: calendarId },
439
+ params: {
440
+ start_time: startTs,
441
+ end_time: endTs,
442
+ user_id_type: 'open_id' as const,
443
+ },
444
+ },
445
+ opts
446
+ );
447
+ assertLarkOk(res);
448
+
449
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
450
+ const data = res.data as any;
451
+
452
+ log.info(`list: returned ${data?.items?.length ?? 0} events`);
453
+
454
+ // Normalize time fields
455
+ const events = (data?.items ?? []).map((item: unknown) => normalizeEventTimeFields(item as Record<string, unknown>));
456
+
457
+ return json({
458
+ events,
459
+ has_more: data?.has_more ?? false,
460
+ page_token: data?.page_token,
461
+ });
462
+ }
463
+
464
+ async function handleGet(
465
+ args: unknown,
466
+ context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }
467
+ ): Promise<ToolResult> {
468
+ const p = args as z.infer<ReturnType<typeof z.object<typeof getActionSchema>>>;
469
+ const { larkClient } = context;
470
+
471
+ if (!p.event_id) {
472
+ return jsonError('event_id is required');
473
+ }
474
+
475
+ const accessTokenResult = await getAccessToken(context);
476
+ if (typeof accessTokenResult === 'object' && 'content' in accessTokenResult) {
477
+ return accessTokenResult;
478
+ }
479
+ const accessToken = accessTokenResult;
480
+
481
+ const calendarId = await resolveCalendarId(p.calendar_id, larkClient!, accessToken);
482
+
483
+ log.info(`get: calendar_id=${calendarId}, event_id=${p.event_id}`);
484
+
485
+ const Lark = await import('@larksuiteoapi/node-sdk');
486
+ const opts = Lark.withUserAccessToken(accessToken);
487
+
488
+ const res = await larkClient!.sdk.calendar.calendarEvent.get(
489
+ {
490
+ path: { calendar_id: calendarId, event_id: p.event_id },
491
+ },
492
+ opts
493
+ );
494
+ assertLarkOk(res);
495
+
496
+ log.info(`get: retrieved event ${p.event_id}`);
497
+
498
+ return json({
499
+ event: normalizeEventTimeFields(res.data?.event as Record<string, unknown> | undefined),
500
+ });
501
+ }
502
+
503
+ async function handlePatch(
504
+ args: unknown,
505
+ context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }
506
+ ): Promise<ToolResult> {
507
+ const p = args as z.infer<ReturnType<typeof z.object<typeof patchActionSchema>>>;
508
+ const { larkClient } = context;
509
+
510
+ if (!p.event_id) {
511
+ return jsonError('event_id is required');
512
+ }
513
+
514
+ const accessTokenResult = await getAccessToken(context);
515
+ if (typeof accessTokenResult === 'object' && 'content' in accessTokenResult) {
516
+ return accessTokenResult;
517
+ }
518
+ const accessToken = accessTokenResult;
519
+
520
+ const calendarId = await resolveCalendarId(p.calendar_id, larkClient!, accessToken);
521
+
522
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
523
+ const updateData: any = {};
524
+
525
+ if (p.start_time) {
526
+ const startTs = parseTimeToTimestamp(p.start_time);
527
+ if (!startTs) {
528
+ return jsonError("Invalid start_time format. Must use ISO 8601 with timezone, e.g., '2024-01-01T00:00:00+08:00'");
529
+ }
530
+ updateData.start_time = { timestamp: startTs };
531
+ }
532
+
533
+ if (p.end_time) {
534
+ const endTs = parseTimeToTimestamp(p.end_time);
535
+ if (!endTs) {
536
+ return jsonError("Invalid end_time format. Must use ISO 8601 with timezone, e.g., '2024-01-01T00:00:00+08:00'");
537
+ }
538
+ updateData.end_time = { timestamp: endTs };
539
+ }
540
+
541
+ if (p.summary) updateData.summary = p.summary;
542
+ if (p.description) updateData.description = p.description;
543
+
544
+ log.info(`patch: calendar_id=${calendarId}, event_id=${p.event_id}, fields=${Object.keys(updateData).join(',')}`);
545
+
546
+ const Lark = await import('@larksuiteoapi/node-sdk');
547
+ const opts = Lark.withUserAccessToken(accessToken);
548
+
549
+ const res = await larkClient!.sdk.calendar.calendarEvent.patch(
550
+ {
551
+ path: { calendar_id: calendarId, event_id: p.event_id },
552
+ data: updateData,
553
+ },
554
+ opts
555
+ );
556
+ assertLarkOk(res);
557
+
558
+ log.info(`patch: updated event ${p.event_id}`);
559
+
560
+ return json({
561
+ event: normalizeEventTimeFields(res.data?.event as Record<string, unknown> | undefined),
562
+ });
563
+ }
564
+
565
+ async function handleDelete(
566
+ args: unknown,
567
+ context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }
568
+ ): Promise<ToolResult> {
569
+ const p = args as z.infer<ReturnType<typeof z.object<typeof deleteActionSchema>>>;
570
+ const { larkClient } = context;
571
+
572
+ if (!p.event_id) {
573
+ return jsonError('event_id is required');
574
+ }
575
+
576
+ const accessTokenResult = await getAccessToken(context);
577
+ if (typeof accessTokenResult === 'object' && 'content' in accessTokenResult) {
578
+ return accessTokenResult;
579
+ }
580
+ const accessToken = accessTokenResult;
581
+
582
+ const calendarId = await resolveCalendarId(p.calendar_id, larkClient!, accessToken);
583
+
584
+ log.info(`delete: calendar_id=${calendarId}, event_id=${p.event_id}, notify=${p.need_notification ?? true}`);
585
+
586
+ const Lark = await import('@larksuiteoapi/node-sdk');
587
+ const opts = Lark.withUserAccessToken(accessToken);
588
+
589
+ const res = await larkClient!.sdk.calendar.calendarEvent.delete(
590
+ {
591
+ path: { calendar_id: calendarId, event_id: p.event_id },
592
+ params: {
593
+ need_notification: p.need_notification === false ? 'false' : 'true',
594
+ },
595
+ },
596
+ opts
597
+ );
598
+ assertLarkOk(res);
599
+
600
+ log.info(`delete: deleted event ${p.event_id}`);
601
+
602
+ return json({
603
+ success: true,
604
+ event_id: p.event_id,
605
+ });
606
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Calendar Tools Index
6
+ *
7
+ * Calendar tools for Feishu/Lark.
8
+ * Adapted from openclaw-lark for MCP Server architecture.
9
+ *
10
+ * Tools:
11
+ * - feishu_calendar: Manage calendars (list, get, primary)
12
+ * - feishu_calendar_event: Manage calendar events
13
+ */
14
+
15
+ import type { ToolRegistry } from '../index.js';
16
+ import { registerCalendarTool } from './calendar.js';
17
+ import { registerCalendarEventTool } from './event.js';
18
+ import { logger } from '../../utils/logger.js';
19
+
20
+ const log = logger('tools:calendar');
21
+
22
+ /**
23
+ * Register all Calendar tools with the given registry.
24
+ */
25
+ export function registerCalendarTools(registry: ToolRegistry): void {
26
+ registerCalendarTool(registry);
27
+ registerCalendarEventTool(registry);
28
+
29
+ log.info('Calendar tools registered', {
30
+ tools: [
31
+ 'feishu_calendar_list',
32
+ 'feishu_calendar_get',
33
+ 'feishu_calendar_primary',
34
+ 'feishu_calendar_event_create',
35
+ 'feishu_calendar_event_list',
36
+ 'feishu_calendar_event_get',
37
+ 'feishu_calendar_event_patch',
38
+ 'feishu_calendar_event_delete',
39
+ ].join(', '),
40
+ });
41
+ }