feishu-user-plugin 1.3.6 → 1.3.8
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.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +71 -0
- package/README.md +72 -41
- package/package.json +10 -3
- package/scripts/capture-feishu-protobuf.js +86 -0
- package/scripts/check-changelog.js +31 -0
- package/scripts/check-docs-sync.js +41 -0
- package/scripts/check-tool-count.js +40 -0
- package/scripts/check-version.js +40 -0
- package/scripts/decode-feishu-protobuf.js +115 -0
- package/scripts/smoke.js +224 -0
- package/scripts/sync-claude-md.sh +12 -0
- package/scripts/sync-server-json.js +71 -0
- package/scripts/sync-team-skills.sh +22 -0
- package/scripts/test-all-tools.js +158 -0
- package/scripts/test-wiki-attach-fallback.js +71 -0
- package/scripts/test-ws-events.js +84 -0
- package/skills/feishu-user-plugin/SKILL.md +5 -5
- package/skills/feishu-user-plugin/references/CLAUDE.md +248 -318
- package/skills/feishu-user-plugin/references/table.md +18 -9
- package/src/auth/cookie.js +30 -0
- package/src/auth/credentials.js +399 -0
- package/src/auth/profile-router.js +248 -0
- package/src/auth/uat.js +231 -0
- package/src/cli.js +45 -13
- package/src/clients/official/base.js +188 -0
- package/src/clients/official/bitable.js +269 -0
- package/src/clients/official/calendar.js +176 -0
- package/src/clients/official/contacts.js +54 -0
- package/src/clients/official/docs.js +301 -0
- package/src/clients/official/drive.js +77 -0
- package/src/clients/official/groups.js +68 -0
- package/src/clients/official/im.js +414 -0
- package/src/clients/official/index.js +30 -0
- package/src/clients/official/okr.js +127 -0
- package/src/clients/official/tasks.js +142 -0
- package/src/clients/official/uploads.js +260 -0
- package/src/clients/official/wiki.js +207 -0
- package/src/{client.js → clients/user.js} +25 -33
- package/src/config.js +13 -8
- package/src/events/event-buffer.js +100 -0
- package/src/events/index.js +5 -0
- package/src/events/ws-server.js +86 -0
- package/src/index.js +4 -1977
- package/src/logger.js +20 -0
- package/src/oauth.js +5 -1
- package/src/official.js +5 -1944
- package/src/prompts/_registry.js +69 -0
- package/src/prompts/index.js +54 -0
- package/src/server.js +305 -0
- package/src/setup.js +16 -1
- package/src/test-all.js +2 -2
- package/src/test-comprehensive.js +3 -3
- package/src/test-send.js +1 -1
- package/src/tools/_registry.js +31 -0
- package/src/tools/bitable.js +246 -0
- package/src/tools/calendar.js +207 -0
- package/src/tools/contacts.js +66 -0
- package/src/tools/diagnostics.js +172 -0
- package/src/tools/docs.js +158 -0
- package/src/tools/drive.js +111 -0
- package/src/tools/events.js +64 -0
- package/src/tools/groups.js +81 -0
- package/src/tools/im-read.js +259 -0
- package/src/tools/messaging-bot.js +151 -0
- package/src/tools/messaging-user.js +292 -0
- package/src/tools/okr.js +159 -0
- package/src/tools/profile.js +74 -0
- package/src/tools/tasks.js +168 -0
- package/src/tools/uploads.js +63 -0
- package/src/tools/wiki.js +191 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself
|
|
3
|
+
"version": "1.3.8",
|
|
4
|
+
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats (auto-expanded merge_forward), manage docs / bitable / wiki (full CRUD) / drive / OKR (with progress writes) / calendar (read+write) / Tasks v2 / multi-profile auto-switch / real-time WS events. 82 tools + 9 prompts, 3 auth layers.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "EthanQC"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,77 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.3.8] - 2026-05-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Multi-profile auto-switch (B)**: when `~/.feishu-user-plugin/credentials.json` has ≥2 profiles, read-only tools (`read_*` / `list_*` / `get_*` / `search_*` / `download_*` plus `manage_bitable_*` read-action variants) auto-retry across profiles on permission-denied errors. Trigger codes `91403 / 1254301 / 1254000 / 99991672 / HTTP 403` plus message patterns. The winning profile is cached to `credentials.json::profileHints` so subsequent calls go straight to the right account. Writes never auto-switch — pass `via_profile: "auto"` per call to opt in. New tool: `manage_profile_hints(action=list|set|clear, resource_key?, profile?)`. Single-profile users see no behaviour change.
|
|
11
|
+
- **Real-time WebSocket events (C)**: MCP server opens a `WSClient` connection at boot when `LARK_APP_ID/SECRET` are configured; events accumulate into an in-memory FIFO buffer (cap 1000) and surface via the new `get_new_events(event_type?, event_types?, chat_id?, since_seconds?, max_events=50, peek=false)` tool. Currently registers `im.message.receive_v1` (replies / group activity). feishu.cn only — Lark international not supported by Feishu's `WSClient`. Buffer is in-memory; events received before MCP boot aren't replayed.
|
|
12
|
+
- **Cookie protobuf wire-format tooling (A.0)**: `scripts/decode-feishu-protobuf.js` decodes a captured payload against `proto/lark.proto` and reports unknown field tags + wire types so we know what to add. `scripts/capture-feishu-protobuf.js` documents the per-type session recipe and runs DECODE on `/tmp/feishu-captures`. Living write-up in `docs/COOKIE-PROTOBUF-CAPTURES.md`. Actual IMAGE / AUDIO / STICKER / CARD / search_messages captures move to v1.3.9 — tooling is in place; capture session is high-touch and preferably done by hand.
|
|
13
|
+
- **`FEISHU_PLUGIN_PROFILE` env override (E.1)**: harness env can pin an active profile, validated at boot (fatal exit 2 on typo). Lets a single `credentials.json` serve different harnesses with different active profiles (Claude Code on "work", Codex on "personal").
|
|
14
|
+
- **`setup --pointer-only` mode (E.2)**: writes only `FEISHU_PLUGIN_PROFILE=default` to harness env; real creds live solely in `credentials.json`. Eliminates env-vs-file divergence on UAT refresh. Opt-in (interactive prompt + flag).
|
|
15
|
+
- **Migrate startup nudge (E.3)**: legacy env-only setups get a one-line TIP at MCP boot pointing at `npx feishu-user-plugin migrate --confirm`. Skipped when `credentials.json` already exists.
|
|
16
|
+
- **CI / docs gates (F)**: `scripts/sync-server-json.js` regenerates `server.json` from `package.json + TOOLS` (was frozen at v1.2.0 / 33 tools — now matches reality). `scripts/check-tool-count.js` extended to verify `SKILL.md::allowed-tools` in addition to README badge. `scripts/check-changelog.js` blocks publish when CHANGELOG has no section for the tag version. `scripts/check-docs-sync.js` enforces CLAUDE.md / AGENTS.md / skill-ref triple-sync at prepublish + CI.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **`src/auth/uat.js` and `src/auth/cookie.js` extracted (D.1, D.2)** from `clients/official/base.js` and `clients/user.js` respectively. State (`this._uat` / `this._heartbeatTimer` / etc.) still lives on the client; only function bodies moved. base.js drops ~200 lines. Closes the v1.3.7 Phase B deferrals noted in `docs/REFACTOR-NOTES.md`.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **G.1 wiki-attach fallback retest scaffold**: `scripts/test-wiki-attach-fallback.js` monkey-patches `attachToWiki` to throw 91403 and verifies `upload_drive_file` surfaces the failure rather than silently uploading to drive root. POSIX skip 77 when missing creds / `FEISHU_TEST_FOLDER_TOKEN`.
|
|
23
|
+
|
|
24
|
+
### Deferred to v1.3.9
|
|
25
|
+
- Cookie protobuf wire format reverse for IMAGE / AUDIO / STICKER / CARD / search_messages — tooling is in place (`scripts/decode-feishu-protobuf.js`, `scripts/capture-feishu-protobuf.js`, `docs/COOKIE-PROTOBUF-CAPTURES.md`), capture sessions are pending.
|
|
26
|
+
- `switch_profile` multi-profile e2e — needs a real second profile in tests.
|
|
27
|
+
- Test group `oc_daaa6a50f2a97dc668aaf79ae4dc6e4e` dissolution — needs owner-permission transfer.
|
|
28
|
+
|
|
29
|
+
## [1.3.7] - 2026-05-04
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **Wiki write (5 tools)**: `create_wiki_node` / `update_wiki_node` / `move_wiki_node` / `copy_wiki_node` / `delete_wiki_node`. UAT-first. `create_wiki_node` builds doc/sheet/bitable/mindnote/file/docx/slides directly inside a wiki space, or `node_type=shortcut` for a pointer. `update_wiki_node` only patches `title` (Feishu wiki API doesn't accept content edits — those go through docx/bitable/sheet). `move`/`copy` accept `target_parent_token` + optional `target_space_id` for cross-space migration. `delete_wiki_node` calls `DELETE /wiki/v2/spaces/{id}/nodes/{token}` via raw REST (SDK doesn't type it) — only deletes the node pointer, not the underlying drive resource.
|
|
33
|
+
- **OKR progress writes (3 tools)**: `create_okr_progress_record` / `list_okr_progress_records` / `delete_okr_progress_record`. UAT-first. Requires `okr:okr.content:write` scope. `create` accepts a simplified `content_text` (auto-wrapped into Feishu's block schema) plus optional `source_title` / `source_url` / `progress_percent`. `list` extracts `{progress_id, target_id, target_type}` triples from `get_okrs` since Feishu has no native list endpoint.
|
|
34
|
+
- **Calendar write (5 tools)**: `create_calendar_event` / `update_calendar_event` / `delete_calendar_event` / `respond_calendar_event` / `get_freebusy`. UAT-first. Requires `calendar:calendar.event:write` scope. `start_time` / `end_time` are objects: `{timestamp:"<unix-seconds>", timezone?}` or `{date:"YYYY-MM-DD"}`. `delete` accepts `meeting_chat_id` to also dissolve the linked meeting chat. `respond` is the RSVP path.
|
|
35
|
+
- **Tasks v2 (7 tools, new domain)**: `list_tasks` / `get_task` / `create_task` / `update_task` / `complete_task` / `delete_task` / `manage_task_members`. UAT-first. Requires `task:task` scope. v2 uses `task_guid` instead of v1 numeric `task_id`. `update_task` requires explicit `update_fields=["summary","due","completed_at",...]` — Feishu only patches the listed fields. `complete_task(completed=true|false)` is a convenience wrapper.
|
|
36
|
+
- **MCP prompts (9)**: `/send` `/reply` `/digest` `/search` `/doc` `/table` `/wiki` `/drive` `/status`. Mirror the Claude Code skills via `prompts/list` + `prompts/get`, so Codex / Cursor / OpenClaw / Windsurf get the same guided UX. Reference bodies are read at server start from `skills/feishu-user-plugin/references/`.
|
|
37
|
+
- **Single-source credentials store**: `~/.feishu-user-plugin/credentials.json` (mode 0600, schema `docs/CREDENTIALS-FORMAT.md`). Multiple MCP processes (Claude Code + Codex sharing the file) see token rotations consistently — closes the "Codex still has the old UAT after a refresh in Claude Code" drift. Cookie heartbeat + UAT refresh persist back atomically. Opt-in: `npx feishu-user-plugin migrate` (dry-run) / `migrate --confirm` (writes). Env vars remain as backward-compat fallback. Server's `Auth:` startup line on stderr shows source (`credentials.json profile=default` vs `env vars (legacy)`).
|
|
38
|
+
- **Semi-automated regression**: `scripts/test-all-tools.js` walks every tool with representative payloads. `tests/baseline/` snapshots `tools-list.json` / `prompts-list.json` / `login-status-shape.json`; `npm run smoke` diffs against them, `npm run smoke:baseline` regenerates after intentional schema change. `docs/TESTING-METHODOLOGY.md` documents when to use unit / smoke / live MCP / `test-all-tools`.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- **C1.4 — `send_*_as_user` silently dropped messages with `oc_xxx` chat IDs**: cookie protobuf gateway's `PutMessageRequest.chatId` only recognizes numeric IDs; an `oc_xxx` was treated as unknown and the server returned an empty packet. Now auto-resolves `oc_xxx` via `getChatInfo(name) → cookie search(name) → numeric` and caches the mapping. Covers `send_as_user` / `send_image_as_user` / `send_file_as_user` / `send_post_as_user` / `send_card_as_user` / `batch_send`. Numeric IDs pass through unchanged. Resolution failure throws a clear error.
|
|
42
|
+
- **`list_wiki_nodes` returned 131006 in spaces the bot wasn't invited to**: `list_wiki_spaces` was already UAT-first, but `list_wiki_nodes` was bot-only. Made `list_wiki_nodes` UAT-first to match.
|
|
43
|
+
- **C1.15 — `get_user_info` showed current user as external tenant**: `getUserById` previously hit contact API first (requires `contact:user.base:readonly`); some OAuth configs returned no permission for same-tenant queries and the user was wrongly downgraded. Now UAT-first, contact API as fallback.
|
|
44
|
+
- **`manage_drive_file(action=delete)` printed `task=undefined`**: `DELETE /drive/v1/files/{token}` is synchronous and returns no `task_id`. Switched to `File deleted ({type})` when no task_id, `File deletion queued: task=...` when one is returned.
|
|
45
|
+
- **`send_image_as_user` failed silently**: cookie protobuf gateway rejects the simple `{imageKey}` content payload (HTTP 400) because Feishu Web actually encodes images with extra metadata (dimensions, MIME, thumbnails) that aren't in `proto/lark.proto`. Now throws a clear error pointing to `send_message_as_bot(msg_type="image", payload={image_key:"..."})` as the workaround. Wire format reverse-engineering deferred to v1.3.8 (needs Chrome DevTools traffic capture).
|
|
46
|
+
- Documented common error codes in tool schemas: 9499 (`manage_members` missing `member_id_type`, default `open_id`), 1062501 / 1061002 (`manage_drive_file` missing `type`).
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
- **Phase A refactor**: 7,500-line `src/index.js` split into `src/tools/<domain>.js` (handlers + schemas) and `src/clients/official/<domain>.js` (API methods). `src/server.js` orchestrates registration; `src/tools/_registry.js` provides shared `ctx` (factories, profile state, `resolveDocId`). See `docs/REFACTOR-NOTES.md` for the file-responsibility matrix.
|
|
50
|
+
- **Tool consolidation (82 → 80)**: 21 bitable tools collapsed into 5 `manage_bitable_*` dispatchers (app / table / field / view / record, each with `action=list|create|update|delete|...`). 3 doc-block tools → `manage_doc_block(action=create|update|delete)`. 3 drive ops → `manage_drive_file(action=copy|move|delete)`. 2 download tools → `download_message_resource(kind=image|file)` + `download_doc_image`. Semantics unchanged; parameters collapsed onto an `action` field.
|
|
51
|
+
- **Writes default to UAT**: every `create`/`edit` for docx / bitable / drive / wiki / OKR / calendar / tasks runs through `_asUserOrApp` — UAT first, bot only as fallback. Forced bot fallback appends a ⚠ warning to the response (and points to `npx feishu-user-plugin oauth`) so the ownership shift surfaces immediately.
|
|
52
|
+
- **ID input normalization**: docx / bitable tools' `document_id` / `app_token` accept native token (`doccnXXX` / `docxXXX` / `bascnXXX`), wiki node token (`wikcnXXX` / `wikmXXX` / `wiknXXX`), and full Feishu URLs. Internally resolved via `getWikiNode` with a 10-minute cache.
|
|
53
|
+
- **Upload scope inventory**: `uploadMedia` / `upload_drive_file` / `upload_bitable_attachment` / `manage_doc_block(image_path|file_path)` collectively need `drive:drive`, `drive:file:upload`, `docs:document.media:upload`, and `sheets:spreadsheet` (sheet uploads only). Documented in CLAUDE.md and the OAuth scope table.
|
|
54
|
+
- **team-skills sync via PR**: post-merge hook in this repo now opens an auto-merging PR against team-skills instead of pushing to main. CI `validate.yml` enforces a version triangle across `plugin.json` / `SKILL.md` / `README.md` first `### vX.Y.Z` heading.
|
|
55
|
+
|
|
56
|
+
## [1.3.6] - 2026-05-03
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- **Upload completeness**: `uploadDocMedia` → `uploadMedia` accepting 8 `parent_type`s (docx / sheet / bitable × image / file + legacy doc_*). New `create_doc_block` modes for files (`file_path` / `file_token`, block_type 23, auto view-wrap). `update_doc_block` accepts `file_token` to swap existing file blocks. New `upload_drive_file` (`drive/v1/files/upload_all`; optional `wiki_space_id` auto-attaches via `move_docs_to_wiki`). New `upload_bitable_attachment` (`parent_type=bitable_image|bitable_file`).
|
|
60
|
+
- **`batch_send` tool**: fan-out the same or different content to multiple targets in one call. Each target dispatches sequentially with anti-rate-limit throttling and reports per-target `ok` / `error`. Identity is the cookie user unless `target.via=bot`.
|
|
61
|
+
- **Multi-profile support**: `list_profiles` / `switch_profile` tools + `LARK_PROFILES_JSON` env. Hot-swap credentials without restarting the MCP server; cached client instances rebuild against the new profile.
|
|
62
|
+
- **`send_card_as_user` (bot-routed default)**: send Feishu interactive cards. v1.3.6 routes through the bot identity; the `as_user` suffix is reserved for v1.3.7's reverse-engineered cookie path. `via="user"` returns an explicit not-yet-implemented error.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
- OAuth scopes added: `drive:file:upload` (narrower scope for `drive/v1/files/upload_all`), `sheets:spreadsheet` (sheet image / file uploads). Existing users must re-run `npx feishu-user-plugin oauth` to pick them up.
|
|
66
|
+
|
|
67
|
+
## [1.3.5] - 2026-04-24
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- **Cross-process UAT refresh lock**: file lock at `~/.claude/feishu-uat-refresh.lock` (`O_CREAT|O_EXCL`, 30s stale detection) serializes UAT refresh across concurrent MCP processes. Inside the critical section, the lock holder re-reads `~/.claude.json` to see whether a peer already rotated the token; if so it adopts the fresh one. Closes the "Codex spawned 6 MCP servers, all raced to refresh" failure mode that was burning refresh tokens on 2026-04-23.
|
|
71
|
+
- **`get_login_status` UAT health check**: now actually exercises the UAT (calls `listChatsAsUser({pageSize:1})`) instead of just checking presence. Surfaces "configured but 401" cases that previously stayed silent until the next real tool call.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
- **Bot-fallback ⚠️ warning**: every write tool that silently fell back from UAT to bot identity (`create_doc` / `create_bitable` / `create_folder` / `create_doc_block` / etc.) now appends a `fallbackWarning` to the response so users see the ownership change immediately. Before, callers only learned days later when a teammate could read their "private" resource.
|
|
75
|
+
- **Auto-expand `merge_forward`**: `read_messages` / `read_p2p_messages` walk a `merge_forward` placeholder into its child messages by default (`expand_merge_forward=false` to opt out). Children carry `parentMessageId` (use that, NOT the child id, when downloading their media). Text children get `urls[]` + `feishuDocs[]` extracted so agents can feed them straight into `read_doc` / WebFetch.
|
|
76
|
+
- **`download_file` tool**: download a file attachment (`msg_type=file`). Returns base64 + mimeType + byte count; optional `save_path` writes to disk. Same parent-id rule for `merge_forward` children as `download_image`.
|
|
77
|
+
|
|
7
78
|
## [1.3.4] - 2026-04-22
|
|
8
79
|
|
|
9
80
|
### Added
|
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://nodejs.org)
|
|
5
5
|
[](https://modelcontextprotocol.io)
|
|
6
|
-
[](#tools)
|
|
7
7
|
[](CONTRIBUTING.md)
|
|
8
8
|
|
|
9
|
-
**All-in-one Feishu/Lark MCP Server --
|
|
9
|
+
**All-in-one Feishu/Lark MCP Server -- 82 tools, 9 skills, 3 auth layers for messaging, docs, bitable, calendar, tasks, drive, OKR, and more.**
|
|
10
10
|
|
|
11
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
|
|
|
@@ -21,6 +21,7 @@ The only MCP server that lets you send messages as your **personal identity** (n
|
|
|
21
21
|
- **Calendar & Tasks** -- Create events, check free/busy, manage tasks.
|
|
22
22
|
- **9 slash commands** for Claude Code -- `/send`, `/reply`, `/search`, `/digest`, `/doc`, `/table`, `/wiki`, `/drive`, `/status`
|
|
23
23
|
- **Auto session management** -- Cookie heartbeat every 4h, UAT auto-refresh with token rotation.
|
|
24
|
+
- **Real-time events** (v1.3.8) -- `get_new_events` drains an in-memory queue of incoming Feishu messages — react to replies / group activity within seconds without polling. WS connection auto-starts on boot.
|
|
24
25
|
- **Multi-platform** -- Claude Code, Cursor, Windsurf, VS Code, OpenClaw.
|
|
25
26
|
|
|
26
27
|
## Why This Exists
|
|
@@ -337,7 +338,7 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
337
338
|
}
|
|
338
339
|
```
|
|
339
340
|
|
|
340
|
-
## Tools (
|
|
341
|
+
## Tools (80 total)
|
|
341
342
|
|
|
342
343
|
### User Identity -- Messaging (10 tools, cookie auth)
|
|
343
344
|
|
|
@@ -349,8 +350,6 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
349
350
|
| `send_image_as_user` | Send image (requires `image_key` from `upload_image`) |
|
|
350
351
|
| `send_file_as_user` | Send file (requires `file_key` from `upload_file`) |
|
|
351
352
|
| `send_post_as_user` | Send rich text with title + formatted paragraphs |
|
|
352
|
-
| `send_sticker_as_user` | Send sticker/emoji |
|
|
353
|
-
| `send_audio_as_user` | Send audio message |
|
|
354
353
|
| `batch_send` | Fan-out send to multiple targets in one call (text / image / file / post). v1.3.6 |
|
|
355
354
|
| `send_card_as_user` | Send a Feishu interactive card. v1.3.6 default routes through bot identity; user-identity is reserved for v1.3.7. |
|
|
356
355
|
|
|
@@ -392,8 +391,8 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
392
391
|
| `add_members` | Add users to a group |
|
|
393
392
|
| `remove_members` | Remove users from a group |
|
|
394
393
|
| `upload_image` / `upload_file` | Upload image/file, returns key for sending |
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
394
|
+
| `download_message_resource` | v1.3.7 (C2.4): download a message-attached image or file. Args: `message_id`, `key`, `kind=image|file`, `save_path?`. **Required save_path when bytes > 2 MiB** (Anthropic 5 MB inline cap). Replaces v1.3.6 download_image (message mode) + download_file. |
|
|
395
|
+
| `download_doc_image` | v1.3.7 (C2.4): download an image embedded in a docx (image_token + optional doc_token). Same 2 MiB cap. Replaces v1.3.6 download_image (docx mode). |
|
|
397
396
|
|
|
398
397
|
### Wiki, OKR, and Calendar (v1.3.4)
|
|
399
398
|
|
|
@@ -409,7 +408,7 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
409
408
|
|
|
410
409
|
All docx / bitable tools' `document_id` / `app_token` parameter also accepts a Wiki node token or a full Feishu URL — the plugin resolves it transparently.
|
|
411
410
|
|
|
412
|
-
### Official API -- Documents (
|
|
411
|
+
### Official API -- Documents (5 tools)
|
|
413
412
|
|
|
414
413
|
| Tool | Description |
|
|
415
414
|
|------|-------------|
|
|
@@ -417,67 +416,86 @@ All docx / bitable tools' `document_id` / `app_token` parameter also accepts a W
|
|
|
417
416
|
| `read_doc` | Read raw text content |
|
|
418
417
|
| `get_doc_blocks` | Get structured block tree |
|
|
419
418
|
| `create_doc` | Create a new document |
|
|
420
|
-
| `
|
|
421
|
-
| `update_doc_block` | Update a specific block: generic `update_body`, `image_token`, or `file_token` (v1.3.6) |
|
|
422
|
-
| `delete_doc_blocks` | Delete a range of blocks |
|
|
419
|
+
| `manage_doc_block` | Insert / update / delete blocks (`action=create|update|delete`). Supports generic `children`, image (`image_path`/`image_token`), and file (`file_path`/`file_token`) shortcuts. v1.3.7 consolidates the v1.3.6 trio create_doc_block / update_doc_block / delete_doc_blocks. |
|
|
423
420
|
|
|
424
|
-
### Official API -- Bitable (
|
|
421
|
+
### Official API -- Bitable (6 tools, v1.3.7 consolidation)
|
|
425
422
|
|
|
426
|
-
| Tool | Description |
|
|
427
|
-
|
|
428
|
-
| `
|
|
429
|
-
| `
|
|
430
|
-
| `
|
|
431
|
-
| `
|
|
432
|
-
| `
|
|
433
|
-
| `
|
|
434
|
-
| `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` | Batch operations (max 500/call) |
|
|
435
|
-
| `upload_bitable_attachment` | Upload a file into a Bitable Attachment-type field. Returns `file_token` to write into the field as `[{file_token}]`. v1.3.6 |
|
|
423
|
+
| Tool | Actions | Description |
|
|
424
|
+
|------|---------|-------------|
|
|
425
|
+
| `manage_bitable_app` | create / copy / get_meta | App-level operations (v1.3.7 consolidates create_bitable / copy_bitable / get_bitable_meta) |
|
|
426
|
+
| `manage_bitable_table` | list / create / update / delete | Table CRUD (rename via update) |
|
|
427
|
+
| `manage_bitable_field` | list / create / update / delete | Field (column) management. `type` required for both create AND update. |
|
|
428
|
+
| `manage_bitable_view` | list / create / delete | Views (grid, kanban, gallery, form, gantt, calendar) |
|
|
429
|
+
| `manage_bitable_record` | search / get / create / update / delete | Record CRUD. create/update/delete accept arrays — single record or up to 500/call. |
|
|
430
|
+
| `upload_bitable_attachment` | — | Upload a file into a Bitable Attachment-type field. Returns `file_token` to write into the field as `[{file_token}]`. v1.3.6 |
|
|
436
431
|
|
|
437
|
-
### Official API -- Calendar (
|
|
432
|
+
### Official API -- Calendar (8 tools, write tools v1.3.7)
|
|
438
433
|
|
|
439
434
|
| Tool | Description |
|
|
440
435
|
|------|-------------|
|
|
441
436
|
| `list_calendars` | List accessible calendars |
|
|
442
|
-
| `create_calendar_event` | Create a calendar event |
|
|
443
437
|
| `list_calendar_events` | List events in a calendar |
|
|
444
|
-
| `
|
|
445
|
-
| `
|
|
438
|
+
| `get_calendar_event` | Full event details |
|
|
439
|
+
| `create_calendar_event` | Create an event (v1.3.7). Requires `calendar:calendar.event:write`. |
|
|
440
|
+
| `update_calendar_event` | Patch event fields (v1.3.7) |
|
|
441
|
+
| `delete_calendar_event` | Delete an event, optionally dissolve its meeting chat (v1.3.7) |
|
|
442
|
+
| `respond_calendar_event` | RSVP as accept / decline / tentative (v1.3.7) |
|
|
443
|
+
| `get_freebusy` | Freebusy lookup for `user_ids` in a time range (v1.3.7) |
|
|
446
444
|
|
|
447
|
-
### Official API -- Tasks (
|
|
445
|
+
### Official API -- Tasks v2 (7 tools, v1.3.7 new domain)
|
|
446
|
+
|
|
447
|
+
Identifier is `task_guid` (not v1's numeric `task_id`). Requires `task:task` scope.
|
|
448
448
|
|
|
449
449
|
| Tool | Description |
|
|
450
450
|
|------|-------------|
|
|
451
|
-
| `
|
|
452
|
-
| `get_task` |
|
|
453
|
-
| `
|
|
454
|
-
| `update_task` |
|
|
455
|
-
| `complete_task` | Mark
|
|
451
|
+
| `list_tasks` | List the current user's tasks (filter by completed / type) |
|
|
452
|
+
| `get_task` | Full task detail |
|
|
453
|
+
| `create_task` | Create a task (summary required; due/members optional) |
|
|
454
|
+
| `update_task` | Patch fields. **`update_fields` is required** — Feishu only updates the listed keys. |
|
|
455
|
+
| `complete_task` | Mark complete (or uncomplete with `completed=false`) |
|
|
456
|
+
| `delete_task` | Permanent delete |
|
|
457
|
+
| `manage_task_members` | `action=add|remove`, members `[{id, role:"assignee"|"follower"}]` |
|
|
456
458
|
|
|
457
|
-
### Official API -- Drive (
|
|
459
|
+
### Official API -- Drive (4 tools)
|
|
458
460
|
|
|
459
461
|
| Tool | Description |
|
|
460
462
|
|------|-------------|
|
|
461
463
|
| `list_files` | List files in a folder |
|
|
462
464
|
| `create_folder` | Create a new folder |
|
|
463
|
-
| `
|
|
464
|
-
| `move_file` | Move a file |
|
|
465
|
-
| `delete_file` | Delete a file/folder |
|
|
465
|
+
| `manage_drive_file` | Copy / move / delete a Drive file (`action=copy|move|delete`, `type` required). v1.3.7 consolidates v1.3.6 copy_file / move_file / delete_file. |
|
|
466
466
|
| `upload_drive_file` | Upload a local file into a Drive folder (`drive/v1/files/upload_all`). Optional `wiki_space_id` attaches the upload as a Wiki node atomically. v1.3.6 |
|
|
467
467
|
|
|
468
|
-
### Official API -- Wiki
|
|
468
|
+
### Official API -- Wiki (8 tools)
|
|
469
469
|
|
|
470
470
|
| Tool | Description |
|
|
471
471
|
|------|-------------|
|
|
472
|
-
| `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` | Wiki spaces, search, browse |
|
|
473
|
-
| `
|
|
472
|
+
| `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` / `get_wiki_node` | Wiki spaces, search, browse + resolve a wiki node to underlying obj_token |
|
|
473
|
+
| `create_wiki_node` | Create a new wiki node (doc/sheet/bitable/mindnote/file/docx/slides) inside a space |
|
|
474
|
+
| `update_wiki_node` | Rename a wiki node (title only — content edits via docx/bitable tools) |
|
|
475
|
+
| `move_wiki_node` | Move a wiki node to a different parent or different space |
|
|
476
|
+
| `copy_wiki_node` | Deep-copy a wiki node to a different location (optionally to a different space) |
|
|
474
477
|
|
|
475
|
-
### Plugin -- Profiles (
|
|
478
|
+
### Plugin -- Profiles (3 tools, v1.3.6 + v1.3.8)
|
|
476
479
|
|
|
477
480
|
| Tool | Description |
|
|
478
481
|
|------|-------------|
|
|
479
482
|
| `list_profiles` | List available identity profiles (default + extras from `LARK_PROFILES_JSON`) and the active one |
|
|
480
483
|
| `switch_profile` | Hot-swap active profile; cached client instances rebuild against new credentials |
|
|
484
|
+
| `manage_profile_hints` | Inspect/set/clear the resourceKey → profile cache used by the v1.3.8 auto-switch middleware |
|
|
485
|
+
|
|
486
|
+
### Multi-profile auto-switch (v1.3.8)
|
|
487
|
+
|
|
488
|
+
When `~/.feishu-user-plugin/credentials.json` has more than one profile, the plugin auto-switches between them on **read** paths when the active profile gets a permission-denied error. The winning profile is cached per resource so subsequent calls go straight to the right account.
|
|
489
|
+
|
|
490
|
+
**Whitelist** -- only `read_*`, `list_*`, `get_*`, `search_*`, `download_*` (and the read-action variants of `manage_bitable_*`) get auto-retry. Writes never auto-switch -- they fail loud so you don't accidentally create resources under the wrong account.
|
|
491
|
+
|
|
492
|
+
**Triggers** -- error codes 91403, 1254301, 1254000, 99991672, HTTP 403, plus message patterns `access_denied / permission_denied / docx_no_permission / no permission / forbidden`.
|
|
493
|
+
|
|
494
|
+
**Per-call override** -- pass `via_profile: "<name>"` in any tool call to pin to that profile (no auto-switch). Pass `via_profile: "auto"` to opt **into** auto-switch on a write call (escape hatch -- be careful).
|
|
495
|
+
|
|
496
|
+
**Cache management** -- `manage_profile_hints(action="list" | "set" | "clear", resource_key?, profile?)` lets you inspect or edit the cache.
|
|
497
|
+
|
|
498
|
+
Single-profile users (the vast majority): zero behaviour change -- the router short-circuits and `manage_profile_hints` is a no-op.
|
|
481
499
|
|
|
482
500
|
## Claude Code Slash Commands (9 skills)
|
|
483
501
|
|
|
@@ -537,7 +555,7 @@ feishu-user-plugin/
|
|
|
537
555
|
│ ├── SKILL.md # Main skill definition (trigger, tools, auth)
|
|
538
556
|
│ └── references/ # 8 skill reference docs + CLAUDE.md
|
|
539
557
|
├── src/
|
|
540
|
-
│ ├── index.js # MCP server entry point (
|
|
558
|
+
│ ├── index.js # MCP server entry point (78 tools)
|
|
541
559
|
│ ├── client.js # User identity client (Protobuf gateway)
|
|
542
560
|
│ ├── official.js # Official API client (REST, UAT)
|
|
543
561
|
│ ├── utils.js # ID generators, cookie parser
|
|
@@ -566,6 +584,19 @@ Issues and PRs welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for development s
|
|
|
566
584
|
|
|
567
585
|
If Feishu updates their protocol and something breaks, please [open an issue](https://github.com/EthanQC/feishu-user-plugin/issues/new?template=bug_report.md) with the error details.
|
|
568
586
|
|
|
587
|
+
### Automated sync hooks
|
|
588
|
+
|
|
589
|
+
This repo uses husky to enforce several invariants on every commit:
|
|
590
|
+
|
|
591
|
+
- **CLAUDE.md sync** — staging `CLAUDE.md` automatically regenerates `AGENTS.md` (identical body, different first line) and `skills/feishu-user-plugin/references/CLAUDE.md` (verbatim copy). Both are re-staged in the same commit.
|
|
592
|
+
- **Version triangle** — if `package.json`, `.claude-plugin/plugin.json`, or `skills/feishu-user-plugin/SKILL.md` are staged, all three `version` fields must agree or the commit is rejected.
|
|
593
|
+
- **Tool-count badge** — if `src/server.js` or any file under `src/tools/` is staged, the `N tools` badge in `README.md` must match the actual `TOOLS.length` exported by `src/server.js`.
|
|
594
|
+
- **Smoke test** — any change under `src/` triggers `npm run smoke` to catch schema regressions before commit.
|
|
595
|
+
|
|
596
|
+
CI (`.github/workflows/validate.yml`) runs the same checks on every PR to `main`, so bypassing the local hook still gets caught.
|
|
597
|
+
|
|
598
|
+
On the maintainer's machine, a post-merge hook (`scripts/sync-team-skills.sh`) auto-opens a sync PR in the `~/team-skills` repo after every merge to main. The hook silently skips if `~/team-skills` is absent.
|
|
599
|
+
|
|
569
600
|
## License
|
|
570
601
|
|
|
571
602
|
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "All-in-one Feishu plugin for Claude Code & Codex — messaging (with merge_forward expansion + batch_send), docs (image + file blocks read/write), bitable (
|
|
3
|
+
"version": "1.3.8",
|
|
4
|
+
"description": "All-in-one Feishu plugin for Claude Code & Codex — messaging (with merge_forward expansion + batch_send), docs (image + file blocks read/write), bitable, wiki (full CRUD), drive, OKR (with progress writes), calendar (read+write), Tasks v2, multi-profile auto-switch, real-time WS events. 82 tools + 9 prompts, 3 auth layers.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"feishu-user-plugin": "src/cli.js"
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
"test": "node src/test-all.js",
|
|
12
12
|
"test:quick": "node src/test-send.js",
|
|
13
13
|
"oauth": "node src/oauth.js",
|
|
14
|
-
"
|
|
14
|
+
"smoke": "node scripts/smoke.js diff",
|
|
15
|
+
"smoke:baseline": "node scripts/smoke.js write-baseline",
|
|
16
|
+
"test:tools": "node scripts/test-all-tools.js",
|
|
17
|
+
"prepublishOnly": "node scripts/check-version.js && node scripts/check-tool-count.js && node scripts/sync-server-json.js check && node scripts/check-docs-sync.js && node scripts/check-changelog.js && node scripts/confirm-version.js",
|
|
18
|
+
"prepare": "husky"
|
|
15
19
|
},
|
|
16
20
|
"keywords": [
|
|
17
21
|
"feishu",
|
|
@@ -55,5 +59,8 @@
|
|
|
55
59
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
56
60
|
"dotenv": "^16.4.7",
|
|
57
61
|
"protobufjs": "^7.4.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"husky": "^9.1.7"
|
|
58
65
|
}
|
|
59
66
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Companion script to docs/COOKIE-PROTOBUF-CAPTURES.md.
|
|
4
|
+
//
|
|
5
|
+
// Drives a single capture session: prints the recipe to follow, sets up
|
|
6
|
+
// the output dir, and after capture, decodes everything dropped into it.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node scripts/capture-feishu-protobuf.js IMAGE # prints the IMAGE recipe
|
|
10
|
+
// node scripts/capture-feishu-protobuf.js DECODE # decodes everything in /tmp/feishu-captures/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const CAPTURE_DIR = '/tmp/feishu-captures';
|
|
17
|
+
const TYPES = {
|
|
18
|
+
IMAGE: {
|
|
19
|
+
description: 'Image message via cookie protobuf — cmd=5 PutMessageRequest, content.type=5 (IMAGE)',
|
|
20
|
+
targetMessage: 'Content',
|
|
21
|
+
},
|
|
22
|
+
AUDIO: {
|
|
23
|
+
description: 'Audio message — cmd=5, content.type=7 (AUDIO)',
|
|
24
|
+
targetMessage: 'Content',
|
|
25
|
+
},
|
|
26
|
+
STICKER: {
|
|
27
|
+
description: 'Sticker — cmd=5, content.type=10 (STICKER)',
|
|
28
|
+
targetMessage: 'Content',
|
|
29
|
+
},
|
|
30
|
+
CARD: {
|
|
31
|
+
description: 'Interactive card — cmd=5, content.type=14 (CARD)',
|
|
32
|
+
targetMessage: 'Content',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const cmd = process.argv[2] || 'help';
|
|
37
|
+
|
|
38
|
+
if (cmd === 'DECODE') {
|
|
39
|
+
if (!fs.existsSync(CAPTURE_DIR)) { console.error('No captures yet — run a TYPE first.'); process.exit(1); }
|
|
40
|
+
const files = fs.readdirSync(CAPTURE_DIR).filter(f => f.endsWith('.bin') || f.endsWith('.b64'));
|
|
41
|
+
if (!files.length) { console.error('No capture files in ' + CAPTURE_DIR); process.exit(1); }
|
|
42
|
+
for (const f of files) {
|
|
43
|
+
const type = path.basename(f).split('-')[0].toUpperCase();
|
|
44
|
+
const meta = TYPES[type] || { targetMessage: 'Packet' };
|
|
45
|
+
console.log(`\n=== ${f} (decoding as ${meta.targetMessage}) ===`);
|
|
46
|
+
const fullPath = path.join(CAPTURE_DIR, f);
|
|
47
|
+
const decodeScript = path.join(__dirname, 'decode-feishu-protobuf.js');
|
|
48
|
+
try {
|
|
49
|
+
if (f.endsWith('.b64')) {
|
|
50
|
+
const b64 = fs.readFileSync(fullPath, 'utf8').trim();
|
|
51
|
+
execSync(`node ${decodeScript} ${meta.targetMessage} --b64 ${JSON.stringify(b64)}`, { stdio: 'inherit' });
|
|
52
|
+
} else {
|
|
53
|
+
execSync(`node ${decodeScript} ${meta.targetMessage} < ${fullPath}`, { stdio: 'inherit' });
|
|
54
|
+
}
|
|
55
|
+
} catch (e) { console.error(` decode failed: ${e.message}`); }
|
|
56
|
+
}
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!TYPES[cmd]) {
|
|
61
|
+
console.log('Usage: node scripts/capture-feishu-protobuf.js [IMAGE|AUDIO|STICKER|CARD|DECODE]');
|
|
62
|
+
console.log('\nCapture types:');
|
|
63
|
+
for (const [k, v] of Object.entries(TYPES)) console.log(` ${k} — ${v.description}`);
|
|
64
|
+
console.log('\nRecipe (IMAGE example):');
|
|
65
|
+
console.log(' 1. The agent uses Playwright MCP to:');
|
|
66
|
+
console.log(' a. Open https://www.feishu.cn/messenger/ with LARK_COOKIE');
|
|
67
|
+
console.log(' b. Click "我自己" / self-chat');
|
|
68
|
+
console.log(' c. Drag-drop a small test PNG OR click the image button + select file');
|
|
69
|
+
console.log(' d. Wait for the upload to complete');
|
|
70
|
+
console.log(' e. Click "send" and watch network for the POST to /im/gateway/');
|
|
71
|
+
console.log(` 2. Save the raw POST body to ${CAPTURE_DIR}/image-1.bin`);
|
|
72
|
+
console.log(` 3. Run: node scripts/capture-feishu-protobuf.js DECODE`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fs.mkdirSync(CAPTURE_DIR, { recursive: true });
|
|
77
|
+
console.log(`=== ${cmd} capture session ===`);
|
|
78
|
+
console.log(TYPES[cmd].description);
|
|
79
|
+
console.log(`\nCapture dir: ${CAPTURE_DIR}`);
|
|
80
|
+
console.log('\nRecipe:');
|
|
81
|
+
console.log(' 1. Use Playwright MCP to open feishu.cn/messenger/ with cookie auth');
|
|
82
|
+
console.log(' 2. Send the message of type ' + cmd + ' to "我自己" via the web UI');
|
|
83
|
+
console.log(' 3. Capture POST /im/gateway/ request body via fetch monkey-patch');
|
|
84
|
+
console.log(` 4. Drop the raw body to ${CAPTURE_DIR}/${cmd.toLowerCase()}-1.bin (or .b64)`);
|
|
85
|
+
console.log(` 5. Run: node scripts/capture-feishu-protobuf.js DECODE`);
|
|
86
|
+
console.log('\nSee docs/COOKIE-PROTOBUF-CAPTURES.md for full step-by-step.');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Verifies CHANGELOG.md has an "## [vX.Y.Z]" section matching package.json
|
|
4
|
+
// version. Run from publish workflow + locally before tagging.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node scripts/check-changelog.js → checks current package.json version
|
|
8
|
+
// node scripts/check-changelog.js 1.3.8 → checks given version explicitly
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const explicit = process.argv[2];
|
|
14
|
+
const pkgVersion = explicit || require(path.join(__dirname, '..', 'package.json')).version;
|
|
15
|
+
|
|
16
|
+
const cl = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md'), 'utf8');
|
|
17
|
+
|
|
18
|
+
// Common section headings used in this repo: "## [v1.3.7]" or "## v1.3.7" or "## 1.3.7" or "### v1.3.7".
|
|
19
|
+
const patterns = [
|
|
20
|
+
new RegExp(`^##\\s*\\[?v?${pkgVersion.replace(/\./g, '\\.')}\\]?`, 'm'),
|
|
21
|
+
new RegExp(`^###\\s*v?${pkgVersion.replace(/\./g, '\\.')}`, 'm'),
|
|
22
|
+
];
|
|
23
|
+
const match = patterns.some(re => re.test(cl));
|
|
24
|
+
|
|
25
|
+
if (!match) {
|
|
26
|
+
console.error(`ERROR: CHANGELOG.md has no section for v${pkgVersion}.`);
|
|
27
|
+
console.error(`Add "## [v${pkgVersion}]" or "### v${pkgVersion}" with the release notes before tagging.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`OK: CHANGELOG.md has v${pkgVersion} section`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Verifies CLAUDE.md is in sync with AGENTS.md (Codex) and
|
|
4
|
+
// skills/feishu-user-plugin/references/CLAUDE.md (skill reference copy).
|
|
5
|
+
//
|
|
6
|
+
// Pre-commit hook (scripts/sync-claude-md.sh) already auto-regenerates these
|
|
7
|
+
// from CLAUDE.md, but this script gives prepublishOnly + CI a hard gate.
|
|
8
|
+
//
|
|
9
|
+
// Match logic mirrors validate.yml's diff steps:
|
|
10
|
+
// AGENTS.md = "# feishu-user-plugin — Codex Instructions\n" + tail -n +2 CLAUDE.md
|
|
11
|
+
// skills/.../CLAUDE.md = identical to CLAUDE.md
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const ROOT = path.join(__dirname, '..');
|
|
17
|
+
const claude = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8');
|
|
18
|
+
|
|
19
|
+
// AGENTS.md: header replaced with "# feishu-user-plugin — Codex Instructions"
|
|
20
|
+
const claudeBody = claude.split('\n').slice(1).join('\n'); // drop first line
|
|
21
|
+
const expectedAgents = '# feishu-user-plugin — Codex Instructions\n' + claudeBody;
|
|
22
|
+
const actualAgents = fs.readFileSync(path.join(ROOT, 'AGENTS.md'), 'utf8');
|
|
23
|
+
|
|
24
|
+
const failures = [];
|
|
25
|
+
if (actualAgents !== expectedAgents) {
|
|
26
|
+
failures.push('AGENTS.md is out of sync with CLAUDE.md');
|
|
27
|
+
failures.push('Fix: bash scripts/sync-claude-md.sh (or edit CLAUDE.md and re-stage)');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const skillRef = path.join(ROOT, 'skills', 'feishu-user-plugin', 'references', 'CLAUDE.md');
|
|
31
|
+
const actualSkillRef = fs.readFileSync(skillRef, 'utf8');
|
|
32
|
+
if (actualSkillRef !== claude) {
|
|
33
|
+
failures.push('skills/feishu-user-plugin/references/CLAUDE.md is out of sync with CLAUDE.md');
|
|
34
|
+
failures.push('Fix: bash scripts/sync-claude-md.sh (or edit CLAUDE.md and re-stage)');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (failures.length) {
|
|
38
|
+
for (const f of failures) console.error(f);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
console.log('OK: CLAUDE.md / AGENTS.md / skill reference all in sync');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { TOOLS } = require(path.join(__dirname, '..', 'src', 'server'));
|
|
6
|
+
|
|
7
|
+
const failures = [];
|
|
8
|
+
|
|
9
|
+
// Source 1: README.md "N tools" badge
|
|
10
|
+
const readme = fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf8');
|
|
11
|
+
const readmeMatch = readme.match(/(\d+)\s+tools/);
|
|
12
|
+
if (!readmeMatch) {
|
|
13
|
+
failures.push('No "N tools" badge in README.md');
|
|
14
|
+
} else if (parseInt(readmeMatch[1], 10) !== TOOLS.length) {
|
|
15
|
+
failures.push(`README.md claims ${readmeMatch[1]} tools, src/server.js has ${TOOLS.length}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Source 2: SKILL.md `allowed-tools` frontmatter — comma-separated list.
|
|
19
|
+
const skillMd = fs.readFileSync(path.join(__dirname, '..', 'skills', 'feishu-user-plugin', 'SKILL.md'), 'utf8');
|
|
20
|
+
const skillMatch = skillMd.match(/^allowed-tools:\s*(.+)$/m);
|
|
21
|
+
if (!skillMatch) {
|
|
22
|
+
failures.push('No `allowed-tools:` line in skills/feishu-user-plugin/SKILL.md frontmatter');
|
|
23
|
+
} else {
|
|
24
|
+
const skillTools = skillMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
25
|
+
const skillSet = new Set(skillTools);
|
|
26
|
+
const toolSet = new Set(TOOLS.map(t => t.name));
|
|
27
|
+
const missingFromSkill = [...toolSet].filter(t => !skillSet.has(t)).sort();
|
|
28
|
+
const extraInSkill = [...skillSet].filter(t => !toolSet.has(t)).sort();
|
|
29
|
+
if (missingFromSkill.length || extraInSkill.length) {
|
|
30
|
+
failures.push(`SKILL.md allowed-tools out of sync (server has ${TOOLS.length}, SKILL.md has ${skillTools.length}):`);
|
|
31
|
+
if (missingFromSkill.length) failures.push(` missing from SKILL.md: ${missingFromSkill.join(', ')}`);
|
|
32
|
+
if (extraInSkill.length) failures.push(` extra in SKILL.md (not registered): ${extraInSkill.join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (failures.length) {
|
|
37
|
+
for (const f of failures) console.error(f);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
console.log(`OK: ${TOOLS.length} tools (README badge + SKILL.md allowed-tools both match)`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
// Source 1: package.json
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
10
|
+
const pkgVersion = pkg.version;
|
|
11
|
+
|
|
12
|
+
// Source 2: .claude-plugin/plugin.json
|
|
13
|
+
const plugin = JSON.parse(fs.readFileSync(path.join(ROOT, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
14
|
+
const pluginVersion = plugin.version;
|
|
15
|
+
|
|
16
|
+
// Source 3: skills/feishu-user-plugin/SKILL.md frontmatter version line
|
|
17
|
+
const skillMd = fs.readFileSync(path.join(ROOT, 'skills', 'feishu-user-plugin', 'SKILL.md'), 'utf8');
|
|
18
|
+
const skillMatch = skillMd.match(/^version:\s*["']?([^"'\s]+)["']?/m);
|
|
19
|
+
if (!skillMatch) {
|
|
20
|
+
console.error('ERROR: Could not find version in skills/feishu-user-plugin/SKILL.md frontmatter');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const skillVersion = skillMatch[1];
|
|
24
|
+
|
|
25
|
+
const sources = [
|
|
26
|
+
{ label: 'package.json', version: pkgVersion, path: 'package.json' },
|
|
27
|
+
{ label: '.claude-plugin/plugin.json', version: pluginVersion, path: '.claude-plugin/plugin.json' },
|
|
28
|
+
{ label: 'skills/feishu-user-plugin/SKILL.md', version: skillVersion, path: 'skills/feishu-user-plugin/SKILL.md' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const versions = sources.map((s) => s.version);
|
|
32
|
+
const allEqual = versions.every((v) => v === versions[0]);
|
|
33
|
+
|
|
34
|
+
if (!allEqual) {
|
|
35
|
+
console.error('ERROR: Version triangle mismatch!');
|
|
36
|
+
sources.forEach((s) => console.error(` ${s.path}: ${s.version}`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`OK: version ${pkgVersion}`);
|