feishu-doc-cli 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 m1heng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # feishu-doc-cli
2
+
3
+ CLI tool to read [Feishu Open Platform](https://open.feishu.cn) documentation as Markdown.
4
+
5
+ Feishu developer docs live behind a SPA that AI coding agents cannot read directly. This tool exposes the docs through a simple CLI, outputting raw Markdown that agents (and humans) can consume.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g feishu-doc-cli
11
+ ```
12
+
13
+ Requires Node.js >= 18.
14
+
15
+ ## Usage
16
+
17
+ ### Read a document
18
+
19
+ ```bash
20
+ feishu-doc read /home/intro
21
+ ```
22
+
23
+ Output:
24
+
25
+ ```markdown
26
+ # 开放平台概述
27
+
28
+ > Path: /home/intro
29
+ > Updated: 2025-11-28
30
+
31
+ ---
32
+
33
+ (original Markdown content)
34
+ ```
35
+
36
+ ### Browse the document tree
37
+
38
+ ```bash
39
+ feishu-doc tree --depth 2
40
+ ```
41
+
42
+ ```
43
+ ├── 📁 文档首页
44
+ │ └── 📄 首页 → /home/index
45
+ ├── 📁 开发指南
46
+ │ ├── 📁 平台简介
47
+ │ ├── 📁 开发流程
48
+ │ ...
49
+ ```
50
+
51
+ Filter to a subtree:
52
+
53
+ ```bash
54
+ feishu-doc tree "/uAjLw4CM/uYjL24iN/platform-overveiw" --depth 3
55
+ ```
56
+
57
+ ### Search documents by title
58
+
59
+ ```bash
60
+ feishu-doc search "消息"
61
+ ```
62
+
63
+ ```
64
+ Found 109 document(s):
65
+
66
+ 发送消息
67
+ feishu-doc read "/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create"
68
+ ```
69
+
70
+ ### Options
71
+
72
+ | Option | Description | Default |
73
+ |--------|-------------|---------|
74
+ | `--lang zh\|en` | Language | `zh` |
75
+ | `--depth <n>` | Tree display depth | unlimited |
76
+
77
+ ## Following links
78
+
79
+ Documents contain links in various formats. **All of them work directly with `feishu-doc read`** — just copy the `href` value from any link in the content:
80
+
81
+ ```bash
82
+ # /ssl:ttdoc/ links (most common in content)
83
+ feishu-doc read "/ssl:ttdoc/ukTMukTMukTM/uMTNz4yM1MjLzUzM"
84
+
85
+ # Full Feishu URLs
86
+ feishu-doc read "https://open.feishu.cn/document/client-docs/intro"
87
+
88
+ # Full Lark URLs
89
+ feishu-doc read "https://open.larkoffice.com/document/client-docs/bot-v3/bot-overview"
90
+
91
+ # /document/ paths
92
+ feishu-doc read "/document/client-docs/intro"
93
+
94
+ # API fullPath (as shown in tree output)
95
+ feishu-doc read "/home/intro"
96
+ ```
97
+
98
+ The CLI normalizes all formats internally. No need to manually convert.
99
+
100
+ ## How it works
101
+
102
+ Feishu Open Platform exposes two public APIs (no authentication required):
103
+
104
+ | API | Purpose |
105
+ |-----|---------|
106
+ | `GET /api/tools/docment/directory_list` | Full document tree (~925 KB JSON) |
107
+ | `GET /document_portal/v1/document/get_detail?fullPath=<path>` | Document content in Markdown |
108
+
109
+ This CLI is a thin wrapper around these two endpoints.
110
+
111
+ ## License
112
+
113
+ MIT
package/dist/api.js ADDED
@@ -0,0 +1,67 @@
1
+ const BASE = "https://open.feishu.cn";
2
+ /**
3
+ * Normalize any link format found in Feishu docs to an API-usable fullPath.
4
+ *
5
+ * Accepted inputs:
6
+ * /ssl:ttdoc/xxx → /xxx
7
+ * https://open.feishu.cn/document/xxx → /xxx
8
+ * https://open.larkoffice.com/document/xxx → /xxx
9
+ * /document/xxx → /xxx
10
+ * /xxx → /xxx (passthrough)
11
+ */
12
+ export function normalizePath(input) {
13
+ let p = input.trim();
14
+ // Strip URL with anchor — keep only the path part
15
+ // e.g. https://open.feishu.cn/document/xxx#anchor → /xxx
16
+ for (const domain of [
17
+ "https://open.feishu.cn",
18
+ "https://open.larkoffice.com",
19
+ ]) {
20
+ if (p.startsWith(domain)) {
21
+ try {
22
+ p = new URL(p).pathname;
23
+ }
24
+ catch {
25
+ // Malformed URL — strip the domain prefix as fallback
26
+ p = p.slice(domain.length);
27
+ }
28
+ break;
29
+ }
30
+ }
31
+ // /ssl:ttdoc/xxx → /xxx
32
+ if (p.startsWith("/ssl:ttdoc/")) {
33
+ p = p.slice("/ssl:ttdoc".length);
34
+ }
35
+ // /document/xxx → /xxx
36
+ if (p.startsWith("/document/")) {
37
+ p = p.slice("/document".length);
38
+ }
39
+ // Ensure leading slash
40
+ if (!p.startsWith("/")) {
41
+ p = "/" + p;
42
+ }
43
+ return p;
44
+ }
45
+ function headers(lang) {
46
+ const locale = lang === "en" ? "en-US" : "zh-CN";
47
+ return { Cookie: `open_locale=${locale}` };
48
+ }
49
+ export async function fetchTree(lang) {
50
+ const res = await fetch(`${BASE}/api/tools/docment/directory_list`, {
51
+ headers: headers(lang),
52
+ });
53
+ const json = (await res.json());
54
+ if (json.code !== 0 || !json.data)
55
+ throw new Error(`API error: code ${json.code}`);
56
+ return json.data.items;
57
+ }
58
+ export async function fetchDoc(path, lang) {
59
+ const fullPath = normalizePath(path);
60
+ const url = `${BASE}/document_portal/v1/document/get_detail?fullPath=${encodeURIComponent(fullPath)}`;
61
+ const res = await fetch(url, { headers: headers(lang) });
62
+ const json = (await res.json());
63
+ if (json.code !== 0 || !json.data) {
64
+ throw new Error(`API error: ${json.msg || `code ${json.code}`} (fullPath: ${fullPath})`);
65
+ }
66
+ return json.data;
67
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ import { fetchTree, fetchDoc, normalizePath } from "./api.js";
3
+ import { renderTree, searchTree, filterSubtree } from "./tree.js";
4
+ function usage() {
5
+ console.log(`feishu-doc - Read Feishu Open Platform docs as Markdown
6
+
7
+ Usage:
8
+ feishu-doc read <path> Read a document
9
+ feishu-doc tree [path] Show document tree
10
+ feishu-doc search <keyword> Search document titles
11
+
12
+ Options:
13
+ --lang zh|en Language (default: zh)
14
+ --depth <n> Tree display depth limit
15
+
16
+ Path formats (all accepted by 'read'):
17
+ /home/intro
18
+ /ssl:ttdoc/ukTMukTMukTM/uMTNz4yM1MjLzUzM
19
+ /document/client-docs/intro
20
+ https://open.feishu.cn/document/client-docs/intro
21
+ https://open.larkoffice.com/document/client-docs/bot-v3/bot-overview
22
+
23
+ Tip: Any link href found in document content can be passed directly to 'read'.`);
24
+ process.exit(0);
25
+ }
26
+ function parseArgs(argv) {
27
+ const args = [];
28
+ let lang = "zh";
29
+ let depth = Infinity;
30
+ for (let i = 0; i < argv.length; i++) {
31
+ if (argv[i] === "--lang" && argv[i + 1]) {
32
+ lang = argv[++i];
33
+ }
34
+ else if (argv[i] === "--depth" && argv[i + 1]) {
35
+ const n = parseInt(argv[++i], 10);
36
+ if (Number.isFinite(n) && n > 0)
37
+ depth = n;
38
+ }
39
+ else if (argv[i] === "--help" || argv[i] === "-h") {
40
+ usage();
41
+ }
42
+ else {
43
+ args.push(argv[i]);
44
+ }
45
+ }
46
+ return { args, lang, depth };
47
+ }
48
+ async function cmdRead(path, lang) {
49
+ const doc = await fetchDoc(path, lang);
50
+ const date = new Date(doc.updateTime).toISOString().slice(0, 10);
51
+ console.log(`# ${doc.name}\n`);
52
+ console.log(`> Path: ${doc.fullPath}`);
53
+ console.log(`> Updated: ${date}\n`);
54
+ console.log("---\n");
55
+ console.log(doc.content);
56
+ }
57
+ async function cmdTree(lang, depth, path) {
58
+ const tree = await fetchTree(lang);
59
+ const nodes = path ? filterSubtree(tree, normalizePath(path)) : tree;
60
+ if (!nodes) {
61
+ console.error(`Path not found in tree: ${path}`);
62
+ process.exit(1);
63
+ return;
64
+ }
65
+ console.log(renderTree(nodes, depth));
66
+ }
67
+ async function cmdSearch(keyword, lang) {
68
+ const tree = await fetchTree(lang);
69
+ const results = searchTree(tree, keyword);
70
+ if (results.length === 0) {
71
+ console.log(`No documents found matching "${keyword}"`);
72
+ return;
73
+ }
74
+ console.log(`Found ${results.length} document(s):\n`);
75
+ for (const r of results) {
76
+ console.log(` ${r.name}`);
77
+ console.log(` feishu-doc read "${r.fullPath}"\n`);
78
+ }
79
+ }
80
+ async function main() {
81
+ const { args, lang, depth } = parseArgs(process.argv.slice(2));
82
+ const command = args[0];
83
+ if (!command)
84
+ usage();
85
+ switch (command) {
86
+ case "read": {
87
+ const path = args[1];
88
+ if (!path) {
89
+ console.error("Error: missing <path>\nUsage: feishu-doc read <path>");
90
+ process.exit(1);
91
+ }
92
+ await cmdRead(path, lang);
93
+ break;
94
+ }
95
+ case "tree": {
96
+ await cmdTree(lang, depth, args[1]);
97
+ break;
98
+ }
99
+ case "search": {
100
+ const keyword = args[1];
101
+ if (!keyword) {
102
+ console.error("Error: missing <keyword>\nUsage: feishu-doc search <keyword>");
103
+ process.exit(1);
104
+ }
105
+ await cmdSearch(keyword, lang);
106
+ break;
107
+ }
108
+ default:
109
+ console.error(`Unknown command: ${command}`);
110
+ usage();
111
+ }
112
+ }
113
+ main().catch((err) => {
114
+ console.error(`Error: ${err.message}`);
115
+ process.exit(1);
116
+ });
package/dist/tree.js ADDED
@@ -0,0 +1,49 @@
1
+ export function renderTree(nodes, maxDepth = Infinity, depth = 0, prefix = "") {
2
+ if (depth >= maxDepth)
3
+ return "";
4
+ const lines = [];
5
+ for (let i = 0; i < nodes.length; i++) {
6
+ const node = nodes[i];
7
+ const isLast = i === nodes.length - 1;
8
+ const connector = isLast ? "└── " : "├── ";
9
+ const isDir = node.type === "DirectoryType";
10
+ const icon = isDir ? "📁" : "📄";
11
+ const pathHint = isDir ? "" : ` → ${node.fullPath}`;
12
+ lines.push(`${prefix}${connector}${icon} ${node.name}${pathHint}`);
13
+ if (node.items.length > 0) {
14
+ const childPrefix = prefix + (isLast ? " " : "│ ");
15
+ const sub = renderTree(node.items, maxDepth, depth + 1, childPrefix);
16
+ if (sub)
17
+ lines.push(sub);
18
+ }
19
+ }
20
+ return lines.join("\n");
21
+ }
22
+ export function searchTree(nodes, keyword) {
23
+ const results = [];
24
+ const kw = keyword.toLowerCase();
25
+ function walk(items) {
26
+ for (const node of items) {
27
+ if (node.type === "DocumentType" &&
28
+ node.name.toLowerCase().includes(kw)) {
29
+ results.push({ name: node.name, fullPath: node.fullPath });
30
+ }
31
+ if (node.items.length > 0)
32
+ walk(node.items);
33
+ }
34
+ }
35
+ walk(nodes);
36
+ return results;
37
+ }
38
+ export function filterSubtree(nodes, path) {
39
+ for (const node of nodes) {
40
+ if (node.fullPath === path)
41
+ return [node];
42
+ if (node.items.length > 0) {
43
+ const found = filterSubtree(node.items, path);
44
+ if (found)
45
+ return found;
46
+ }
47
+ }
48
+ return null;
49
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "feishu-doc-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to read Feishu Open Platform documentation as Markdown, designed for AI coding agents",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "m1heng",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/m1heng/feishu-doc-cli"
11
+ },
12
+ "homepage": "https://github.com/m1heng/feishu-doc-cli#readme",
13
+ "keywords": [
14
+ "feishu",
15
+ "lark",
16
+ "documentation",
17
+ "cli",
18
+ "markdown",
19
+ "ai-agent",
20
+ "developer-tools"
21
+ ],
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "bin": {
26
+ "feishu-doc": "dist/cli.js"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "prepublishOnly": "tsc"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.3.0",
34
+ "typescript": "^5.4.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ }
39
+ }