paper-search-cli 0.2.0 → 0.3.1
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/.env.example +2 -6
- package/README.md +149 -653
- package/README.zh.md +270 -0
- package/dist/cli.js +184 -21
- package/dist/cli.js.map +1 -1
- package/dist/config/ConfigService.d.ts +1 -1
- package/dist/config/ConfigService.d.ts.map +1 -1
- package/dist/config/ConfigService.js +1 -3
- package/dist/config/ConfigService.js.map +1 -1
- package/dist/config/ResultCaps.d.ts +4 -0
- package/dist/config/ResultCaps.d.ts.map +1 -0
- package/dist/config/ResultCaps.js +10 -0
- package/dist/config/ResultCaps.js.map +1 -0
- package/dist/core/capabilityProfile.d.ts +18 -0
- package/dist/core/capabilityProfile.d.ts.map +1 -0
- package/dist/core/capabilityProfile.js +167 -0
- package/dist/core/capabilityProfile.js.map +1 -0
- package/dist/core/diagnostics.js +16 -16
- package/dist/core/diagnostics.js.map +1 -1
- package/dist/core/handleToolCall.d.ts.map +1 -1
- package/dist/core/handleToolCall.js +33 -0
- package/dist/core/handleToolCall.js.map +1 -1
- package/dist/core/liveSmoke.d.ts +42 -0
- package/dist/core/liveSmoke.d.ts.map +1 -0
- package/dist/core/liveSmoke.js +226 -0
- package/dist/core/liveSmoke.js.map +1 -0
- package/dist/core/platformMetadata.js +2 -2
- package/dist/core/platformMetadata.js.map +1 -1
- package/dist/core/schemas.d.ts +77 -2
- package/dist/core/schemas.d.ts.map +1 -1
- package/dist/core/schemas.js +58 -3
- package/dist/core/schemas.js.map +1 -1
- package/dist/core/textReports.d.ts +21 -0
- package/dist/core/textReports.d.ts.map +1 -0
- package/dist/core/textReports.js +85 -0
- package/dist/core/textReports.js.map +1 -0
- package/dist/core/tools.d.ts.map +1 -1
- package/dist/core/tools.js +60 -1
- package/dist/core/tools.js.map +1 -1
- package/dist/platforms/BioRxivSearcher.d.ts.map +1 -1
- package/dist/platforms/BioRxivSearcher.js +40 -21
- package/dist/platforms/BioRxivSearcher.js.map +1 -1
- package/dist/platforms/CORESearcher.d.ts.map +1 -1
- package/dist/platforms/CORESearcher.js +39 -9
- package/dist/platforms/CORESearcher.js.map +1 -1
- package/dist/platforms/GoogleScholarSearcher.d.ts.map +1 -1
- package/dist/platforms/GoogleScholarSearcher.js +3 -2
- package/dist/platforms/GoogleScholarSearcher.js.map +1 -1
- package/dist/platforms/OpenAIRESearcher.js +1 -1
- package/dist/platforms/OpenAIRESearcher.js.map +1 -1
- package/dist/services/CitationService.d.ts.map +1 -1
- package/dist/services/CitationService.js +8 -2
- package/dist/services/CitationService.js.map +1 -1
- package/dist/services/JournalMetricsService.js +1 -1
- package/dist/services/JournalMetricsService.js.map +1 -1
- package/dist/services/OpenAccessFallbackService.d.ts +20 -0
- package/dist/services/OpenAccessFallbackService.d.ts.map +1 -1
- package/dist/services/OpenAccessFallbackService.js +95 -72
- package/dist/services/OpenAccessFallbackService.js.map +1 -1
- package/dist/skills/SkillInstaller.d.ts +108 -0
- package/dist/skills/SkillInstaller.d.ts.map +1 -0
- package/dist/skills/SkillInstaller.js +389 -0
- package/dist/skills/SkillInstaller.js.map +1 -0
- package/dist/utils/RateLimiter.d.ts.map +1 -1
- package/dist/utils/RateLimiter.js +7 -0
- package/dist/utils/RateLimiter.js.map +1 -1
- package/package.json +2 -2
- package/skills/paper-search/SKILL.md +52 -143
- package/skills/paper-search/references/capability-routing.md +147 -0
- package/skills/paper-search/references/cli-contract.md +152 -0
- package/skills/paper-search/references/management-layer.md +140 -0
- package/README-sc.md +0 -766
package/README.zh.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Paper Search CLI
|
|
2
|
+
|
|
3
|
+
简体中文 | [English](README.md)
|
|
4
|
+
|
|
5
|
+
Paper Search CLI 是一个面向 AI agent 的 Skill + CLI 包,基于独立的 Node.js 命令行工具构建,用于学术文献工作。它为 AI agent、终端用户和脚本提供一个可复现的命令层,并通过 agent 友好的 JSON 输出覆盖文献元数据检索、施引/参考文献扩展、期刊指标检索、PDF 获取/下载和论文正文片段检索。
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
[](https://linux.do)
|
|
13
|
+
|
|
14
|
+
[快速开始](#快速开始) | [架构说明](#架构说明) | [配置](#配置) | [Agent Skill](#agent-skill) | [支持平台](#支持平台) | [命令](#命令) | [排障](#排障)
|
|
15
|
+
|
|
16
|
+
## 核心功能
|
|
17
|
+
|
|
18
|
+
| 功能 | 主要命令 | 返回内容 |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| 文献元数据检索 | `paper-search search`, `paper-search run search_*` | 题名、作者、年份、期刊、DOI、PMID/PMCID、arXiv ID、URL、摘要和来源元数据 |
|
|
21
|
+
| 引文扩展 | `paper-search run get_paper_citations`, `paper-search run get_paper_references` | 已知 Semantic Scholar paper ID、DOI 或 arXiv ID 对应论文的施引文献和参考文献 |
|
|
22
|
+
| 期刊指标检索 | `paper-search journal-metrics`, `paper-search run query_journal_metrics` | 影响因子、5 年 IF、JCR/SSCI 分区、中科院分区、JCI、ESI、预警和等级字段 |
|
|
23
|
+
| PDF 获取和下载 | `paper-search download`, `paper-search run download_with_fallback` | 通过原生来源、开放获取、已配置权限来源和启用时的 Sci-Hub fallback 获取 PDF |
|
|
24
|
+
| 正文片段检索 | `paper-search run search_semantic_snippets` | Semantic Scholar Open Access 正文片段,用于查方法、参数和写法线索 |
|
|
25
|
+
|
|
26
|
+
## 架构说明
|
|
27
|
+
|
|
28
|
+
`paper-search` 不是 MCP Server,而是普通 CLI。AI 工具可以通过随包发布的 Skill 调用它,终端用户和脚本也可以直接调用同一个 `paper-search` 命令。
|
|
29
|
+
|
|
30
|
+
| 层 | 负责什么 |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| CLI 本体 | 执行文献检索、引文扩展、期刊指标检索、PDF 获取/下载、正文片段检索和稳定 JSON 输出 |
|
|
33
|
+
| Bundled Skill | 随包发布 `skills/paper-search`,提供 agent 路由规则和 focused references;不保存密钥、cookie 或账号信息 |
|
|
34
|
+
| Friendly Management Layer | 围绕五个主要能力 `metadata_search`、`citation_expansion`、`journal_metrics`、`pdf_discovery`、`body_snippet_search` 提供 `doctor`、`smoke`、`skills`、`config`、`tools`。`doctor` 健康报告包含脱敏配置、Capability Profile、平台/来源状态、缺失项和降级项;`smoke` 检查命令入口连通性和 live 可用性;`skills` 负责同步随包发布的 Skill |
|
|
35
|
+
|
|
36
|
+
五个主要能力由 CLI 本体执行,由管理层报告和检查。Capability Profile 也会报告 `entitled_access`,让用户看到出版商 API key、数据库 key、TDM token 或机构权限是否已配置。某一个能力缺少配置或降级,不会导致其他独立能力不可用。
|
|
37
|
+
|
|
38
|
+
## 快速开始
|
|
39
|
+
|
|
40
|
+
要求 Node.js >= 18.0.0 和 npm。
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g paper-search-cli
|
|
44
|
+
paper-search setup
|
|
45
|
+
paper-search doctor --pretty
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
尝试五个主要功能:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
paper-search search "machine learning clinical prediction" --platform crossref --max-results 3 --pretty
|
|
52
|
+
paper-search run get_paper_citations --arg doi="10.1038/nature12373" --arg limit=5 --pretty
|
|
53
|
+
paper-search journal-metrics "Nature" "BMJ" --pretty
|
|
54
|
+
paper-search download 10.48550/arxiv.1201.0490 --platform arxiv --save-path ./downloads
|
|
55
|
+
paper-search run search_semantic_snippets --arg query="propensity score matching" --arg limit=3 --pretty
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
常用检查:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
paper-search tools --pretty
|
|
62
|
+
paper-search doctor --format text
|
|
63
|
+
paper-search smoke --mock --pretty
|
|
64
|
+
paper-search skills status --pretty
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 支持平台
|
|
68
|
+
|
|
69
|
+
平台组表适合快速选来源;下面的能力矩阵用于更清楚地判断每个平台实际能做什么。
|
|
70
|
+
|
|
71
|
+
### 平台组
|
|
72
|
+
|
|
73
|
+
| 平台组 | 平台 | 主要用途 |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| 综合学术元数据 | Crossref, OpenAlex, Semantic Scholar, Google Scholar | 广覆盖发现、DOI 元数据、引用线索、文献初筛 |
|
|
76
|
+
| 期刊指标 | EasyScholar | 影响因子、JCR/SSCI 分区、中科院分区、JCI、ESI、预警 |
|
|
77
|
+
| 生物医学和生命科学 | PubMed, PubMed Central, Europe PMC | 生物医学元数据、PMID/PMCID 核验、开放全文 |
|
|
78
|
+
| 预印本和会议稿 | arXiv, bioRxiv, medRxiv, OpenReview, IACR ePrint | 预印本、AI/ML 投稿、密码学 ePrint |
|
|
79
|
+
| 计算机和工程 | DBLP, ACM metadata, IEEE Xplore, USENIX | CS 目录、工程元数据、会议论文 |
|
|
80
|
+
| 开放获取和仓储 | CORE, OpenAIRE, Unpaywall | 仓储发现和开放获取 PDF fallback |
|
|
81
|
+
| 引文库和出版商 | Web of Science, Scopus, ScienceDirect, Springer Nature/SpringerLink, Wiley | 机构权限元数据、出版商记录、entitled access |
|
|
82
|
+
| DOI 定向 fallback | Sci-Hub | 启用时作为 DOI 定向 PDF fallback |
|
|
83
|
+
|
|
84
|
+
### 能力矩阵
|
|
85
|
+
|
|
86
|
+
#### 综合学术元数据
|
|
87
|
+
|
|
88
|
+
| 平台 | 元数据检索 | PDF 路径 | 正文/全文 | 被引统计 | 配置 | 说明 |
|
|
89
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
90
|
+
| Crossref | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 | ❌ 不需要 | 默认广覆盖元数据来源 |
|
|
91
|
+
| OpenAlex | ✅ 支持 | 🟡 条件支持 | ❌ 不支持 | ✅ 支持 | ❌ 不需要 | 免费元数据;记录含 OA 链接时可帮助 PDF fallback |
|
|
92
|
+
| Semantic Scholar | ✅ 支持 | 🟡 条件支持 | ✅ 正文片段 | ✅ 支持 | 🟡 可选;正文片段需要 `SEMANTIC_SCHOLAR_API_KEY` | 适合 AI/CS、引文扩展和正文片段线索 |
|
|
93
|
+
| Google Scholar | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 | ❌ 不需要 | 基于页面解析的广覆盖发现 |
|
|
94
|
+
|
|
95
|
+
#### 期刊指标
|
|
96
|
+
|
|
97
|
+
| 平台 | 元数据检索 | PDF 路径 | 正文/全文 | 被引统计 | 配置 | 说明 |
|
|
98
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
99
|
+
| EasyScholar | 🟡 仅期刊指标 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 | ✅ 必需 `EASYSCHOLAR_KEY` | 影响因子、JCR/SSCI 分区、中科院分区、JCI、ESI、预警和等级字段 |
|
|
100
|
+
|
|
101
|
+
#### 生物医学和生命科学
|
|
102
|
+
|
|
103
|
+
| 平台 | 元数据检索 | PDF 路径 | 正文/全文 | 被引统计 | 配置 | 说明 |
|
|
104
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
105
|
+
| PubMed | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 | 🟡 可选 `PUBMED_API_KEY`, `NCBI_EMAIL`, `NCBI_TOOL` | NCBI E-utilities 生物医学元数据 |
|
|
106
|
+
| PubMed Central | ✅ 支持 | ✅ 支持 | ✅ 支持 | ❌ 不支持 | ❌ 不需要 | 生物医学开放全文和 PMC PDF |
|
|
107
|
+
| Europe PMC | ✅ 支持 | 🟡 条件支持 | 🟡 条件支持 | ❌ 不支持 | ❌ 不需要 | 生物医学元数据和开放全文链接 |
|
|
108
|
+
|
|
109
|
+
#### 预印本和会议稿
|
|
110
|
+
|
|
111
|
+
| 平台 | 元数据检索 | PDF 路径 | 正文/全文 | 被引统计 | 配置 | 说明 |
|
|
112
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
113
|
+
| arXiv | ✅ 支持 | ✅ 支持 | ✅ 支持 | ❌ 不支持 | ❌ 不需要 | 物理、计算机、数学等预印本 |
|
|
114
|
+
| bioRxiv | ✅ 支持 | ✅ 支持 | ✅ 支持 | ❌ 不支持 | ❌ 不需要 | 生物学预印本 |
|
|
115
|
+
| medRxiv | ✅ 支持 | ✅ 支持 | ✅ 支持 | ❌ 不支持 | ❌ 不需要 | 医学预印本 |
|
|
116
|
+
| OpenReview | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不需要 | 公开 OpenReview notes、评审和投稿记录 |
|
|
117
|
+
| IACR ePrint | ✅ 支持 | ✅ 支持 | ✅ 支持 | ❌ 不支持 | ❌ 不需要 | 密码学 ePrint 论文 |
|
|
118
|
+
|
|
119
|
+
#### 计算机和工程
|
|
120
|
+
|
|
121
|
+
| 平台 | 元数据检索 | PDF 路径 | 正文/全文 | 被引统计 | 配置 | 说明 |
|
|
122
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
123
|
+
| DBLP | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不需要 | 官方 DBLP 计算机文献目录 |
|
|
124
|
+
| ACM metadata | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 | ❌ 不需要 | 通过 Crossref 的 ACM DOI 前缀元数据检索;不抓取 ACM 页面 |
|
|
125
|
+
| USENIX | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不需要 | 基于 DBLP 的 USENIX 元数据;不抓取 USENIX 搜索页 |
|
|
126
|
+
| IEEE Xplore | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 | ✅ 必需 `IEEE_API_KEY` | 官方 IEEE Xplore Metadata API |
|
|
127
|
+
|
|
128
|
+
#### 开放获取和仓储
|
|
129
|
+
|
|
130
|
+
| 平台 | 元数据检索 | PDF 路径 | 正文/全文 | 被引统计 | 配置 | 说明 |
|
|
131
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
132
|
+
| CORE | ✅ 支持 | 🟡 条件支持 | 🟡 条件支持 | ❌ 不支持 | 🟡 可选 `CORE_API_KEY` | 仓储记录可能暴露 PDF 或全文链接 |
|
|
133
|
+
| OpenAIRE | ✅ 支持 | 🟡 条件支持 | ❌ 不支持 | ❌ 不支持 | 🟡 可选 `OPENAIRE_API_KEY` | 公开检索通常不需要 key |
|
|
134
|
+
| Unpaywall | 🟡 仅 DOI 查询 | 🟡 条件支持 | ❌ 不支持 | ❌ 不支持 | ✅ 必需 `UNPAYWALL_EMAIL` 或 `PAPER_SEARCH_UNPAYWALL_EMAIL` | DOI 开放获取 PDF 定位;需要邮箱,不是 API key |
|
|
135
|
+
|
|
136
|
+
#### 引文库和出版商
|
|
137
|
+
|
|
138
|
+
| 平台 | 元数据检索 | PDF 路径 | 正文/全文 | 被引统计 | 配置 | 说明 |
|
|
139
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
140
|
+
| Web of Science | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 | ✅ 必需 `WOS_API_KEY` | 引文数据库元数据、日期排序、年份范围 |
|
|
141
|
+
| ScienceDirect | ✅ 支持 | 🟡 条件支持 | ❌ 不支持 | ✅ 支持 | ✅ 必需 `ELSEVIER_API_KEY` | Elsevier 元数据;ScienceDirect 和 Scopus 产品权限需要分别开通 |
|
|
142
|
+
| Springer Nature / SpringerLink | ✅ 支持 | 🟡 条件支持 | ❌ 不支持 | ❌ 不支持 | ✅ 必需 `SPRINGER_API_KEY`;🟡 可选 `SPRINGER_OPENACCESS_API_KEY` | `springerlink` 是现有 Springer 集成的别名 |
|
|
143
|
+
| Wiley | ❌ 不支持关键词搜索 | ✅ DOI 下载 | ✅ 支持 | ❌ 不支持 | ✅ 必需 `WILEY_TDM_TOKEN` | TDM API;先通过其他元数据来源找到 DOI 再下载 |
|
|
144
|
+
| Scopus | ✅ 支持 | 🟡 条件元数据 | ❌ 不支持 | ✅ 支持 | ✅ 必需 `ELSEVIER_API_KEY` | 摘要和引文数据库;ScienceDirect 和 Scopus 产品权限需要分别开通 |
|
|
145
|
+
|
|
146
|
+
#### DOI 定向 fallback
|
|
147
|
+
|
|
148
|
+
| 平台 | 元数据检索 | PDF 路径 | 正文/全文 | 被引统计 | 配置 | 说明 |
|
|
149
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
150
|
+
| Sci-Hub | ❌ 不支持 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不需要 | DOI/URL 定向查询;启用时作为 PDF 最后 fallback |
|
|
151
|
+
|
|
152
|
+
说明:
|
|
153
|
+
|
|
154
|
+
- 元数据检索指发现和初筛论文,不等于 PDF 下载或正文证据。
|
|
155
|
+
- `pdf_discovery` 会区分开放获取来源、已配置权限来源,以及单独标识的 Sci-Hub 最后 fallback。
|
|
156
|
+
- EasyScholar 是期刊指标来源,不是论文检索来源。
|
|
157
|
+
- Sci-Hub 不属于 `metadata_search`,只作为 DOI/URL 定向 PDF fallback。
|
|
158
|
+
- `🟡 条件支持` 表示只有记录暴露 DOI、开放获取链接、PDF URL,或用户配置了相应权限时才可用。
|
|
159
|
+
- API key 只在使用对应 key-backed 来源或工作流时才需要。
|
|
160
|
+
|
|
161
|
+
## 配置
|
|
162
|
+
|
|
163
|
+
多数免费元数据来源无需配置。为了稳定支持 agent 工作流,建议先运行 setup,把凭证写入用户级配置:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
paper-search setup
|
|
167
|
+
paper-search config list --pretty
|
|
168
|
+
paper-search doctor --pretty
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
默认配置路径:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
~/.config/paper-search-cli/config.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
配置文件权限会写成 `0600`。`config list`、`doctor` 和相关命令都会脱敏密钥。
|
|
178
|
+
|
|
179
|
+
### API Key 分级
|
|
180
|
+
|
|
181
|
+
| 等级 | 配置项 | 用途 | 什么时候配置 |
|
|
182
|
+
| --- | --- | --- | --- |
|
|
183
|
+
| 多数用户推荐 | `SEMANTIC_SCHOLAR_API_KEY` | 正文片段检索,以及更稳定的 Semantic Scholar 请求 | 需要查方法细节或高频使用 Semantic Scholar 时配置 |
|
|
184
|
+
| 多数用户推荐 | `UNPAYWALL_EMAIL` 或 `PAPER_SEARCH_UNPAYWALL_EMAIL` | DOI 开放获取 PDF 解析 | setup 时配置;需要的是邮箱,不是 API key |
|
|
185
|
+
| 多数用户推荐 | `CROSSREF_MAILTO` | Crossref polite pool | 长任务或高频元数据检索时配置 |
|
|
186
|
+
| 多数用户推荐 | `CORE_API_KEY` | CORE 仓储检索 | 依赖 CORE 或遇到匿名限流时配置 |
|
|
187
|
+
| 期刊指标 | `EASYSCHOLAR_KEY` | EasyScholar 影响因子、JCR/SSCI、中科院分区、JCI、ESI、预警 | 需要期刊指标时配置;建议用 `paper-search setup EASYSCHOLAR_KEY` 隐藏输入 |
|
|
188
|
+
| 生物医学高频 | `PUBMED_API_KEY`, `NCBI_EMAIL`, `NCBI_TOOL` | NCBI E-utilities 稳定性和更高限额 | 高频使用 PubMed 时配置 |
|
|
189
|
+
| 机构或出版商权限 | `WOS_API_KEY`, `IEEE_API_KEY`, `ELSEVIER_API_KEY`, `SPRINGER_API_KEY`, `SPRINGER_OPENACCESS_API_KEY`, `WILEY_TDM_TOKEN` | Web of Science、IEEE、Scopus、ScienceDirect、Springer、Wiley 元数据或权限访问 | 只有具备对应 API 或机构权限时配置 |
|
|
190
|
+
| 通常可选 | `OPENAIRE_API_KEY` | OpenAIRE 账号或配额场景 | 公开检索通常不需要 |
|
|
191
|
+
|
|
192
|
+
常用申请入口:
|
|
193
|
+
|
|
194
|
+
| 服务 | 链接 |
|
|
195
|
+
| --- | --- |
|
|
196
|
+
| EasyScholar | [EasyScholar Open API](https://www.easyscholar.cc/console/user/open) |
|
|
197
|
+
| Semantic Scholar | [Semantic Scholar API](https://www.semanticscholar.org/product/api) |
|
|
198
|
+
| Unpaywall | [Unpaywall API](https://unpaywall.org/products/api) |
|
|
199
|
+
| CORE | [CORE API](https://core.ac.uk/services/api) |
|
|
200
|
+
| PubMed | [NCBI API Keys](https://ncbiinsights.ncbi.nlm.nih.gov/2017/11/02/new-api-keys-for-the-e-utilities/) |
|
|
201
|
+
| Web of Science | [Clarivate Developer Portal](https://developer.clarivate.com/apis) |
|
|
202
|
+
| IEEE Xplore | [IEEE Xplore Metadata API](https://developer.ieee.org/docs/read/Searching_the_IEEE_Xplore_Metadata_API) |
|
|
203
|
+
| Elsevier | [Elsevier Developer Portal](https://dev.elsevier.com/apikey/manage) |
|
|
204
|
+
| Springer Nature | [Springer Nature Developers](https://dev.springernature.com/) |
|
|
205
|
+
| Wiley TDM | [Wiley Text and Data Mining](https://onlinelibrary.wiley.com/library-info/resources/text-and-datamining) |
|
|
206
|
+
| OpenAIRE | [OpenAIRE APIs](https://develop.openaire.eu/) |
|
|
207
|
+
|
|
208
|
+
## Agent Skill
|
|
209
|
+
|
|
210
|
+
npm 包会随包发布 agent Skill,位置是 `skills/paper-search/SKILL.md`。终端用户可以只用 CLI;AI agent 工作流应安装或同步 Skill,让 agent 正确路由五个主要功能。
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
paper-search setup --install-skills agents
|
|
214
|
+
paper-search skills status --pretty
|
|
215
|
+
paper-search skills diff --targets agents --format text
|
|
216
|
+
paper-search skills update --targets agents --pretty
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
支持的 target 包括 `agents`、`codex`、`claude`、`cursor`、`gemini`、`antigravity` 和 `all`。Skill 更新会覆盖 package-managed Skill 文件,同时保留 installed Skill 目录里的 extra files。
|
|
220
|
+
|
|
221
|
+
Skill 只告诉 agent 如何调用 `paper-search` CLI。API key 仍然应通过 `paper-search setup`、`paper-search config`、`.env` 或 shell 环境变量配置。
|
|
222
|
+
|
|
223
|
+
## 命令
|
|
224
|
+
|
|
225
|
+
| 命令 | 用途 |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `paper-search search` | 集成式元数据检索 |
|
|
228
|
+
| `paper-search journal-metrics` | EasyScholar 期刊指标检索 |
|
|
229
|
+
| `paper-search download` | 对已核验 paper ID 或 DOI 下载 PDF |
|
|
230
|
+
| `paper-search run` | 用 `--arg` 或 `--json-args` 精确调用工具 |
|
|
231
|
+
| `paper-search tools` | 运行时工具名和 schema |
|
|
232
|
+
| `paper-search doctor` | 脱敏配置、Capability Profile 和平台状态 |
|
|
233
|
+
| `paper-search smoke` | mock 或 live 自检 |
|
|
234
|
+
| `paper-search skills` | Bundled Skill 状态、diff 和同步 |
|
|
235
|
+
| `paper-search config` | 用户级配置管理 |
|
|
236
|
+
|
|
237
|
+
完整命令和工具 schema:运行 `paper-search tools --pretty`,或查看 [`skills/paper-search/references/cli-contract.md`](skills/paper-search/references/cli-contract.md)。
|
|
238
|
+
|
|
239
|
+
## 输出
|
|
240
|
+
|
|
241
|
+
命令默认返回 JSON。需要格式化 JSON 时使用 `--pretty`;需要可读报告时再使用 `--format text`。
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
paper-search search "machine learning" --platform crossref --max-results 1 --pretty
|
|
245
|
+
paper-search doctor --format text
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## 排障
|
|
249
|
+
|
|
250
|
+
| 问题 | 首先检查 |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| 找不到命令 | 用 `npm install -g paper-search-cli` 重新全局安装 |
|
|
253
|
+
| 能力缺失 | 运行 `paper-search doctor --pretty`,再用 `paper-search setup` 配置缺失 key |
|
|
254
|
+
| 平台限流 | 降低 `--max-results`,配置对应 key,或切换来源 |
|
|
255
|
+
| Skill 似乎过期 | 运行 `paper-search skills status --pretty`,再运行 `paper-search skills update --targets agents --pretty` |
|
|
256
|
+
| 需要完整 CLI 细节 | 运行 `paper-search tools --pretty` |
|
|
257
|
+
|
|
258
|
+
## 使用边界
|
|
259
|
+
|
|
260
|
+
部分来源可能受平台条款、机构订阅或本地法律约束。只有在具备相应访问权和授权时才使用受限集成。
|
|
261
|
+
|
|
262
|
+
## 项目来源
|
|
263
|
+
|
|
264
|
+
本项目感谢 [LinuxDo](https://linux.do) 社区。CLI + Skill 路线和 paper-search 工作流改进来自社区交流与开源分享。
|
|
265
|
+
|
|
266
|
+
本项目也参考了 [openags/paper-search-mcp](https://github.com/openags/paper-search-mcp) 的思路,并将工作流调整为独立 CLI。
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT
|
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,12 @@ import { TOOLS } from './core/tools.js';
|
|
|
8
8
|
import { initializeSearchers } from './core/searchers.js';
|
|
9
9
|
import { handleToolCall } from './core/handleToolCall.js';
|
|
10
10
|
import { diagnoseError, diagnoseToolResult, diagnosticContextFromCli, getRequirementStatus } from './core/diagnostics.js';
|
|
11
|
+
import { buildCapabilityProfile } from './core/capabilityProfile.js';
|
|
11
12
|
import { CONFIG_KEYS, getConfigPath, importEnvFile, initUserConfig, listConfigEntries, loadUserConfigIntoEnv, maskValue, readUserConfig, assertConfigKey, setUserConfigValue, unsetUserConfigValue } from './config/ConfigService.js';
|
|
12
13
|
import { setupGlobalProxy } from './utils/HttpClient.js';
|
|
14
|
+
import { defaultSkillTargetIds, describeSkillTargets, diffSkillTargets, installSkillTargets, parseSkillTargets, statusSkillTargets } from './skills/SkillInstaller.js';
|
|
15
|
+
import { renderDoctorTextReport, renderSkillDiffTextReport } from './core/textReports.js';
|
|
16
|
+
import { runLiveSmoke } from './core/liveSmoke.js';
|
|
13
17
|
dotenv.config();
|
|
14
18
|
loadUserConfigIntoEnv();
|
|
15
19
|
setupGlobalProxy();
|
|
@@ -37,10 +41,13 @@ Usage:
|
|
|
37
41
|
paper-search journal-metrics <journal...> [--include-raw]
|
|
38
42
|
paper-search run <tool-name> --json-args '{"query":"machine learning","maxResults":5}'
|
|
39
43
|
paper-search status [--validate]
|
|
44
|
+
paper-search doctor [--validate]
|
|
45
|
+
paper-search smoke --mock|--live
|
|
46
|
+
paper-search skills <status|update|diff> [--targets agents,codex]
|
|
40
47
|
paper-search tools
|
|
41
48
|
paper-search diagnostics
|
|
42
49
|
paper-search config <init|set|get|unset|list|doctor|path|import-env|keys>
|
|
43
|
-
paper-search setup [--all]
|
|
50
|
+
paper-search setup [--all] [--install-skills agents,codex] [--skip-skills]
|
|
44
51
|
paper-search download <paper-id> --platform arxiv [--save-path ./downloads]
|
|
45
52
|
|
|
46
53
|
Global flags:
|
|
@@ -56,7 +63,10 @@ Examples:
|
|
|
56
63
|
paper-search setup
|
|
57
64
|
paper-search config set SEMANTIC_SCHOLAR_API_KEY sk_xxx
|
|
58
65
|
paper-search run search_pubmed --arg query="osteoarthritis occupational exposure" --arg maxResults=3
|
|
59
|
-
paper-search
|
|
66
|
+
paper-search doctor --pretty
|
|
67
|
+
paper-search doctor --format text
|
|
68
|
+
paper-search skills status --pretty
|
|
69
|
+
paper-search skills diff --format text
|
|
60
70
|
`;
|
|
61
71
|
}
|
|
62
72
|
function parseCli(argv) {
|
|
@@ -173,6 +183,17 @@ function flagsToArgs(flags) {
|
|
|
173
183
|
function formatOutput(payload, flags) {
|
|
174
184
|
return JSON.stringify(payload, null, flags.pretty ? 2 : 0);
|
|
175
185
|
}
|
|
186
|
+
function prepareOutput(result) {
|
|
187
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
188
|
+
return { payload: result };
|
|
189
|
+
}
|
|
190
|
+
const exitCode = result.exitCode;
|
|
191
|
+
if (typeof exitCode !== 'number') {
|
|
192
|
+
return { payload: result };
|
|
193
|
+
}
|
|
194
|
+
const { exitCode: _exitCode, ...payload } = result;
|
|
195
|
+
return { payload, exitCode };
|
|
196
|
+
}
|
|
176
197
|
function extractText(response) {
|
|
177
198
|
const content = response?.content;
|
|
178
199
|
if (!Array.isArray(content))
|
|
@@ -283,13 +304,22 @@ async function run(parsed) {
|
|
|
283
304
|
requirements: getRequirementStatus()
|
|
284
305
|
};
|
|
285
306
|
}
|
|
307
|
+
if (command === 'smoke') {
|
|
308
|
+
return handleSmokeCommand(flags);
|
|
309
|
+
}
|
|
310
|
+
if (command === 'skills') {
|
|
311
|
+
return handleSkillsCommand(positionals, flags);
|
|
312
|
+
}
|
|
286
313
|
if (command === 'config') {
|
|
287
314
|
return handleConfigCommand(positionals, flags);
|
|
288
315
|
}
|
|
289
316
|
if (command === 'setup') {
|
|
290
317
|
return handleSetupCommand(positionals, flags);
|
|
291
318
|
}
|
|
292
|
-
if (command === '
|
|
319
|
+
if (command === 'doctor') {
|
|
320
|
+
return handleDoctorCommand(flags);
|
|
321
|
+
}
|
|
322
|
+
if (command === 'status') {
|
|
293
323
|
return callTool('get_platform_status', { validate: flags.validate === true }, flags);
|
|
294
324
|
}
|
|
295
325
|
if (command === 'search') {
|
|
@@ -342,9 +372,6 @@ async function run(parsed) {
|
|
|
342
372
|
}
|
|
343
373
|
function handleConfigCommand(positionals, flags) {
|
|
344
374
|
const subcommand = positionals[0] || 'list';
|
|
345
|
-
if (subcommand === 'setup') {
|
|
346
|
-
return handleSetupCommand(positionals.slice(1), flags);
|
|
347
|
-
}
|
|
348
375
|
if (subcommand === 'path') {
|
|
349
376
|
return { ok: true, path: getConfigPath() };
|
|
350
377
|
}
|
|
@@ -367,6 +394,7 @@ function handleConfigCommand(positionals, flags) {
|
|
|
367
394
|
return {
|
|
368
395
|
ok: true,
|
|
369
396
|
path: getConfigPath(),
|
|
397
|
+
message: 'Use `paper-search doctor --pretty` for capability and health checks.',
|
|
370
398
|
configured: entries.filter(entry => entry.configured).length,
|
|
371
399
|
missing: entries.filter(entry => !entry.configured).map(entry => entry.key),
|
|
372
400
|
entries
|
|
@@ -439,7 +467,7 @@ const DEFAULT_SETUP_PROMPTS = [
|
|
|
439
467
|
secret: false
|
|
440
468
|
},
|
|
441
469
|
{
|
|
442
|
-
key: '
|
|
470
|
+
key: 'CORE_API_KEY',
|
|
443
471
|
label: 'CORE API key, optional but recommended for stable CORE access',
|
|
444
472
|
secret: true
|
|
445
473
|
},
|
|
@@ -480,6 +508,9 @@ async function handleSetupCommand(positionals, flags) {
|
|
|
480
508
|
const skipped = [];
|
|
481
509
|
const autoGenerated = [];
|
|
482
510
|
let defaultEmail = '';
|
|
511
|
+
const skillsRoot = typeof flags.skillsRoot === 'string' ? flags.skillsRoot : undefined;
|
|
512
|
+
const skipSkills = flags.skipSkills === true;
|
|
513
|
+
const explicitSkillTargets = typeof flags.installSkills === 'string' ? parseSkillTargets(flags.installSkills, skillsRoot) : undefined;
|
|
483
514
|
process.stderr.write(`Paper Search CLI setup\n`);
|
|
484
515
|
process.stderr.write(`Config file: ${path}\n`);
|
|
485
516
|
process.stderr.write(`Press Enter to keep an existing value or skip an optional value.\n`);
|
|
@@ -511,24 +542,152 @@ async function handleSetupCommand(positionals, flags) {
|
|
|
511
542
|
process.env[prompt.key] = value;
|
|
512
543
|
updated.push(prompt.key);
|
|
513
544
|
}
|
|
545
|
+
let selectedSkillTargets = [];
|
|
546
|
+
if (!skipSkills) {
|
|
547
|
+
if (explicitSkillTargets) {
|
|
548
|
+
selectedSkillTargets = explicitSkillTargets;
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
selectedSkillTargets = await promptSkillTargets(session, skillsRoot);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const skillDestinations = describeSkillTargets(selectedSkillTargets, skillsRoot);
|
|
555
|
+
const skillResult = selectedSkillTargets.length > 0
|
|
556
|
+
? installSkillTargets(selectedSkillTargets, { skillsRoot })
|
|
557
|
+
: undefined;
|
|
558
|
+
const capabilityProfile = buildCapabilityProfile();
|
|
559
|
+
const nextSteps = [
|
|
560
|
+
'Run "paper-search doctor --pretty" to review capability and platform health.',
|
|
561
|
+
'Run "paper-search smoke --mock --pretty" for an offline self-check.',
|
|
562
|
+
'Run "paper-search search \\"machine learning\\" --platform crossref --max-results 1 --pretty" for a no-key search check.'
|
|
563
|
+
];
|
|
564
|
+
return {
|
|
565
|
+
ok: skillResult ? skillResult.ok : true,
|
|
566
|
+
path,
|
|
567
|
+
updated,
|
|
568
|
+
kept,
|
|
569
|
+
skipped,
|
|
570
|
+
autoGenerated,
|
|
571
|
+
capabilityProfile,
|
|
572
|
+
skills: {
|
|
573
|
+
skipped: skipSkills || selectedSkillTargets.length === 0,
|
|
574
|
+
destinations: skillDestinations,
|
|
575
|
+
result: skillResult
|
|
576
|
+
},
|
|
577
|
+
nextSteps
|
|
578
|
+
};
|
|
514
579
|
}
|
|
515
580
|
finally {
|
|
516
581
|
session.close();
|
|
517
582
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
583
|
+
}
|
|
584
|
+
async function handleDoctorCommand(flags) {
|
|
585
|
+
const platformStatus = await callTool('get_platform_status', { validate: flags.validate === true }, {});
|
|
586
|
+
const configEntries = listConfigEntries(true);
|
|
587
|
+
const capabilityProfile = buildCapabilityProfile();
|
|
588
|
+
const report = {
|
|
589
|
+
ok: true,
|
|
590
|
+
config: {
|
|
591
|
+
path: getConfigPath(),
|
|
592
|
+
configured: configEntries.filter(entry => entry.configured).length,
|
|
593
|
+
missing: configEntries.filter(entry => !entry.configured).map(entry => entry.key),
|
|
594
|
+
entries: configEntries
|
|
595
|
+
},
|
|
596
|
+
capabilityProfile,
|
|
597
|
+
platformStatus
|
|
598
|
+
};
|
|
599
|
+
if (flags.format === 'text') {
|
|
600
|
+
return renderDoctorTextReport(report);
|
|
601
|
+
}
|
|
602
|
+
return report;
|
|
603
|
+
}
|
|
604
|
+
async function handleSmokeCommand(flags) {
|
|
605
|
+
const mode = typeof flags.mode === 'string' ? flags.mode : flags.live === true ? 'live' : 'mock';
|
|
606
|
+
if (flags.live === true || mode === 'live') {
|
|
607
|
+
const capabilityProfile = buildCapabilityProfile();
|
|
608
|
+
const skillsRoot = typeof flags.skillsRoot === 'string' ? flags.skillsRoot : undefined;
|
|
609
|
+
const skillStatus = statusSkillTargets(defaultSkillTargetIds(skillsRoot), { skillsRoot });
|
|
610
|
+
return runLiveSmoke({
|
|
611
|
+
capabilityProfile,
|
|
612
|
+
skillStatus,
|
|
613
|
+
hasConfig: key => Boolean(process.env[key] || readUserConfig()[key]),
|
|
614
|
+
callTool: (tool, args) => callTool(tool, args, {})
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
const capabilityProfile = buildCapabilityProfile();
|
|
618
|
+
const skillStatus = statusSkillTargets(defaultSkillTargetIds(typeof flags.skillsRoot === 'string' ? flags.skillsRoot : undefined), {
|
|
619
|
+
skillsRoot: typeof flags.skillsRoot === 'string' ? flags.skillsRoot : undefined
|
|
620
|
+
});
|
|
621
|
+
const cases = [
|
|
622
|
+
{
|
|
623
|
+
name: 'metadata_search excludes Sci-Hub',
|
|
624
|
+
ok: !capabilityProfile.entries.find(entry => entry.id === 'metadata_search')?.configured.includes('scihub')
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: 'citation_expansion uses Semantic Scholar Graph API',
|
|
628
|
+
ok: capabilityProfile.entries.find(entry => entry.id === 'citation_expansion')?.configured.includes('semantic_scholar_graph') === true
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: 'pdf_discovery has source groups',
|
|
632
|
+
ok: Boolean(capabilityProfile.entries.find(entry => entry.id === 'pdf_discovery')?.sourceGroups?.open_access_sources) &&
|
|
633
|
+
Boolean(capabilityProfile.entries.find(entry => entry.id === 'pdf_discovery')?.sourceGroups?.entitled_access_sources) &&
|
|
634
|
+
Boolean(capabilityProfile.entries.find(entry => entry.id === 'pdf_discovery')?.sourceGroups?.scihub_sources)
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: 'journal_metrics uses EASYSCHOLAR_KEY',
|
|
638
|
+
ok: capabilityProfile.entries.find(entry => entry.id === 'journal_metrics')?.requiredKeys?.join(',') === 'EASYSCHOLAR_KEY'
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: 'skill status runs offline',
|
|
642
|
+
ok: skillStatus.ok
|
|
643
|
+
}
|
|
521
644
|
];
|
|
645
|
+
const failed = cases.filter(item => !item.ok);
|
|
522
646
|
return {
|
|
523
|
-
ok:
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
nextSteps
|
|
647
|
+
ok: failed.length === 0,
|
|
648
|
+
mode: 'mock',
|
|
649
|
+
cases,
|
|
650
|
+
failedCases: failed,
|
|
651
|
+
capabilityProfile,
|
|
652
|
+
skillStatus
|
|
530
653
|
};
|
|
531
654
|
}
|
|
655
|
+
function handleSkillsCommand(positionals, flags) {
|
|
656
|
+
const subcommand = positionals[0] || 'status';
|
|
657
|
+
const skillsRoot = typeof flags.skillsRoot === 'string' ? flags.skillsRoot : undefined;
|
|
658
|
+
const sourceRoot = typeof flags.sourceRoot === 'string' ? flags.sourceRoot : undefined;
|
|
659
|
+
const targetIds = flags.all === true
|
|
660
|
+
? parseSkillTargets('all', skillsRoot)
|
|
661
|
+
: parseSkillTargets(typeof flags.targets === 'string' ? flags.targets : '', skillsRoot);
|
|
662
|
+
if (subcommand === 'status') {
|
|
663
|
+
return statusSkillTargets(targetIds, { skillsRoot, sourceRoot });
|
|
664
|
+
}
|
|
665
|
+
if (subcommand === 'update') {
|
|
666
|
+
return installSkillTargets(targetIds, { skillsRoot, sourceRoot });
|
|
667
|
+
}
|
|
668
|
+
if (subcommand === 'diff') {
|
|
669
|
+
const result = diffSkillTargets(targetIds, { skillsRoot, sourceRoot });
|
|
670
|
+
if (flags.format === 'text') {
|
|
671
|
+
return renderSkillDiffTextReport(result);
|
|
672
|
+
}
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
throw new CliError(`Unknown skills command: ${subcommand}`, 'UNKNOWN_SKILLS_COMMAND');
|
|
676
|
+
}
|
|
677
|
+
async function promptSkillTargets(session, skillsRoot) {
|
|
678
|
+
const defaults = defaultSkillTargetIds(skillsRoot);
|
|
679
|
+
const destinations = describeSkillTargets(defaults, skillsRoot);
|
|
680
|
+
process.stderr.write('\n[Optional] Install paper-search Skill\n');
|
|
681
|
+
process.stderr.write('Purpose: teach local AI agents to use paper-search CLI for literature tasks.\n');
|
|
682
|
+
process.stderr.write('Detected destinations:\n');
|
|
683
|
+
for (const item of destinations) {
|
|
684
|
+
process.stderr.write(` ${item.target.padEnd(12)} ${item.path}\n`);
|
|
685
|
+
}
|
|
686
|
+
const answer = await session.line(`Install targets [agents/codex/claude/cursor/gemini/antigravity/all/skip] (${defaults.join(',')}): `);
|
|
687
|
+
if (!answer.trim())
|
|
688
|
+
return defaults;
|
|
689
|
+
return parseSkillTargets(answer, skillsRoot);
|
|
690
|
+
}
|
|
532
691
|
function randomCommonEmail() {
|
|
533
692
|
return `paper.search.${randomBytes(6).toString('hex')}@gmail.com`;
|
|
534
693
|
}
|
|
@@ -630,11 +789,15 @@ async function main() {
|
|
|
630
789
|
try {
|
|
631
790
|
maybePrintSetupHint(parsed);
|
|
632
791
|
const result = await run(parsed);
|
|
633
|
-
|
|
634
|
-
|
|
792
|
+
const output = prepareOutput(result);
|
|
793
|
+
if (typeof output.exitCode === 'number') {
|
|
794
|
+
process.exitCode = output.exitCode;
|
|
795
|
+
}
|
|
796
|
+
if (typeof output.payload === 'string') {
|
|
797
|
+
process.stdout.write(`${output.payload}\n`);
|
|
635
798
|
}
|
|
636
799
|
else {
|
|
637
|
-
process.stdout.write(`${formatOutput(
|
|
800
|
+
process.stdout.write(`${formatOutput(output.payload, parsed.flags)}\n`);
|
|
638
801
|
}
|
|
639
802
|
}
|
|
640
803
|
catch (error) {
|
|
@@ -672,7 +835,7 @@ function maybePrintSetupHint(parsed) {
|
|
|
672
835
|
process.stderr.write([
|
|
673
836
|
'No Paper Search API credentials are configured yet.',
|
|
674
837
|
'Free metadata search still works, but body-snippet search and higher provider limits need optional keys.',
|
|
675
|
-
'Run "paper-search setup" to add keys, or "paper-search
|
|
838
|
+
'Run "paper-search setup" to add keys, or "paper-search doctor --pretty" to inspect config and capabilities.',
|
|
676
839
|
''
|
|
677
840
|
].join('\n'));
|
|
678
841
|
}
|