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 +21 -0
- package/README.md +113 -0
- package/dist/api.js +67 -0
- package/dist/cli.js +116 -0
- package/dist/tree.js +49 -0
- package/package.json +39 -0
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
|
+
}
|