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,152 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * feishu_wiki_space tool - Manage Feishu Wiki spaces.
6
+ *
7
+ * Actions: list, get, create
8
+ */
9
+
10
+ import { z } from 'zod';
11
+ import type { ToolRegistry } from '../index.js';
12
+ import { LarkClient } from '../../core/lark-client.js';
13
+ import { getValidAccessToken, NeedAuthorizationError } from '../../core/uat-client.js';
14
+ import { assertLarkOk } from '../../core/api-error.js';
15
+ import { json, jsonError, type ToolResult } from '../im/helpers.js';
16
+ import { logger } from '../../utils/logger.js';
17
+
18
+ const log = logger('tools:wiki:space');
19
+
20
+ // Schemas
21
+ const listActionSchema = {
22
+ action: z.literal('list').describe('List wiki spaces'),
23
+ page_size: z.number().min(1).max(50).optional().describe('Page size (default 10)'),
24
+ page_token: z.string().optional().describe('Pagination token'),
25
+ };
26
+
27
+ const getActionSchema = {
28
+ action: z.literal('get').describe('Get a wiki space'),
29
+ space_id: z.string().describe('Space ID'),
30
+ };
31
+
32
+ const createActionSchema = {
33
+ action: z.literal('create').describe('Create a wiki space'),
34
+ name: z.string().optional().describe('Space name'),
35
+ description: z.string().optional().describe('Space description'),
36
+ };
37
+
38
+ async function getAccessToken(context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }): Promise<string | ToolResult> {
39
+ const { larkClient, config } = context;
40
+ if (!larkClient) return jsonError('LarkClient not initialized.');
41
+ const { appId, appSecret, brand } = config;
42
+ if (!appId || !appSecret) return jsonError('Missing FEISHU_APP_ID or FEISHU_APP_SECRET.');
43
+
44
+ const { listStoredTokens } = await import('../../core/token-store.js');
45
+ const tokens = await listStoredTokens(appId);
46
+ if (tokens.length === 0) return jsonError('No user authorization found.');
47
+ const userOpenId = tokens[0].userOpenId;
48
+
49
+ try {
50
+ return await getValidAccessToken({ userOpenId, appId, appSecret, domain: brand ?? 'feishu' });
51
+ } catch (err) {
52
+ if (err instanceof NeedAuthorizationError) return jsonError('User authorization expired.');
53
+ throw err;
54
+ }
55
+ }
56
+
57
+ export function registerWikiSpaceTool(registry: ToolRegistry): void {
58
+ registry.register({
59
+ name: 'feishu_wiki_space_list',
60
+ description: 'List Feishu wiki spaces.\n\nRequires OAuth authorization.',
61
+ inputSchema: listActionSchema,
62
+ handler: async (args, context) => {
63
+ const p = args as z.infer<ReturnType<typeof z.object<typeof listActionSchema>>>;
64
+ const { larkClient } = context;
65
+
66
+ const tokenResult = await getAccessToken(context);
67
+ if (typeof tokenResult === 'object' && 'content' in tokenResult) return tokenResult;
68
+ const accessToken = tokenResult;
69
+
70
+ log.info(`list: page_size=${p.page_size ?? 10}`);
71
+
72
+ const Lark = await import('@larksuiteoapi/node-sdk');
73
+ const opts = Lark.withUserAccessToken(accessToken);
74
+
75
+ const res = await larkClient!.sdk.wiki.space.list(
76
+ { params: { page_size: p.page_size as any, page_token: p.page_token } },
77
+ opts
78
+ );
79
+ assertLarkOk(res);
80
+
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ const data = res.data as any;
83
+
84
+ return json({
85
+ spaces: data?.items,
86
+ has_more: data?.has_more,
87
+ page_token: data?.page_token,
88
+ });
89
+ },
90
+ });
91
+
92
+ registry.register({
93
+ name: 'feishu_wiki_space_get',
94
+ description: 'Get a Feishu wiki space by ID.\n\nRequires OAuth authorization.',
95
+ inputSchema: getActionSchema,
96
+ handler: async (args, context) => {
97
+ const p = args as z.infer<ReturnType<typeof z.object<typeof getActionSchema>>>;
98
+ const { larkClient } = context;
99
+
100
+ const tokenResult = await getAccessToken(context);
101
+ if (typeof tokenResult === 'object' && 'content' in tokenResult) return tokenResult;
102
+ const accessToken = tokenResult;
103
+
104
+ log.info(`get: space_id=${p.space_id}`);
105
+
106
+ const Lark = await import('@larksuiteoapi/node-sdk');
107
+ const opts = Lark.withUserAccessToken(accessToken);
108
+
109
+ const res = await larkClient!.sdk.wiki.space.get(
110
+ { path: { space_id: p.space_id } },
111
+ opts
112
+ );
113
+ assertLarkOk(res);
114
+
115
+ return json({ space: res.data?.space });
116
+ },
117
+ });
118
+
119
+ registry.register({
120
+ name: 'feishu_wiki_space_create',
121
+ description: 'Create a Feishu wiki space.\n\nRequires OAuth authorization.',
122
+ inputSchema: createActionSchema,
123
+ handler: async (args, context) => {
124
+ const p = args as z.infer<ReturnType<typeof z.object<typeof createActionSchema>>>;
125
+ const { larkClient } = context;
126
+
127
+ const tokenResult = await getAccessToken(context);
128
+ if (typeof tokenResult === 'object' && 'content' in tokenResult) return tokenResult;
129
+ const accessToken = tokenResult;
130
+
131
+ log.info(`create: name=${p.name ?? '(empty)'}`);
132
+
133
+ const Lark = await import('@larksuiteoapi/node-sdk');
134
+ const opts = Lark.withUserAccessToken(accessToken);
135
+
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ const data: any = {};
138
+ if (p.name) data.name = p.name;
139
+ if (p.description) data.description = p.description;
140
+
141
+ const res = await larkClient!.sdk.wiki.space.create({ data }, opts);
142
+ assertLarkOk(res);
143
+
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ const spaceData = res.data?.space as any;
146
+
147
+ return json({ space: res.data?.space, space_id: spaceData?.space_id });
148
+ },
149
+ });
150
+
151
+ log.debug('feishu_wiki_space tools registered');
152
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Utils module exports for cc-lark MCP Server.
6
+ */
7
+
8
+ export * from './logger.js';
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Simple logger utility for the cc-lark MCP Server.
6
+ *
7
+ * Provides prefixed logging with ANSI color support for console output.
8
+ * Adapted from openclaw-lark lark-logger for MCP Server architecture.
9
+ */
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Public interface
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface Logger {
16
+ readonly subsystem: string;
17
+ debug(message: string, meta?: Record<string, unknown>): void;
18
+ info(message: string, meta?: Record<string, unknown>): void;
19
+ warn(message: string, meta?: Record<string, unknown>): void;
20
+ error(message: string, meta?: Record<string, unknown>): void;
21
+ child(name: string): Logger;
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // ANSI colors
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const CYAN = '\x1b[36m';
29
+ const YELLOW = '\x1b[33m';
30
+ const RED = '\x1b[31m';
31
+ const GRAY = '\x1b[90m';
32
+ const RESET = '\x1b[0m';
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Message formatting
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Format a log message with optional metadata.
40
+ *
41
+ * @param prefix - Log prefix (subsystem tag)
42
+ * @param message - Log message
43
+ * @param meta - Optional metadata to include
44
+ * @returns Formatted message string
45
+ */
46
+ function formatMessage(
47
+ prefix: string,
48
+ message: string,
49
+ meta: Record<string, unknown> | undefined
50
+ ): string {
51
+ if (!meta || Object.keys(meta).length === 0) {
52
+ return `${prefix} ${message}`;
53
+ }
54
+
55
+ const parts = Object.entries(meta)
56
+ .map(([k, v]) => {
57
+ if (v === undefined || v === null) return null;
58
+ if (typeof v === 'object') return `${k}=${JSON.stringify(v)}`;
59
+ return `${k}=${v}`;
60
+ })
61
+ .filter(Boolean);
62
+
63
+ return parts.length > 0 ? `${prefix} ${message} (${parts.join(', ')})` : `${prefix} ${message}`;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Logger implementation
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Create a new logger instance.
72
+ *
73
+ * @param subsystem - Subsystem name for log prefix
74
+ * @returns Logger instance
75
+ */
76
+ function createLogger(subsystem: string): Logger {
77
+ const tag = `cc-lark/${subsystem}`;
78
+
79
+ return {
80
+ subsystem,
81
+
82
+ debug(message: string, meta?: Record<string, unknown>): void {
83
+ const formatted = formatMessage(tag, message, meta);
84
+ console.debug(`${GRAY}[DEBUG]${RESET}`, formatted);
85
+ },
86
+
87
+ info(message: string, meta?: Record<string, unknown>): void {
88
+ const formatted = formatMessage(tag, message, meta);
89
+ console.log(`${CYAN}[INFO]${RESET}`, formatted);
90
+ },
91
+
92
+ warn(message: string, meta?: Record<string, unknown>): void {
93
+ const formatted = formatMessage(tag, message, meta);
94
+ console.warn(`${YELLOW}[WARN]${RESET}`, formatted);
95
+ },
96
+
97
+ error(message: string, meta?: Record<string, unknown>): void {
98
+ const formatted = formatMessage(tag, message, meta);
99
+ console.error(`${RED}[ERROR]${RESET}`, formatted);
100
+ },
101
+
102
+ child(name: string): Logger {
103
+ return createLogger(`${subsystem}/${name}`);
104
+ },
105
+ };
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Public factory
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Create a logger for a subsystem.
114
+ *
115
+ * @param subsystem - Name of the subsystem (e.g., "api", "tools", "config")
116
+ * @returns Logger instance
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const log = logger("api");
121
+ * log.info("Request started", { method: "GET", path: "/users" });
122
+ * log.error("Request failed", { error: "Connection timeout" });
123
+ * ```
124
+ */
125
+ export function logger(subsystem: string): Logger {
126
+ return createLogger(subsystem);
127
+ }
128
+
129
+ /**
130
+ * Default logger instance for general use.
131
+ */
132
+ export const defaultLogger = logger('core');
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Tests for the configuration module.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9
+ import {
10
+ loadConfig,
11
+ validateConfig,
12
+ loadAndValidateConfig,
13
+ hasUserAccessToken,
14
+ getApiBaseUrl,
15
+ ENV_VARS,
16
+ } from '../../src/core/config.js';
17
+
18
+ describe('config', () => {
19
+ const originalEnv = { ...process.env };
20
+
21
+ beforeEach(() => {
22
+ // Clear relevant environment variables before each test
23
+ Object.values(ENV_VARS).forEach(key => {
24
+ delete process.env[key];
25
+ });
26
+ });
27
+
28
+ afterEach(() => {
29
+ // Restore original environment
30
+ Object.values(ENV_VARS).forEach(key => {
31
+ delete process.env[key];
32
+ });
33
+ Object.entries(originalEnv).forEach(([key, value]) => {
34
+ if (value !== undefined) {
35
+ process.env[key] = value;
36
+ }
37
+ });
38
+ });
39
+
40
+ describe('loadConfig', () => {
41
+ it('should load configuration from environment variables', () => {
42
+ process.env[ENV_VARS.APP_ID] = 'test-app-id';
43
+ process.env[ENV_VARS.APP_SECRET] = 'test-app-secret';
44
+ process.env[ENV_VARS.USER_ACCESS_TOKEN] = 'test-user-token';
45
+ process.env[ENV_VARS.BRAND] = 'lark';
46
+
47
+ const config = loadConfig();
48
+
49
+ expect(config.appId).toBe('test-app-id');
50
+ expect(config.appSecret).toBe('test-app-secret');
51
+ expect(config.userAccessToken).toBe('test-user-token');
52
+ expect(config.brand).toBe('lark');
53
+ });
54
+
55
+ it('should return empty strings for missing required values', () => {
56
+ const config = loadConfig();
57
+
58
+ expect(config.appId).toBe('');
59
+ expect(config.appSecret).toBe('');
60
+ expect(config.userAccessToken).toBeUndefined();
61
+ });
62
+
63
+ it('should default brand to feishu if not specified', () => {
64
+ process.env[ENV_VARS.APP_ID] = 'test-app-id';
65
+ process.env[ENV_VARS.APP_SECRET] = 'test-app-secret';
66
+
67
+ const config = loadConfig();
68
+
69
+ expect(config.brand).toBe('feishu');
70
+ });
71
+
72
+ it('should load optional encrypt key and verification token', () => {
73
+ process.env[ENV_VARS.APP_ID] = 'test-app-id';
74
+ process.env[ENV_VARS.APP_SECRET] = 'test-app-secret';
75
+ process.env[ENV_VARS.ENCRYPT_KEY] = 'test-encrypt-key';
76
+ process.env[ENV_VARS.VERIFICATION_TOKEN] = 'test-verification-token';
77
+
78
+ const config = loadConfig();
79
+
80
+ expect(config.encryptKey).toBe('test-encrypt-key');
81
+ expect(config.verificationToken).toBe('test-verification-token');
82
+ });
83
+ });
84
+
85
+ describe('validateConfig', () => {
86
+ it('should pass validation with valid config', () => {
87
+ const config = {
88
+ appId: 'test-app-id',
89
+ appSecret: 'test-app-secret',
90
+ brand: 'feishu' as const,
91
+ };
92
+
93
+ const result = validateConfig(config);
94
+
95
+ expect(result.valid).toBe(true);
96
+ expect(result.errors).toHaveLength(0);
97
+ expect(result.config).toEqual(config);
98
+ });
99
+
100
+ it('should fail validation when appId is missing', () => {
101
+ const config = {
102
+ appId: '',
103
+ appSecret: 'test-app-secret',
104
+ };
105
+
106
+ const result = validateConfig(config);
107
+
108
+ expect(result.valid).toBe(false);
109
+ expect(result.errors).toContain(`Missing required environment variable: ${ENV_VARS.APP_ID}`);
110
+ });
111
+
112
+ it('should fail validation when appSecret is missing', () => {
113
+ const config = {
114
+ appId: 'test-app-id',
115
+ appSecret: '',
116
+ };
117
+
118
+ const result = validateConfig(config);
119
+
120
+ expect(result.valid).toBe(false);
121
+ expect(result.errors).toContain(`Missing required environment variable: ${ENV_VARS.APP_SECRET}`);
122
+ });
123
+
124
+ it('should fail validation for invalid brand value', () => {
125
+ const config = {
126
+ appId: 'test-app-id',
127
+ appSecret: 'test-app-secret',
128
+ brand: 'invalid-brand' as const,
129
+ };
130
+
131
+ const result = validateConfig(config);
132
+
133
+ expect(result.valid).toBe(false);
134
+ expect(result.errors[0]).toContain('Invalid FEISHU_BRAND value');
135
+ });
136
+
137
+ it('should accept custom HTTPS URL as brand', () => {
138
+ const config = {
139
+ appId: 'test-app-id',
140
+ appSecret: 'test-app-secret',
141
+ brand: 'https://custom.lark.com' as const,
142
+ };
143
+
144
+ const result = validateConfig(config);
145
+
146
+ expect(result.valid).toBe(true);
147
+ });
148
+
149
+ it('should accept "lark" as brand', () => {
150
+ const config = {
151
+ appId: 'test-app-id',
152
+ appSecret: 'test-app-secret',
153
+ brand: 'lark' as const,
154
+ };
155
+
156
+ const result = validateConfig(config);
157
+
158
+ expect(result.valid).toBe(true);
159
+ });
160
+
161
+ it('should report multiple validation errors', () => {
162
+ const config = {
163
+ appId: '',
164
+ appSecret: '',
165
+ };
166
+
167
+ const result = validateConfig(config);
168
+
169
+ expect(result.valid).toBe(false);
170
+ expect(result.errors).toHaveLength(2);
171
+ });
172
+ });
173
+
174
+ describe('loadAndValidateConfig', () => {
175
+ it('should load and return valid config', () => {
176
+ process.env[ENV_VARS.APP_ID] = 'test-app-id';
177
+ process.env[ENV_VARS.APP_SECRET] = 'test-app-secret';
178
+
179
+ const config = loadAndValidateConfig();
180
+
181
+ expect(config.appId).toBe('test-app-id');
182
+ expect(config.appSecret).toBe('test-app-secret');
183
+ });
184
+
185
+ it('should throw error for invalid config', () => {
186
+ expect(() => loadAndValidateConfig()).toThrow('Invalid configuration');
187
+ });
188
+ });
189
+
190
+ describe('hasUserAccessToken', () => {
191
+ it('should return true when user access token is set', () => {
192
+ const config = {
193
+ appId: 'test-app-id',
194
+ appSecret: 'test-app-secret',
195
+ userAccessToken: 'test-token',
196
+ };
197
+
198
+ expect(hasUserAccessToken(config)).toBe(true);
199
+ });
200
+
201
+ it('should return false when user access token is not set', () => {
202
+ const config = {
203
+ appId: 'test-app-id',
204
+ appSecret: 'test-app-secret',
205
+ };
206
+
207
+ expect(hasUserAccessToken(config)).toBe(false);
208
+ });
209
+
210
+ it('should return false when user access token is empty string', () => {
211
+ const config = {
212
+ appId: 'test-app-id',
213
+ appSecret: 'test-app-secret',
214
+ userAccessToken: '',
215
+ };
216
+
217
+ expect(hasUserAccessToken(config)).toBe(false);
218
+ });
219
+ });
220
+
221
+ describe('getApiBaseUrl', () => {
222
+ it('should return Feishu URL for feishu brand', () => {
223
+ expect(getApiBaseUrl('feishu')).toBe('https://open.feishu.cn/open-apis');
224
+ });
225
+
226
+ it('should return Lark URL for lark brand', () => {
227
+ expect(getApiBaseUrl('lark')).toBe('https://open.larksuite.com/open-apis');
228
+ });
229
+
230
+ it('should return custom URL for custom brand', () => {
231
+ expect(getApiBaseUrl('https://custom.api.com')).toBe('https://custom.api.com');
232
+ });
233
+
234
+ it('should default to feishu when no brand specified', () => {
235
+ expect(getApiBaseUrl()).toBe('https://open.feishu.cn/open-apis');
236
+ });
237
+ });
238
+ });