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,149 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * feishu_fetch_doc tool - Get document content.
6
+ *
7
+ * Fetches Feishu document content, returning title and Markdown content.
8
+ * Supports pagination for large documents.
9
+ *
10
+ * Adapted from openclaw-lark for MCP Server architecture.
11
+ */
12
+
13
+ import { z } from 'zod';
14
+ import type { ToolRegistry } from '../index.js';
15
+ import { LarkClient } from '../../core/lark-client.js';
16
+ import { getValidAccessToken, NeedAuthorizationError } from '../../core/uat-client.js';
17
+ import { callMcpTool, json, jsonError, processMcpResult, type ToolResult } from './shared.js';
18
+ import { logger } from '../../utils/logger.js';
19
+
20
+ const log = logger('tools:doc:fetch');
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Input schema
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const fetchDocSchema = {
27
+ doc_id: z.string().describe('Document ID or URL (supports auto-parsing)'),
28
+ offset: z
29
+ .number()
30
+ .int()
31
+ .min(0)
32
+ .optional()
33
+ .describe('Character offset (optional, default 0). Use for paginating large documents.'),
34
+ limit: z
35
+ .number()
36
+ .int()
37
+ .min(1)
38
+ .optional()
39
+ .describe('Maximum characters to return (optional). Use only when pagination is requested.'),
40
+ };
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Tool registration
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Register the feishu_fetch_doc tool.
48
+ */
49
+ export function registerFetchDocTool(registry: ToolRegistry): void {
50
+ registry.register({
51
+ name: 'feishu_fetch_doc',
52
+ description: [
53
+ 'Fetch Feishu document content, returning title and Markdown format.',
54
+ '',
55
+ 'Usage:',
56
+ '- Provide doc_id (document ID or full URL)',
57
+ '- Use offset and limit for paginating large documents',
58
+ '',
59
+ 'Parameters:',
60
+ '- doc_id: Document ID or URL (required)',
61
+ '- offset: Character offset for pagination (optional, default 0)',
62
+ '- limit: Maximum characters to return (optional)',
63
+ '',
64
+ 'Returns:',
65
+ '- { title, content, has_more? } where content is Markdown text',
66
+ '',
67
+ 'Requires OAuth authorization (use feishu_oauth tool first).',
68
+ ].join('\n'),
69
+ inputSchema: fetchDocSchema,
70
+ handler: async (args, context) => {
71
+ return handleFetchDoc(args, context);
72
+ },
73
+ });
74
+
75
+ log.debug('feishu_fetch_doc tool registered');
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Handler
80
+ // ---------------------------------------------------------------------------
81
+
82
+ async function handleFetchDoc(
83
+ args: unknown,
84
+ context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }
85
+ ): Promise<ToolResult> {
86
+ const p = args as Record<string, unknown>;
87
+ const { larkClient, config } = context;
88
+
89
+ if (!larkClient) {
90
+ return jsonError('LarkClient not initialized. Check FEISHU_APP_ID and FEISHU_APP_SECRET.');
91
+ }
92
+
93
+ const { appId, appSecret, brand } = config;
94
+ if (!appId || !appSecret) {
95
+ return jsonError('Missing FEISHU_APP_ID or FEISHU_APP_SECRET.');
96
+ }
97
+
98
+ // Get the first stored user token
99
+ const { listStoredTokens } = await import('../../core/token-store.js');
100
+ const tokens = await listStoredTokens(appId);
101
+ if (tokens.length === 0) {
102
+ return jsonError(
103
+ 'No user authorization found. Please use the feishu_oauth tool with action="authorize" to authorize a user first.'
104
+ );
105
+ }
106
+
107
+ const userOpenId = tokens[0].userOpenId;
108
+
109
+ try {
110
+ const accessToken = await getValidAccessToken({
111
+ userOpenId,
112
+ appId,
113
+ appSecret,
114
+ domain: brand ?? 'feishu',
115
+ });
116
+
117
+ log.info('Fetching document', {
118
+ doc_id: p.doc_id,
119
+ offset: p.offset,
120
+ limit: p.limit,
121
+ });
122
+
123
+ // Build MCP tool arguments
124
+ const mcpArgs: Record<string, unknown> = {
125
+ doc_id: p.doc_id,
126
+ };
127
+ if (p.offset !== undefined) mcpArgs.offset = p.offset;
128
+ if (p.limit !== undefined) mcpArgs.limit = p.limit;
129
+
130
+ // Generate a unique tool call ID
131
+ const toolCallId = `fetch-doc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
132
+
133
+ // Call the MCP endpoint
134
+ const result = await callMcpTool('fetch-doc', mcpArgs, toolCallId, accessToken);
135
+
136
+ log.info('Document fetched');
137
+
138
+ return processMcpResult(result);
139
+ } catch (err) {
140
+ if (err instanceof NeedAuthorizationError) {
141
+ return jsonError(
142
+ `User authorization required or expired. Please use feishu_oauth tool with action="authorize" to re-authorize.`,
143
+ { userOpenId }
144
+ );
145
+ }
146
+ log.error('Fetch document failed', { error: err instanceof Error ? err.message : String(err) });
147
+ return jsonError(err instanceof Error ? err.message : String(err));
148
+ }
149
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Doc Tools Index
6
+ *
7
+ * Document tools for Feishu/Lark.
8
+ * Adapted from openclaw-lark for MCP Server architecture.
9
+ *
10
+ * Tools:
11
+ * - feishu_create_doc: Create new docx document from Markdown
12
+ * - feishu_fetch_doc: Get document content
13
+ * - feishu_update_doc: Update document content
14
+ */
15
+
16
+ import type { ToolRegistry } from '../index.js';
17
+ import { registerCreateDocTool } from './create.js';
18
+ import { registerFetchDocTool } from './fetch.js';
19
+ import { registerUpdateDocTool } from './update.js';
20
+ import { logger } from '../../utils/logger.js';
21
+
22
+ const log = logger('tools:doc');
23
+
24
+ // Re-export submodules for external use
25
+ export * from './shared.js';
26
+
27
+ /**
28
+ * Register all Doc tools with the given registry.
29
+ */
30
+ export function registerDocTools(registry: ToolRegistry): void {
31
+ registerCreateDocTool(registry);
32
+ registerFetchDocTool(registry);
33
+ registerUpdateDocTool(registry);
34
+
35
+ log.info('Doc tools registered', {
36
+ tools: ['feishu_create_doc', 'feishu_fetch_doc', 'feishu_update_doc'].join(', '),
37
+ });
38
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Shared utilities for Doc tools.
6
+ *
7
+ * Adapted from openclaw-lark for MCP Server architecture.
8
+ * - Removed OpenClaw runtime dependencies
9
+ * - Uses direct MCP endpoint calls with user access token
10
+ */
11
+
12
+ import { logger } from '../../utils/logger.js';
13
+ import { getUserAgent } from '../../core/version.js';
14
+
15
+ const log = logger('tools:doc:shared');
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** MCP JSON-RPC success response */
22
+ export interface McpRpcSuccess {
23
+ jsonrpc: '2.0';
24
+ id: number | string;
25
+ result: unknown;
26
+ }
27
+
28
+ /** MCP JSON-RPC error response */
29
+ export interface McpRpcError {
30
+ jsonrpc: '2.0';
31
+ id: number | string | null;
32
+ error: { code: number; message: string; data?: unknown };
33
+ }
34
+
35
+ export type McpRpcResponse = McpRpcSuccess | McpRpcError;
36
+
37
+ /** Tool result type that matches MCP SDK expectations. */
38
+ export type ToolResult = {
39
+ content: Array<{ type: 'text'; text: string }>;
40
+ isError?: boolean;
41
+ };
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // MCP endpoint configuration
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /** Default MCP endpoint for Feishu */
48
+ const DEFAULT_MCP_ENDPOINT = 'https://mcp.feishu.cn/mcp';
49
+
50
+ /**
51
+ * Get the MCP endpoint URL.
52
+ * Priority: FEISHU_MCP_ENDPOINT env var > default
53
+ */
54
+ export function getMcpEndpoint(): string {
55
+ return process.env.FEISHU_MCP_ENDPOINT?.trim() || DEFAULT_MCP_ENDPOINT;
56
+ }
57
+
58
+ /**
59
+ * Build authorization header for MCP requests.
60
+ */
61
+ function buildAuthHeader(): string | undefined {
62
+ const token = process.env.FEISHU_MCP_BEARER_TOKEN?.trim() || process.env.FEISHU_MCP_TOKEN?.trim();
63
+
64
+ if (!token) return undefined;
65
+ return token.toLowerCase().startsWith('bearer ') ? token : `Bearer ${token}`;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // JSON-RPC utilities
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /**
73
+ * Check if a value is a plain object (not null, not array).
74
+ */
75
+ function isRecord(v: unknown): v is Record<string, unknown> {
76
+ return typeof v === 'object' && v !== null;
77
+ }
78
+
79
+ /**
80
+ * Recursively unwrap JSON-RPC result envelopes.
81
+ * Some MCP gateways wrap the result in additional JSON-RPC envelopes.
82
+ */
83
+ export function unwrapJsonRpcResult(v: unknown): unknown {
84
+ if (!isRecord(v)) return v;
85
+
86
+ const hasJsonRpc = typeof v.jsonrpc === 'string';
87
+ const hasId = 'id' in v;
88
+ const hasResult = 'result' in v;
89
+ const hasError = 'error' in v;
90
+
91
+ if (hasJsonRpc && (hasResult || hasError)) {
92
+ if (hasError) {
93
+ const err = v.error;
94
+ if (isRecord(err) && typeof err.message === 'string') {
95
+ throw new Error(err.message);
96
+ }
97
+ throw new Error('MCP returned error, but could not parse message');
98
+ }
99
+ return unwrapJsonRpcResult(v.result);
100
+ }
101
+
102
+ // Some implementations wrap without jsonrpc field
103
+ if (!hasJsonRpc && !hasId && hasResult && !hasError) {
104
+ return unwrapJsonRpcResult(v.result);
105
+ }
106
+
107
+ return v;
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // MCP client
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Call an MCP tool on the Feishu MCP endpoint.
116
+ *
117
+ * @param name - MCP tool name (e.g., 'create-doc', 'fetch-doc')
118
+ * @param args - Tool arguments
119
+ * @param toolCallId - Unique ID for this tool call
120
+ * @param uat - User access token
121
+ * @returns Tool result
122
+ */
123
+ export async function callMcpTool(
124
+ name: string,
125
+ args: Record<string, unknown>,
126
+ toolCallId: string,
127
+ uat: string
128
+ ): Promise<unknown> {
129
+ const endpoint = getMcpEndpoint();
130
+ const auth = buildAuthHeader();
131
+
132
+ const body = {
133
+ jsonrpc: '2.0',
134
+ id: toolCallId,
135
+ method: 'tools/call',
136
+ params: {
137
+ name,
138
+ arguments: args,
139
+ },
140
+ };
141
+
142
+ const headers: Record<string, string> = {
143
+ 'Content-Type': 'application/json',
144
+ 'X-Lark-MCP-UAT': uat,
145
+ 'X-Lark-MCP-Allowed-Tools': name,
146
+ 'User-Agent': getUserAgent(),
147
+ };
148
+ if (auth) headers.authorization = auth;
149
+
150
+ log.debug(`Calling MCP tool: ${name}`, { toolCallId, endpoint });
151
+
152
+ const res = await fetch(endpoint, {
153
+ method: 'POST',
154
+ headers,
155
+ body: JSON.stringify(body),
156
+ });
157
+
158
+ const text = await res.text();
159
+ if (!res.ok) {
160
+ throw new Error(`MCP HTTP ${res.status} ${res.statusText}: ${text.slice(0, 4000)}`);
161
+ }
162
+
163
+ let data: McpRpcResponse;
164
+ try {
165
+ data = JSON.parse(text) as McpRpcResponse;
166
+ } catch {
167
+ throw new Error(`MCP returned non-JSON: ${text.slice(0, 4000)}`);
168
+ }
169
+
170
+ if ('error' in data) {
171
+ throw new Error(`MCP error ${data.error.code}: ${data.error.message}`);
172
+ }
173
+
174
+ return unwrapJsonRpcResult(data.result);
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Result formatting
179
+ // ---------------------------------------------------------------------------
180
+
181
+ /**
182
+ * Format a successful tool result.
183
+ */
184
+ export function json(data: unknown): ToolResult {
185
+ return {
186
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Format an error tool result.
192
+ */
193
+ export function jsonError(message: string, details?: unknown): ToolResult {
194
+ return {
195
+ content: [{ type: 'text', text: JSON.stringify({ error: message, details }, null, 2) }],
196
+ isError: true,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Process MCP tool result into MCP content format.
202
+ * Handles the { content: [{ type, text }] } format from MCP tools/call.
203
+ */
204
+ export function processMcpResult(result: unknown): ToolResult {
205
+ // MCP tools/call returns { content: [{ type, text }] } format
206
+ // Extract the text content and parse if JSON
207
+ if (isRecord(result) && Array.isArray((result as Record<string, unknown>).content)) {
208
+ const mcpContent = (result as Record<string, unknown>).content as Array<{
209
+ type: string;
210
+ text: string;
211
+ }>;
212
+ let details: unknown = result;
213
+ if (mcpContent.length === 1 && mcpContent[0]?.type === 'text') {
214
+ try {
215
+ details = JSON.parse(mcpContent[0].text);
216
+ } catch {
217
+ // text is not JSON, keep original result
218
+ }
219
+ }
220
+ return {
221
+ content: mcpContent.map((c) => ({
222
+ type: 'text' as const,
223
+ text: c.text,
224
+ })),
225
+ };
226
+ }
227
+ return json(result);
228
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * feishu_update_doc tool - Update document content.
6
+ *
7
+ * Updates Feishu document with various modes:
8
+ * - overwrite: Replace entire document
9
+ * - append: Add content at the end
10
+ * - replace_range: Replace a selected range
11
+ * - replace_all: Replace all occurrences of text
12
+ * - insert_before: Insert content before a selection
13
+ * - insert_after: Insert content after a selection
14
+ * - delete_range: Delete a selected range
15
+ *
16
+ * Supports async task status checking via task_id.
17
+ *
18
+ * Adapted from openclaw-lark for MCP Server architecture.
19
+ */
20
+
21
+ import { z } from 'zod';
22
+ import type { ToolRegistry } from '../index.js';
23
+ import { LarkClient } from '../../core/lark-client.js';
24
+ import { getValidAccessToken, NeedAuthorizationError } from '../../core/uat-client.js';
25
+ import { callMcpTool, json, jsonError, processMcpResult, type ToolResult } from './shared.js';
26
+ import { logger } from '../../utils/logger.js';
27
+
28
+ const log = logger('tools:doc:update');
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Input schema
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const updateModeSchema = z.enum([
35
+ 'overwrite',
36
+ 'append',
37
+ 'replace_range',
38
+ 'replace_all',
39
+ 'insert_before',
40
+ 'insert_after',
41
+ 'delete_range',
42
+ ]);
43
+
44
+ const updateDocSchema = {
45
+ doc_id: z.string().optional().describe('Document ID or URL'),
46
+ markdown: z.string().optional().describe('Markdown content'),
47
+ mode: updateModeSchema.describe(
48
+ 'Update mode: overwrite, append, replace_range, replace_all, insert_before, insert_after, delete_range (required)'
49
+ ),
50
+ selection_with_ellipsis: z
51
+ .string()
52
+ .optional()
53
+ .describe('Selection expression: start_content...end_content (mutually exclusive with selection_by_title)'),
54
+ selection_by_title: z
55
+ .string()
56
+ .optional()
57
+ .describe('Title selection: e.g., ## Section Title (mutually exclusive with selection_with_ellipsis)'),
58
+ new_title: z.string().optional().describe('New document title (optional)'),
59
+ task_id: z
60
+ .string()
61
+ .optional()
62
+ .describe('Async task ID for checking task status (optional)'),
63
+ };
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Validation
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function validateUpdateDocParams(p: Record<string, unknown>): void {
70
+ // If task_id is provided, we're just querying status
71
+ if (p.task_id) return;
72
+
73
+ // For update operations, doc_id is required
74
+ if (!p.doc_id) {
75
+ throw new Error('update-doc: doc_id is required when not providing task_id');
76
+ }
77
+
78
+ // Mode is required
79
+ if (!p.mode) {
80
+ throw new Error('update-doc: mode is required');
81
+ }
82
+
83
+ const mode = p.mode as string;
84
+
85
+ // Selection modes require exactly one selection parameter
86
+ const selectionModes = ['replace_range', 'insert_before', 'insert_after', 'delete_range'];
87
+ if (selectionModes.includes(mode)) {
88
+ const hasEllipsis = Boolean(p.selection_with_ellipsis);
89
+ const hasTitle = Boolean(p.selection_by_title);
90
+ if ((hasEllipsis && hasTitle) || (!hasEllipsis && !hasTitle)) {
91
+ throw new Error(
92
+ 'update-doc: For modes replace_range/insert_before/insert_after/delete_range, ' +
93
+ 'exactly one of selection_with_ellipsis or selection_by_title must be provided'
94
+ );
95
+ }
96
+ }
97
+
98
+ // delete_range doesn't need markdown
99
+ const needsMarkdown = mode !== 'delete_range';
100
+ if (needsMarkdown && !p.markdown) {
101
+ throw new Error(`update-doc: markdown is required for mode=${mode}`);
102
+ }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Tool registration
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Register the feishu_update_doc tool.
111
+ */
112
+ export function registerUpdateDocTool(registry: ToolRegistry): void {
113
+ registry.register({
114
+ name: 'feishu_update_doc',
115
+ description: [
116
+ 'Update a Feishu document with various modes.',
117
+ '',
118
+ 'Modes:',
119
+ '- overwrite: Replace entire document content',
120
+ '- append: Add content at the end',
121
+ '- replace_range: Replace selected text range',
122
+ '- replace_all: Replace all occurrences of text',
123
+ '- insert_before: Insert content before selection',
124
+ '- insert_after: Insert content after selection',
125
+ '- delete_range: Delete selected range',
126
+ '',
127
+ 'Selection methods (for range-based modes):',
128
+ '- selection_with_ellipsis: "start_text...end_text" pattern',
129
+ '- selection_by_title: "## Section Title" to select by heading',
130
+ '',
131
+ 'Parameters:',
132
+ '- doc_id: Document ID or URL (required for update)',
133
+ '- mode: Update mode (required)',
134
+ '- markdown: New content (required for most modes)',
135
+ '- selection_with_ellipsis: Range selection pattern',
136
+ '- selection_by_title: Title-based selection',
137
+ '- new_title: New document title (optional)',
138
+ '- task_id: Async task ID for status check',
139
+ '',
140
+ 'Returns:',
141
+ '- { task_id } for async operations',
142
+ '- { status, result? } for task_id queries',
143
+ '',
144
+ 'Requires OAuth authorization (use feishu_oauth tool first).',
145
+ ].join('\n'),
146
+ inputSchema: updateDocSchema,
147
+ handler: async (args, context) => {
148
+ return handleUpdateDoc(args, context);
149
+ },
150
+ });
151
+
152
+ log.debug('feishu_update_doc tool registered');
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Handler
157
+ // ---------------------------------------------------------------------------
158
+
159
+ async function handleUpdateDoc(
160
+ args: unknown,
161
+ context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }
162
+ ): Promise<ToolResult> {
163
+ const p = args as Record<string, unknown>;
164
+ const { larkClient, config } = context;
165
+
166
+ if (!larkClient) {
167
+ return jsonError('LarkClient not initialized. Check FEISHU_APP_ID and FEISHU_APP_SECRET.');
168
+ }
169
+
170
+ const { appId, appSecret, brand } = config;
171
+ if (!appId || !appSecret) {
172
+ return jsonError('Missing FEISHU_APP_ID or FEISHU_APP_SECRET.');
173
+ }
174
+
175
+ // Validate parameters
176
+ try {
177
+ validateUpdateDocParams(p);
178
+ } catch (err) {
179
+ return jsonError(err instanceof Error ? err.message : String(err));
180
+ }
181
+
182
+ // Get the first stored user token
183
+ const { listStoredTokens } = await import('../../core/token-store.js');
184
+ const tokens = await listStoredTokens(appId);
185
+ if (tokens.length === 0) {
186
+ return jsonError(
187
+ 'No user authorization found. Please use the feishu_oauth tool with action="authorize" to authorize a user first.'
188
+ );
189
+ }
190
+
191
+ const userOpenId = tokens[0].userOpenId;
192
+
193
+ try {
194
+ const accessToken = await getValidAccessToken({
195
+ userOpenId,
196
+ appId,
197
+ appSecret,
198
+ domain: brand ?? 'feishu',
199
+ });
200
+
201
+ log.info('Updating document', {
202
+ doc_id: p.doc_id,
203
+ mode: p.mode,
204
+ has_markdown: !!p.markdown,
205
+ selection_with_ellipsis: p.selection_with_ellipsis ? '(provided)' : undefined,
206
+ selection_by_title: p.selection_by_title,
207
+ new_title: p.new_title,
208
+ task_id: p.task_id,
209
+ });
210
+
211
+ // Build MCP tool arguments
212
+ const mcpArgs: Record<string, unknown> = {};
213
+ if (p.doc_id) mcpArgs.doc_id = p.doc_id;
214
+ if (p.markdown) mcpArgs.markdown = p.markdown;
215
+ if (p.mode) mcpArgs.mode = p.mode;
216
+ if (p.selection_with_ellipsis) mcpArgs.selection_with_ellipsis = p.selection_with_ellipsis;
217
+ if (p.selection_by_title) mcpArgs.selection_by_title = p.selection_by_title;
218
+ if (p.new_title) mcpArgs.new_title = p.new_title;
219
+ if (p.task_id) mcpArgs.task_id = p.task_id;
220
+
221
+ // Generate a unique tool call ID
222
+ const toolCallId = `update-doc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
223
+
224
+ // Call the MCP endpoint
225
+ const result = await callMcpTool('update-doc', mcpArgs, toolCallId, accessToken);
226
+
227
+ log.info('Document updated/task queried');
228
+
229
+ return processMcpResult(result);
230
+ } catch (err) {
231
+ if (err instanceof NeedAuthorizationError) {
232
+ return jsonError(
233
+ `User authorization required or expired. Please use feishu_oauth tool with action="authorize" to re-authorize.`,
234
+ { userOpenId }
235
+ );
236
+ }
237
+ log.error('Update document failed', { error: err instanceof Error ? err.message : String(err) });
238
+ return jsonError(err instanceof Error ? err.message : String(err));
239
+ }
240
+ }