feishu-user-plugin 1.3.0 → 1.3.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.2.0",
4
- "description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 33 tools + 9 skills, 3 auth layers.",
3
+ "version": "1.3.2",
4
+ "description": "All-in-one Feishu plugin for Claude Code — send messages as yourself (incl. real @-mentions), read chats, manage docs/tables/wiki. 66 tools + 9 skills, 3 auth layers.",
5
5
  "author": {
6
6
  "name": "EthanQC"
7
7
  },
package/README.md CHANGED
@@ -3,22 +3,25 @@
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
4
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org)
5
5
  [![MCP](https://img.shields.io/badge/MCP-Compatible-purple.svg)](https://modelcontextprotocol.io)
6
- [![Tools](https://img.shields.io/badge/Tools-33-orange.svg)](#tools-33-total)
6
+ [![Tools](https://img.shields.io/badge/Tools-76-orange.svg)](#tools)
7
7
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
8
8
 
9
- **All-in-one Feishu/Lark MCP Server -- 33 tools, 9 skills, 3 auth layers for messaging, docs, tables, wiki, and drive.**
9
+ **All-in-one Feishu/Lark MCP Server -- 76 tools, 9 skills, 3 auth layers for messaging, docs, bitable, calendar, tasks, drive, and more.**
10
10
 
11
- The only MCP server that lets you send messages as your **personal identity** (not a bot), while also integrating the full official Feishu API for documents, spreadsheets, wikis, and more.
11
+ The only MCP server that lets you send messages as your **personal identity** (not a bot), while also integrating the full official Feishu API. Works with Claude Code, Cursor, Windsurf, OpenClaw, and any MCP-compatible client.
12
12
 
13
13
  ## Highlights
14
14
 
15
15
  - **Send as yourself** -- Messages show your real name, not a bot. Supports text, rich text, images, files, stickers, and audio.
16
16
  - **Read everything** -- Group chats via bot API, P2P (direct messages) via OAuth UAT.
17
- - **Full Feishu suite** -- Docs, Bitable (spreadsheets), Wiki, Drive, Contacts -- all in one plugin.
18
- - **3 auth layers** -- Cookie-based user identity, app credentials (Official API), and OAuth UAT (P2P reading). All three are needed for full functionality.
17
+ - **Full Feishu suite** -- Docs, Bitable, Wiki, Drive, Calendar, Tasks, Contacts -- all in one plugin.
18
+ - **3 auth layers** -- Cookie-based user identity, app credentials (Official API), and OAuth UAT (P2P reading).
19
+ - **Group management** -- Create groups, add/remove members, pin messages, emoji reactions.
20
+ - **Document editing** -- Not just read/create, but insert/update/delete content blocks.
21
+ - **Calendar & Tasks** -- Create events, check free/busy, manage tasks.
19
22
  - **9 slash commands** for Claude Code -- `/send`, `/reply`, `/search`, `/digest`, `/doc`, `/table`, `/wiki`, `/drive`, `/status`
20
23
  - **Auto session management** -- Cookie heartbeat every 4h, UAT auto-refresh with token rotation.
21
- - **Chat name resolution** -- Pass a group name instead of `oc_xxx` ID; it resolves automatically.
24
+ - **Multi-platform** -- Claude Code, Cursor, Windsurf, VS Code, OpenClaw.
22
25
 
23
26
  ## Why This Exists
24
27
 
@@ -284,6 +287,34 @@ Add to `.vscode/mcp.json` in your project:
284
287
  }
285
288
  ```
286
289
 
290
+ ### OpenClaw
291
+
292
+ Add to `~/.openclaw/openclaw.json` (note: key path is `mcp.servers`, not `mcpServers`):
293
+
294
+ ```json
295
+ {
296
+ "mcp": {
297
+ "servers": {
298
+ "feishu-user-plugin": {
299
+ "command": "npx",
300
+ "args": ["-y", "feishu-user-plugin"],
301
+ "env": {
302
+ "LARK_COOKIE": "your-cookie-string",
303
+ "LARK_APP_ID": "cli_xxxxxxxxxxxx",
304
+ "LARK_APP_SECRET": "your-app-secret",
305
+ "LARK_USER_ACCESS_TOKEN": "your-uat",
306
+ "LARK_USER_REFRESH_TOKEN": "your-refresh-token"
307
+ }
308
+ }
309
+ }
310
+ }
311
+ }
312
+ ```
313
+
314
+ Or via CLI: `openclaw mcp set feishu-user-plugin '{"command":"npx","args":["-y","feishu-user-plugin"],"env":{...}}'`
315
+
316
+ > OpenClaw's built-in Feishu channel handles receiving messages (bot identity). This plugin adds user identity messaging + docs/bitable/calendar/tasks.
317
+
287
318
  ### Windsurf
288
319
 
289
320
  Add to `~/.codeium/windsurf/mcp_config.json`:
@@ -306,86 +337,119 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
306
337
  }
307
338
  ```
308
339
 
309
- ## Tools (33 total)
340
+ ## Tools (76 total)
310
341
 
311
- ### User Identity -- Messaging (cookie auth, Protobuf)
312
-
313
- Send messages as yourself, not as a bot.
342
+ ### User Identity -- Messaging (8 tools, cookie auth)
314
343
 
315
344
  | Tool | Description |
316
345
  |------|-------------|
317
346
  | `send_to_user` | Search user by name + send text -- one step |
318
347
  | `send_to_group` | Search group by name + send text -- one step |
319
- | `send_as_user` | Send text to any chat by ID, supports reply threading (`root_id` / `parent_id`) |
320
- | `send_image_as_user` | Send image (requires `image_key` from upload) |
321
- | `send_file_as_user` | Send file (requires `file_key` from upload) |
322
- | `send_post_as_user` | Send rich text with title + formatted paragraphs (links, @mentions) |
348
+ | `send_as_user` | Send text to any chat by ID, supports reply threading |
349
+ | `send_image_as_user` | Send image (requires `image_key` from `upload_image`) |
350
+ | `send_file_as_user` | Send file (requires `file_key` from `upload_file`) |
351
+ | `send_post_as_user` | Send rich text with title + formatted paragraphs |
323
352
  | `send_sticker_as_user` | Send sticker/emoji |
324
353
  | `send_audio_as_user` | Send audio message |
325
354
 
326
- ### User Identity -- Contacts & Info (cookie auth)
355
+ ### User Identity -- Contacts & Info (5 tools, cookie auth)
327
356
 
328
357
  | Tool | Description |
329
358
  |------|-------------|
330
359
  | `search_contacts` | Search users, bots, or group chats by name |
331
- | `create_p2p_chat` | Create/get P2P (direct message) chat, returns numeric `chat_id` |
332
- | `get_chat_info` | Group details: name, description, member count, owner |
360
+ | `create_p2p_chat` | Create/get P2P (direct message) chat |
361
+ | `get_chat_info` | Group details (supports both oc_xxx and numeric ID) |
333
362
  | `get_user_info` | User display name lookup by user ID |
334
363
  | `get_login_status` | Check cookie, app credentials, and UAT status |
335
364
 
336
- ### User OAuth UAT -- P2P Chat Reading
365
+ ### User OAuth UAT -- P2P Chat Reading (2 tools)
337
366
 
338
367
  | Tool | Description |
339
368
  |------|-------------|
340
- | `read_p2p_messages` | Read P2P (direct message) history. Works for chats the bot cannot access. |
341
- | `list_user_chats` | List group chats the user is in. Note: only returns groups, not P2P. |
369
+ | `read_p2p_messages` | Read P2P (direct message) history |
370
+ | `list_user_chats` | List group chats the user is in |
342
371
 
343
- ### Official API -- IM (Bot Identity)
372
+ ### Official API -- IM (17 tools)
344
373
 
345
374
  | Tool | Description |
346
375
  |------|-------------|
347
376
  | `list_chats` | List all chats the bot has joined |
348
- | `read_messages` | Read message history (accepts chat name or `oc_xxx` ID) |
349
- | `reply_message` | Reply to a specific message by `message_id` (as bot) |
377
+ | `read_messages` | Read message history (accepts chat name, oc_xxx, or numeric ID) |
378
+ | `send_message_as_bot` | Send message as bot to any chat |
379
+ | `reply_message` | Reply to a specific message (as bot) |
350
380
  | `forward_message` | Forward a message to another chat |
351
-
352
- ### Official API -- Documents
381
+ | `delete_message` | Recall/delete a bot message |
382
+ | `update_message` | Edit a sent bot message |
383
+ | `add_reaction` | Add emoji reaction to a message |
384
+ | `delete_reaction` | Remove emoji reaction |
385
+ | `pin_message` | Pin a message in chat |
386
+ | `unpin_message` | Unpin a message |
387
+ | `create_group` | Create a new group chat |
388
+ | `update_group` | Update group name/description |
389
+ | `list_members` | List group members |
390
+ | `add_members` | Add users to a group |
391
+ | `remove_members` | Remove users from a group |
392
+ | `upload_image` / `upload_file` | Upload image/file, returns key for sending |
393
+
394
+ ### Official API -- Documents (7 tools)
353
395
 
354
396
  | Tool | Description |
355
397
  |------|-------------|
356
398
  | `search_docs` | Search documents by keyword |
357
- | `read_doc` | Read raw text content of a document |
399
+ | `read_doc` | Read raw text content |
400
+ | `get_doc_blocks` | Get structured block tree |
358
401
  | `create_doc` | Create a new document |
402
+ | `create_doc_block` | Insert content blocks into a document |
403
+ | `update_doc_block` | Update a specific block |
404
+ | `delete_doc_blocks` | Delete a range of blocks |
405
+
406
+ ### Official API -- Bitable (17 tools)
407
+
408
+ | Tool | Description |
409
+ |------|-------------|
410
+ | `create_bitable` | Create a new Bitable app |
411
+ | `list_bitable_tables` / `create_bitable_table` / `delete_bitable_table` | Table management |
412
+ | `list_bitable_fields` / `create_bitable_field` / `update_bitable_field` / `delete_bitable_field` | Field management |
413
+ | `list_bitable_views` | List views |
414
+ | `search_bitable_records` / `get_bitable_record` | Query records |
415
+ | `create_bitable_record` / `update_bitable_record` / `delete_bitable_record` | Single record CRUD |
416
+ | `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` | Batch operations (max 500/call) |
359
417
 
360
- ### Official API -- Bitable (Spreadsheets)
418
+ ### Official API -- Calendar (5 tools)
361
419
 
362
420
  | Tool | Description |
363
421
  |------|-------------|
364
- | `list_bitable_tables` | List all tables in a Bitable app |
365
- | `list_bitable_fields` | List all fields (columns) in a table |
366
- | `search_bitable_records` | Query records with filter and sort |
367
- | `create_bitable_record` | Create a new record (row) |
368
- | `update_bitable_record` | Update an existing record |
422
+ | `list_calendars` | List accessible calendars |
423
+ | `create_calendar_event` | Create a calendar event |
424
+ | `list_calendar_events` | List events in a calendar |
425
+ | `delete_calendar_event` | Delete an event |
426
+ | `get_freebusy` | Check user availability |
369
427
 
370
- ### Official API -- Wiki
428
+ ### Official API -- Tasks (5 tools)
371
429
 
372
430
  | Tool | Description |
373
431
  |------|-------------|
374
- | `list_wiki_spaces` | List all accessible wiki spaces |
375
- | `search_wiki` | Search wiki/docs by keyword |
376
- | `list_wiki_nodes` | Browse wiki node tree |
432
+ | `create_task` | Create a task |
433
+ | `get_task` | Get task details |
434
+ | `list_tasks` | List tasks |
435
+ | `update_task` | Update a task |
436
+ | `complete_task` | Mark task as complete |
377
437
 
378
- ### Official API -- Drive
438
+ ### Official API -- Drive (5 tools)
379
439
 
380
440
  | Tool | Description |
381
441
  |------|-------------|
382
442
  | `list_files` | List files in a folder |
383
443
  | `create_folder` | Create a new folder |
444
+ | `copy_file` | Copy a file |
445
+ | `move_file` | Move a file |
446
+ | `delete_file` | Delete a file/folder |
384
447
 
385
- ### Official API -- Contacts
448
+ ### Official API -- Wiki & Contacts (4 tools)
386
449
 
387
450
  | Tool | Description |
388
451
  |------|-------------|
452
+ | `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` | Wiki spaces, search, browse |
389
453
  | `find_user` | Find user by email or mobile number |
390
454
 
391
455
  ## Claude Code Slash Commands (9 skills)
@@ -444,7 +508,7 @@ feishu-user-plugin/
444
508
  │ ├── SKILL.md # Main skill definition (trigger, tools, auth)
445
509
  │ └── references/ # 8 skill reference docs + CLAUDE.md
446
510
  ├── src/
447
- │ ├── index.js # MCP server entry point (33 tools)
511
+ │ ├── index.js # MCP server entry point (76 tools)
448
512
  │ ├── client.js # User identity client (Protobuf gateway)
449
513
  │ ├── official.js # Official API client (REST, UAT)
450
514
  │ ├── utils.js # ID generators, cookie parser
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.3.0",
4
- "description": "All-in-one Feishu plugin for Claude Code — messaging, docs, bitable, calendar, tasks, drive. 76 tools + 9 skills, 3 auth layers.",
3
+ "version": "1.3.2",
4
+ "description": "All-in-one Feishu plugin for Claude Code & Codex — messaging, docs, bitable, wiki, drive. 66 tools + 9 skills, 3 auth layers.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "feishu-user-plugin": "src/cli.js"
@@ -10,7 +10,8 @@
10
10
  "start": "node src/index.js",
11
11
  "test": "node src/test-all.js",
12
12
  "test:quick": "node src/test-send.js",
13
- "oauth": "node src/oauth.js"
13
+ "oauth": "node src/oauth.js",
14
+ "prepublishOnly": "node scripts/confirm-version.js"
14
15
  },
15
16
  "keywords": [
16
17
  "feishu",
@@ -37,6 +38,7 @@
37
38
  "files": [
38
39
  "src/",
39
40
  "proto/",
41
+ "scripts/",
40
42
  ".claude-plugin/",
41
43
  "skills/",
42
44
  ".mcp.json.example",
package/proto/lark.proto CHANGED
@@ -140,6 +140,19 @@ message RichText {
140
140
  repeated string elementIds = 1;
141
141
  optional string innerText = 2;
142
142
  optional RichTextElements elements = 3;
143
+ // Index fields: each "*Ids" is a list of elemIds (pointing into the dictionary)
144
+ // that the server needs to register as special-type elements. Discovered from
145
+ // Feishu Web bundle — atIds=6 is what makes @-mentions actually notify.
146
+ repeated string imageIds = 5;
147
+ repeated string atIds = 6;
148
+ repeated string anchorIds = 7;
149
+ repeated string i18nIds = 8;
150
+ repeated string mediaIds = 9;
151
+ repeated string docsIds = 10;
152
+ repeated string interactiveIds = 11;
153
+ repeated string mentionIds = 12;
154
+ optional int32 version = 13;
155
+ repeated string atUserGroupIds = 14;
143
156
  }
144
157
 
145
158
  message RichTextElements {
@@ -167,6 +180,20 @@ message TextProperty {
167
180
  optional string content = 1;
168
181
  }
169
182
 
183
+ // For AT (@-mention) elements. Both fields are required in the real schema;
184
+ // `content` is marked deprecated but still required on the wire.
185
+ message AtProperty {
186
+ optional string userId = 1;
187
+ optional string content = 2;
188
+ }
189
+
190
+ // For A (hyperlink) elements.
191
+ message AnchorProperty {
192
+ optional string href = 1;
193
+ optional string content = 2;
194
+ optional string textContent = 3;
195
+ }
196
+
170
197
  // --- Chat Operations ---
171
198
 
172
199
  // Create P2P chat (cmd=13)
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pre-publish version confirmation gate.
4
+ * Runs as prepublishOnly — blocks `npm publish` until version is confirmed.
5
+ * Automatically skipped in CI (GitHub Actions handles tag/version check separately).
6
+ */
7
+
8
+ if (process.env.CI || process.env.GITHUB_ACTIONS) {
9
+ process.exit(0);
10
+ }
11
+
12
+ const readline = require('readline');
13
+ const pkg = require('../package.json');
14
+
15
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
16
+
17
+ console.log(`\n Package: ${pkg.name}`);
18
+ console.log(` Version: ${pkg.version}`);
19
+ console.log(` Tools: ${pkg.description}\n`);
20
+
21
+ rl.question(` Confirm publish v${pkg.version}? (y/N): `, (answer) => {
22
+ rl.close();
23
+ if (answer.trim().toLowerCase() !== 'y') {
24
+ console.error('\n Publish cancelled. Update version in package.json if needed.\n');
25
+ process.exit(1);
26
+ }
27
+ console.log(' Confirmed. Proceeding with publish...\n');
28
+ });
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+
5
+ const defaultCommand = ['node', '/Users/abble/feishu-user-plugin/src/index.js'];
6
+ const childCommand = process.argv.slice(2);
7
+ const [command, ...args] = childCommand.length > 0 ? childCommand : defaultCommand;
8
+
9
+ const child = spawn(command, args, {
10
+ stdio: ['pipe', 'pipe', 'pipe'],
11
+ env: process.env,
12
+ });
13
+
14
+ let parentMode = null; // 'content-length' | 'newline'
15
+ let parentInBuffer = Buffer.alloc(0);
16
+ let childOutBuffer = '';
17
+
18
+ function writeToParent(jsonLine) {
19
+ if (parentMode === 'content-length') {
20
+ const body = Buffer.from(jsonLine, 'utf8');
21
+ process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
22
+ process.stdout.write(body);
23
+ return;
24
+ }
25
+ process.stdout.write(jsonLine + '\n');
26
+ }
27
+
28
+ function handleParentMessage(message) {
29
+ const line = typeof message === 'string' ? message : JSON.stringify(message);
30
+ child.stdin.write(line + '\n');
31
+ }
32
+
33
+ function tryParseParentBuffer() {
34
+ while (parentInBuffer.length > 0) {
35
+ if (parentMode === 'content-length' || (!parentMode && parentInBuffer.includes(Buffer.from('\r\n\r\n')))) {
36
+ parentMode = 'content-length';
37
+ const headerEnd = parentInBuffer.indexOf(Buffer.from('\r\n\r\n'));
38
+ if (headerEnd === -1) return;
39
+ const headerText = parentInBuffer.subarray(0, headerEnd).toString('utf8');
40
+ const match = headerText.match(/Content-Length:\s*(\d+)/i);
41
+ if (!match) {
42
+ process.stderr.write('[mcp-bridge] Missing Content-Length header\n');
43
+ process.exit(1);
44
+ }
45
+ const bodyLength = Number(match[1]);
46
+ const totalLength = headerEnd + 4 + bodyLength;
47
+ if (parentInBuffer.length < totalLength) return;
48
+ const body = parentInBuffer.subarray(headerEnd + 4, totalLength).toString('utf8');
49
+ parentInBuffer = parentInBuffer.subarray(totalLength);
50
+ handleParentMessage(body);
51
+ continue;
52
+ }
53
+
54
+ parentMode = 'newline';
55
+ const newlineIndex = parentInBuffer.indexOf(0x0a);
56
+ if (newlineIndex === -1) return;
57
+ const line = parentInBuffer.subarray(0, newlineIndex).toString('utf8').replace(/\r$/, '');
58
+ parentInBuffer = parentInBuffer.subarray(newlineIndex + 1);
59
+ if (line.trim()) handleParentMessage(line);
60
+ }
61
+ }
62
+
63
+ process.stdin.on('data', (chunk) => {
64
+ parentInBuffer = Buffer.concat([parentInBuffer, chunk]);
65
+ tryParseParentBuffer();
66
+ });
67
+
68
+ child.stdout.on('data', (chunk) => {
69
+ childOutBuffer += chunk.toString('utf8');
70
+ while (true) {
71
+ const newlineIndex = childOutBuffer.indexOf('\n');
72
+ if (newlineIndex === -1) break;
73
+ const line = childOutBuffer.slice(0, newlineIndex).replace(/\r$/, '');
74
+ childOutBuffer = childOutBuffer.slice(newlineIndex + 1);
75
+ if (line.trim()) writeToParent(line);
76
+ }
77
+ });
78
+
79
+ child.stderr.on('data', (chunk) => {
80
+ process.stderr.write(chunk);
81
+ });
82
+
83
+ child.on('exit', (code, signal) => {
84
+ if (signal) {
85
+ process.stderr.write(`[mcp-bridge] Child exited via signal ${signal}\n`);
86
+ process.exit(1);
87
+ }
88
+ process.exit(code ?? 0);
89
+ });
90
+
91
+ child.on('error', (error) => {
92
+ process.stderr.write(`[mcp-bridge] Failed to spawn child: ${error.message}\n`);
93
+ process.exit(1);
94
+ });
95
+
96
+ process.on('SIGINT', () => child.kill('SIGINT'));
97
+ process.on('SIGTERM', () => child.kill('SIGTERM'));