@jackwener/opencli 0.7.2 → 0.7.4
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/README.md +32 -26
- package/README.zh-CN.md +1 -0
- package/SKILL.md +10 -4
- package/dist/cli-manifest.json +195 -22
- package/dist/clis/linkedin/search.d.ts +1 -0
- package/dist/clis/linkedin/search.js +366 -0
- package/dist/clis/reddit/read.d.ts +1 -0
- package/dist/clis/reddit/read.js +184 -0
- package/dist/clis/youtube/transcript-group.d.ts +44 -0
- package/dist/clis/youtube/transcript-group.js +226 -0
- package/dist/clis/youtube/transcript-group.test.d.ts +1 -0
- package/dist/clis/youtube/transcript-group.test.js +99 -0
- package/dist/clis/youtube/transcript.d.ts +1 -0
- package/dist/clis/youtube/transcript.js +264 -0
- package/dist/clis/youtube/utils.d.ts +8 -0
- package/dist/clis/youtube/utils.js +28 -0
- package/dist/clis/youtube/video.d.ts +1 -0
- package/dist/clis/youtube/video.js +114 -0
- package/dist/doctor.d.ts +29 -2
- package/dist/doctor.js +122 -55
- package/dist/doctor.test.js +42 -1
- package/dist/main.js +2 -1
- package/package.json +1 -1
- package/src/clis/linkedin/search.ts +416 -0
- package/src/clis/reddit/read.ts +186 -0
- package/src/clis/youtube/transcript-group.test.ts +108 -0
- package/src/clis/youtube/transcript-group.ts +287 -0
- package/src/clis/youtube/transcript.ts +280 -0
- package/src/clis/youtube/utils.ts +28 -0
- package/src/clis/youtube/video.ts +116 -0
- package/src/doctor.test.ts +46 -1
- package/src/doctor.ts +149 -53
- package/src/main.ts +2 -1
- package/dist/clis/reddit/read.yaml +0 -76
- package/src/clis/reddit/read.yaml +0 -76
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# OpenCLI
|
|
2
2
|
|
|
3
3
|
> **Make any website your CLI.**
|
|
4
|
-
> Zero risk · Reuse Chrome login · AI-powered discovery
|
|
4
|
+
> Zero risk · Reuse Chrome login · AI-powered discovery · 80+ commands · 19 sites
|
|
5
5
|
|
|
6
6
|
[中文文档](./README.zh-CN.md)
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
|
-
A CLI tool that turns **any website** into a command-line interface —
|
|
12
|
+
A CLI tool that turns **any website** into a command-line interface — Bilibili, Zhihu, 小红书, Twitter/X, Reddit, YouTube, and [many more](#built-in-commands) — powered by browser session reuse and AI-native discovery.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -32,8 +32,9 @@ A CLI tool that turns **any website** into a command-line interface — bilibili
|
|
|
32
32
|
|
|
33
33
|
- **Account-safe** — Reuses Chrome's logged-in state; your credentials never leave the browser.
|
|
34
34
|
- **AI Agent ready** — `explore` discovers APIs, `synthesize` generates adapters, `cascade` finds auth strategies.
|
|
35
|
+
- **Self-healing setup** — `opencli setup` auto-discovers tokens; `opencli doctor` diagnoses config across 10+ tools; `--fix` repairs them all.
|
|
35
36
|
- **Dynamic Loader** — Simply drop `.ts` or `.yaml` adapters into the `clis/` folder for auto-registration.
|
|
36
|
-
- **Dual-Engine Architecture** — Supports both YAML declarative data pipelines and robust browser runtime
|
|
37
|
+
- **Dual-Engine Architecture** — Supports both YAML declarative data pipelines and robust browser runtime TypeScript injections.
|
|
37
38
|
|
|
38
39
|
## Prerequisites
|
|
39
40
|
|
|
@@ -85,10 +86,12 @@ export PLAYWRIGHT_MCP_EXTENSION_TOKEN="<your-token-here>"
|
|
|
85
86
|
|
|
86
87
|
</details>
|
|
87
88
|
|
|
88
|
-
Verify with `opencli doctor` — shows colored status for all config locations:
|
|
89
|
+
Verify with `opencli doctor` — shows colored status for extension install, token consistency, and all config locations:
|
|
89
90
|
|
|
90
91
|
```bash
|
|
91
|
-
opencli doctor
|
|
92
|
+
opencli doctor # Token & config diagnosis
|
|
93
|
+
opencli doctor --live # Also test live browser connectivity
|
|
94
|
+
opencli doctor --fix -y # Auto-fix all mismatched configs
|
|
92
95
|
```
|
|
93
96
|
|
|
94
97
|
## Quick Start
|
|
@@ -130,26 +133,29 @@ npm install -g @jackwener/opencli@latest
|
|
|
130
133
|
|
|
131
134
|
## Built-in Commands
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
|
136
|
-
|
|
137
|
-
| **
|
|
138
|
-
| **
|
|
139
|
-
| **
|
|
140
|
-
| **
|
|
141
|
-
| **
|
|
142
|
-
| **
|
|
143
|
-
| **
|
|
144
|
-
| **
|
|
145
|
-
| **
|
|
146
|
-
| **
|
|
147
|
-
| **
|
|
148
|
-
| **ctrip** | `search` | 🔐 Browser |
|
|
149
|
-
| **github** | `search` | 🌐 Public |
|
|
150
|
-
| **
|
|
151
|
-
| **
|
|
152
|
-
| **
|
|
136
|
+
**19 sites · 80+ commands** — run `opencli list` for the live registry.
|
|
137
|
+
|
|
138
|
+
| Site | Commands | Count | Mode |
|
|
139
|
+
|------|----------|:-----:|------|
|
|
140
|
+
| **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` | 18 | 🔐 Browser |
|
|
141
|
+
| **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | 15 | 🔐 Browser |
|
|
142
|
+
| **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` | 11 | 🔐 Browser |
|
|
143
|
+
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
|
|
144
|
+
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 Browser |
|
|
145
|
+
| **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 5 | 🔐 Browser |
|
|
146
|
+
| **youtube** | `search` `video` `transcript` | 3 | 🔐 Browser |
|
|
147
|
+
| **zhihu** | `hot` `search` `question` | 3 | 🔐 Browser |
|
|
148
|
+
| **boss** | `search` `detail` | 2 | 🔐 Browser |
|
|
149
|
+
| **coupang** | `search` `add-to-cart` | 2 | 🔐 Browser |
|
|
150
|
+
| **bbc** | `news` | 1 | 🌐 Public |
|
|
151
|
+
| **ctrip** | `search` | 1 | 🔐 Browser |
|
|
152
|
+
| **github** | `search` | 1 | 🌐 Public |
|
|
153
|
+
| **hackernews** | `top` | 1 | 🌐 Public |
|
|
154
|
+
| **linkedin** | `search` | 1 | 🔐 Browser |
|
|
155
|
+
| **reuters** | `search` | 1 | 🔐 Browser |
|
|
156
|
+
| **smzdm** | `search` | 1 | 🔐 Browser |
|
|
157
|
+
| **weibo** | `hot` | 1 | 🔐 Browser |
|
|
158
|
+
| **yahoo-finance** | `quote` | 1 | 🔐 Browser |
|
|
153
159
|
|
|
154
160
|
## Output Formats
|
|
155
161
|
|
|
@@ -194,7 +200,7 @@ Explore outputs to `.opencli/explore/<site>/` (manifest.json, endpoints.json, ca
|
|
|
194
200
|
|
|
195
201
|
See **[TESTING.md](./TESTING.md)** for the full testing guide, including:
|
|
196
202
|
|
|
197
|
-
- Current test coverage (unit +
|
|
203
|
+
- Current test coverage (unit + E2E tests across 19 sites)
|
|
198
204
|
- How to run tests locally
|
|
199
205
|
- How to add tests when creating new adapters
|
|
200
206
|
- CI/CD pipeline with sharding
|
package/README.zh-CN.md
CHANGED
|
@@ -141,6 +141,7 @@ npm install -g @jackwener/opencli@latest
|
|
|
141
141
|
| **boss** | `search` `detail` | 🔐 浏览器 |
|
|
142
142
|
| **coupang** | `search` `add-to-cart` | 🔐 浏览器 |
|
|
143
143
|
| **youtube** | `search` | 🔐 浏览器 |
|
|
144
|
+
| **linkedin** | `search` | 🔐 浏览器 |
|
|
144
145
|
| **yahoo-finance** | `quote` | 🔐 浏览器 |
|
|
145
146
|
| **reuters** | `search` | 🔐 浏览器 |
|
|
146
147
|
| **smzdm** | `search` | 🔐 浏览器 |
|
package/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opencli
|
|
3
|
-
description: "OpenCLI — Make any website your CLI. Zero risk, AI-powered, reuse Chrome login."
|
|
4
|
-
version: 0.7.
|
|
3
|
+
description: "OpenCLI — Make any website your CLI. Zero risk, AI-powered, reuse Chrome login. 80+ commands across 19 sites."
|
|
4
|
+
version: 0.7.3
|
|
5
5
|
author: jackwener
|
|
6
|
-
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, AI, agent]
|
|
6
|
+
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, youtube, boss, coupang, AI, agent]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# OpenCLI
|
|
@@ -68,6 +68,7 @@ opencli zhihu question --id 34816524 # 问题详情和回答
|
|
|
68
68
|
opencli xiaohongshu search --keyword "美食" # 搜索笔记
|
|
69
69
|
opencli xiaohongshu notifications # 通知(mentions/likes/connections)
|
|
70
70
|
opencli xiaohongshu feed --limit 10 # 推荐 Feed
|
|
71
|
+
opencli xiaohongshu me # 我的信息
|
|
71
72
|
opencli xiaohongshu user --uid xxx # 用户主页
|
|
72
73
|
|
|
73
74
|
# 雪球 Xueqiu (browser)
|
|
@@ -131,9 +132,13 @@ opencli weibo hot --limit 10 # 微博热搜
|
|
|
131
132
|
|
|
132
133
|
# BOSS直聘 (browser)
|
|
133
134
|
opencli boss search --query "AI agent" # 搜索职位
|
|
135
|
+
opencli boss detail --securityId xxx # 职位详情
|
|
134
136
|
|
|
135
137
|
# YouTube (browser)
|
|
136
138
|
opencli youtube search --query "rust" # 搜索视频
|
|
139
|
+
opencli youtube video --url "https://www.youtube.com/watch?v=xxx" # 视频元数据(标题、播放量、描述等)
|
|
140
|
+
opencli youtube transcript --url "https://www.youtube.com/watch?v=xxx" # 获取视频字幕/转录
|
|
141
|
+
opencli youtube transcript --url "xxx" --lang zh-Hans --mode raw # 指定语言 + 原始时间戳模式
|
|
137
142
|
|
|
138
143
|
# Yahoo Finance (browser)
|
|
139
144
|
opencli yahoo-finance quote --symbol AAPL # 股票行情
|
|
@@ -157,7 +162,8 @@ opencli list -f yaml # YAML output
|
|
|
157
162
|
opencli validate # Validate all CLI definitions
|
|
158
163
|
opencli validate bilibili # Validate specific site
|
|
159
164
|
opencli setup # Interactive token setup (auto-discover + TUI checkbox)
|
|
160
|
-
opencli doctor # Diagnose token config across all tools
|
|
165
|
+
opencli doctor # Diagnose token & extension config across all tools
|
|
166
|
+
opencli doctor --live # Also test live browser connectivity
|
|
161
167
|
opencli doctor --fix -y # Auto-fix all config files (non-interactive)
|
|
162
168
|
```
|
|
163
169
|
|
package/dist/cli-manifest.json
CHANGED
|
@@ -680,6 +680,90 @@
|
|
|
680
680
|
],
|
|
681
681
|
"type": "yaml"
|
|
682
682
|
},
|
|
683
|
+
{
|
|
684
|
+
"site": "linkedin",
|
|
685
|
+
"name": "search",
|
|
686
|
+
"description": "",
|
|
687
|
+
"strategy": "header",
|
|
688
|
+
"browser": true,
|
|
689
|
+
"args": [
|
|
690
|
+
{
|
|
691
|
+
"name": "query",
|
|
692
|
+
"type": "string",
|
|
693
|
+
"required": true,
|
|
694
|
+
"help": "Job search keywords"
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
"name": "location",
|
|
698
|
+
"type": "string",
|
|
699
|
+
"required": false,
|
|
700
|
+
"help": "Location text such as San Francisco Bay Area"
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
"name": "limit",
|
|
704
|
+
"type": "int",
|
|
705
|
+
"default": 10,
|
|
706
|
+
"required": false,
|
|
707
|
+
"help": "Number of jobs to return (max 100)"
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
"name": "start",
|
|
711
|
+
"type": "int",
|
|
712
|
+
"default": 0,
|
|
713
|
+
"required": false,
|
|
714
|
+
"help": "Result offset for pagination"
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
"name": "details",
|
|
718
|
+
"type": "bool",
|
|
719
|
+
"default": false,
|
|
720
|
+
"required": false,
|
|
721
|
+
"help": "Include full job description and apply URL (slower)"
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
"name": "company",
|
|
725
|
+
"type": "string",
|
|
726
|
+
"required": false,
|
|
727
|
+
"help": "Comma-separated company names or LinkedIn company IDs"
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
"name": "experience_level",
|
|
731
|
+
"type": "string",
|
|
732
|
+
"required": false,
|
|
733
|
+
"help": "Comma-separated: internship, entry, associate, mid-senior, director, executive"
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
"name": "job_type",
|
|
737
|
+
"type": "string",
|
|
738
|
+
"required": false,
|
|
739
|
+
"help": "Comma-separated: full-time, part-time, contract, temporary, volunteer, internship, other"
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
"name": "date_posted",
|
|
743
|
+
"type": "string",
|
|
744
|
+
"required": false,
|
|
745
|
+
"help": "One of: any, month, week, 24h"
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
"name": "remote",
|
|
749
|
+
"type": "string",
|
|
750
|
+
"required": false,
|
|
751
|
+
"help": "Comma-separated: on-site, hybrid, remote"
|
|
752
|
+
}
|
|
753
|
+
],
|
|
754
|
+
"type": "ts",
|
|
755
|
+
"modulePath": "linkedin/search.js",
|
|
756
|
+
"domain": "www.linkedin.com",
|
|
757
|
+
"columns": [
|
|
758
|
+
"rank",
|
|
759
|
+
"title",
|
|
760
|
+
"company",
|
|
761
|
+
"location",
|
|
762
|
+
"listed",
|
|
763
|
+
"salary",
|
|
764
|
+
"url"
|
|
765
|
+
]
|
|
766
|
+
},
|
|
683
767
|
{
|
|
684
768
|
"site": "reddit",
|
|
685
769
|
"name": "comment",
|
|
@@ -858,19 +942,18 @@
|
|
|
858
942
|
"site": "reddit",
|
|
859
943
|
"name": "read",
|
|
860
944
|
"description": "Read a Reddit post and its comments",
|
|
861
|
-
"domain": "reddit.com",
|
|
862
945
|
"strategy": "cookie",
|
|
863
946
|
"browser": true,
|
|
864
947
|
"args": [
|
|
865
948
|
{
|
|
866
949
|
"name": "post_id",
|
|
867
|
-
"type": "
|
|
950
|
+
"type": "str",
|
|
868
951
|
"required": true,
|
|
869
952
|
"help": "Post ID (e.g. 1abc123) or full URL"
|
|
870
953
|
},
|
|
871
954
|
{
|
|
872
955
|
"name": "sort",
|
|
873
|
-
"type": "
|
|
956
|
+
"type": "str",
|
|
874
957
|
"default": "best",
|
|
875
958
|
"required": false,
|
|
876
959
|
"help": "Comment sort: best, top, new, controversial, old, qa"
|
|
@@ -880,32 +963,39 @@
|
|
|
880
963
|
"type": "int",
|
|
881
964
|
"default": 25,
|
|
882
965
|
"required": false,
|
|
883
|
-
"help": "Number of top-level comments
|
|
884
|
-
}
|
|
885
|
-
],
|
|
886
|
-
"columns": [
|
|
887
|
-
"type",
|
|
888
|
-
"author",
|
|
889
|
-
"score",
|
|
890
|
-
"text"
|
|
891
|
-
],
|
|
892
|
-
"pipeline": [
|
|
966
|
+
"help": "Number of top-level comments"
|
|
967
|
+
},
|
|
893
968
|
{
|
|
894
|
-
"
|
|
969
|
+
"name": "depth",
|
|
970
|
+
"type": "int",
|
|
971
|
+
"default": 2,
|
|
972
|
+
"required": false,
|
|
973
|
+
"help": "Max reply depth (1=no replies, 2=one level of replies, etc.)"
|
|
895
974
|
},
|
|
896
975
|
{
|
|
897
|
-
"
|
|
976
|
+
"name": "replies",
|
|
977
|
+
"type": "int",
|
|
978
|
+
"default": 5,
|
|
979
|
+
"required": false,
|
|
980
|
+
"help": "Max replies shown per comment at each level (sorted by score)"
|
|
898
981
|
},
|
|
899
982
|
{
|
|
900
|
-
"
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
983
|
+
"name": "max_length",
|
|
984
|
+
"type": "int",
|
|
985
|
+
"default": 2000,
|
|
986
|
+
"required": false,
|
|
987
|
+
"help": "Max characters per comment body (min 100)"
|
|
906
988
|
}
|
|
907
989
|
],
|
|
908
|
-
"type": "
|
|
990
|
+
"type": "ts",
|
|
991
|
+
"modulePath": "reddit/read.js",
|
|
992
|
+
"domain": "reddit.com",
|
|
993
|
+
"columns": [
|
|
994
|
+
"type",
|
|
995
|
+
"author",
|
|
996
|
+
"score",
|
|
997
|
+
"text"
|
|
998
|
+
]
|
|
909
999
|
},
|
|
910
1000
|
{
|
|
911
1001
|
"site": "reddit",
|
|
@@ -2639,6 +2729,89 @@
|
|
|
2639
2729
|
"url"
|
|
2640
2730
|
]
|
|
2641
2731
|
},
|
|
2732
|
+
{
|
|
2733
|
+
"site": "youtube",
|
|
2734
|
+
"name": "transcript-group",
|
|
2735
|
+
"description": "",
|
|
2736
|
+
"strategy": "cookie",
|
|
2737
|
+
"browser": true,
|
|
2738
|
+
"args": [],
|
|
2739
|
+
"type": "ts",
|
|
2740
|
+
"modulePath": "youtube/transcript-group.js"
|
|
2741
|
+
},
|
|
2742
|
+
{
|
|
2743
|
+
"site": "youtube",
|
|
2744
|
+
"name": "transcript-group.test",
|
|
2745
|
+
"description": "",
|
|
2746
|
+
"strategy": "cookie",
|
|
2747
|
+
"browser": true,
|
|
2748
|
+
"args": [],
|
|
2749
|
+
"type": "ts",
|
|
2750
|
+
"modulePath": "youtube/transcript-group.test.js"
|
|
2751
|
+
},
|
|
2752
|
+
{
|
|
2753
|
+
"site": "youtube",
|
|
2754
|
+
"name": "transcript",
|
|
2755
|
+
"description": "Get YouTube video transcript/subtitles",
|
|
2756
|
+
"strategy": "cookie",
|
|
2757
|
+
"browser": true,
|
|
2758
|
+
"args": [
|
|
2759
|
+
{
|
|
2760
|
+
"name": "url",
|
|
2761
|
+
"type": "str",
|
|
2762
|
+
"required": true,
|
|
2763
|
+
"help": "YouTube video URL or video ID"
|
|
2764
|
+
},
|
|
2765
|
+
{
|
|
2766
|
+
"name": "lang",
|
|
2767
|
+
"type": "str",
|
|
2768
|
+
"required": false,
|
|
2769
|
+
"help": "Language code (e.g. en, zh-Hans). Omit to auto-select"
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
"name": "mode",
|
|
2773
|
+
"type": "str",
|
|
2774
|
+
"default": "grouped",
|
|
2775
|
+
"required": false,
|
|
2776
|
+
"help": "Output mode: grouped (readable paragraphs) or raw (every segment)"
|
|
2777
|
+
}
|
|
2778
|
+
],
|
|
2779
|
+
"type": "ts",
|
|
2780
|
+
"modulePath": "youtube/transcript.js",
|
|
2781
|
+
"domain": "www.youtube.com"
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
"site": "youtube",
|
|
2785
|
+
"name": "utils",
|
|
2786
|
+
"description": "",
|
|
2787
|
+
"strategy": "cookie",
|
|
2788
|
+
"browser": true,
|
|
2789
|
+
"args": [],
|
|
2790
|
+
"type": "ts",
|
|
2791
|
+
"modulePath": "youtube/utils.js"
|
|
2792
|
+
},
|
|
2793
|
+
{
|
|
2794
|
+
"site": "youtube",
|
|
2795
|
+
"name": "video",
|
|
2796
|
+
"description": "Get YouTube video metadata (title, views, description, etc.)",
|
|
2797
|
+
"strategy": "cookie",
|
|
2798
|
+
"browser": true,
|
|
2799
|
+
"args": [
|
|
2800
|
+
{
|
|
2801
|
+
"name": "url",
|
|
2802
|
+
"type": "str",
|
|
2803
|
+
"required": true,
|
|
2804
|
+
"help": "YouTube video URL or video ID"
|
|
2805
|
+
}
|
|
2806
|
+
],
|
|
2807
|
+
"type": "ts",
|
|
2808
|
+
"modulePath": "youtube/video.js",
|
|
2809
|
+
"domain": "www.youtube.com",
|
|
2810
|
+
"columns": [
|
|
2811
|
+
"field",
|
|
2812
|
+
"value"
|
|
2813
|
+
]
|
|
2814
|
+
},
|
|
2642
2815
|
{
|
|
2643
2816
|
"site": "zhihu",
|
|
2644
2817
|
"name": "hot",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|