web-access-cli 0.1.0 → 0.3.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.
Files changed (41) hide show
  1. package/README.md +54 -55
  2. package/README_CN.md +55 -54
  3. package/THIRD_PARTY_NOTICES.md +15 -0
  4. package/dist/cli-output.js +18 -0
  5. package/dist/cli-output.js.map +1 -0
  6. package/dist/cli.js +44 -6
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config/config.js +76 -15
  9. package/dist/config/config.js.map +1 -1
  10. package/dist/config/edit.js +5 -94
  11. package/dist/config/edit.js.map +1 -1
  12. package/dist/config/file.js +97 -0
  13. package/dist/config/file.js.map +1 -0
  14. package/dist/config/provider-order.js +55 -0
  15. package/dist/config/provider-order.js.map +1 -0
  16. package/dist/core/diagnostics.js +7 -6
  17. package/dist/core/diagnostics.js.map +1 -1
  18. package/dist/core/errors.js.map +1 -1
  19. package/dist/core/provider-order.js +9 -0
  20. package/dist/core/provider-order.js.map +1 -0
  21. package/dist/core/router.js +157 -41
  22. package/dist/core/router.js.map +1 -1
  23. package/dist/core/schema.js +86 -10
  24. package/dist/core/schema.js.map +1 -1
  25. package/dist/core/types.js +6 -0
  26. package/dist/core/types.js.map +1 -1
  27. package/dist/providers/anysearch.js +15 -26
  28. package/dist/providers/anysearch.js.map +1 -1
  29. package/dist/providers/bocha.js +177 -0
  30. package/dist/providers/bocha.js.map +1 -0
  31. package/dist/providers/common.js.map +1 -1
  32. package/dist/providers/deepseek.js +140 -0
  33. package/dist/providers/deepseek.js.map +1 -0
  34. package/dist/providers/registry.js +4 -0
  35. package/dist/providers/registry.js.map +1 -1
  36. package/dist/transport/http.js +1 -0
  37. package/dist/transport/http.js.map +1 -1
  38. package/dist/version.js +1 -1
  39. package/package.json +66 -60
  40. package/schemas/config.schema.json +10 -0
  41. package/schemas/output.schema.json +1350 -331
package/README.md CHANGED
@@ -5,7 +5,7 @@ English | [简体中文](README_CN.md)
5
5
  [![CI](https://github.com/Juexe/web-access-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Juexe/web-access-cli/actions/workflows/ci.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
 
8
- An agent-neutral CLI for web capabilities. It defines search and web content extraction as stable capabilities while normalizing differences among Tavily, Exa, Brave, SearXNG, AnySearch, XCrawl, Firecrawl, Jina, and other providers behind a unified internal schema.
8
+ An agent-neutral CLI for web capabilities. It defines search and web content extraction as stable capabilities while normalizing differences among Tavily, Exa, Bocha, Brave, SearXNG, AnySearch, XCrawl, DeepSeek, Firecrawl, Jina, and other providers behind a unified internal schema.
9
9
 
10
10
  The CLI is the primary product interface. It is not tied to Pi, Claude Code, Codex, Cursor, OpenCode, or any other agent. Skills, MCP integrations, and agent plugins may only integrate as adapters on top of the CLI; they must not leak into the core capabilities or provider implementations.
11
11
 
@@ -13,35 +13,35 @@ The CLI is the primary product interface. It is not tied to Pi, Claude Code, Cod
13
13
 
14
14
  | Capability | Provider types | Normalized output |
15
15
  | --- | --- | --- |
16
- | `search` | Tavily, Exa, Brave, SearXNG, AnySearch, XCrawl | `rank`, `title`, `url`, `snippet` |
16
+ | `search` | Tavily, Exa, Bocha, Brave, SearXNG, AnySearch, XCrawl, DeepSeek | `rank`, `title`, `url`, `snippet` |
17
17
  | `extract` | Firecrawl v2, Jina Reader, Exa Contents, AnySearch, XCrawl, HTTP | Markdown `Document` |
18
18
 
19
- A provider type describes an implementation, while a provider instance is a configurable instance of that type. One type can have multiple instances, such as `exa_team` and `exa_personal`. A route is an ordered array of instance IDs that determines both which instances are enabled and the order in which `auto` tries them.
19
+ A provider type describes an implementation, while a provider instance is a configurable instance of that type. One type can have multiple instances, such as `exa_team` and `exa_personal`. A `providers` route is an ordered array of instance IDs that determines which instances are enabled and the initial `auto` order. The CLI stores the learned effective order in the sibling `_providers` field.
20
20
 
21
21
  ## Installation
22
22
 
23
23
  Node.js 22.19 or later is required.
24
24
 
25
- The npm package has not been published yet. Build and run the CLI from source:
25
+ Install the CLI from npm:
26
26
 
27
27
  ```sh
28
- git clone https://github.com/Juexe/web-access-cli.git
29
- cd web-access-cli
30
- pnpm install --frozen-lockfile
31
- pnpm build
32
- node dist/cli.js --help
28
+ npm install --global web-access-cli
29
+ web-access --help
33
30
  ```
34
31
 
35
- After building, an optional global installation can point to the absolute repository path:
32
+ Alternatively, install it with pnpm:
36
33
 
37
34
  ```sh
38
- pnpm add --global <absolute-path-to-web-access-cli>
35
+ pnpm add --global web-access-cli
39
36
  web-access --help
40
37
  ```
41
38
 
42
- For development in this repository:
39
+ For development from source:
43
40
 
44
41
  ```sh
42
+ git clone https://github.com/juexe/web-access-cli.git
43
+ cd web-access-cli
44
+ pnpm install --frozen-lockfile
45
45
  pnpm check
46
46
  pnpm pack:check
47
47
  ```
@@ -82,6 +82,7 @@ web-access search "browser automation" --include-domain example.com --exclude-do
82
82
 
83
83
  web-access extract https://example.com/article
84
84
  web-access extract https://example.com/article --provider http --timeout 30000
85
+ web-access extract https://example.com/article --json
85
86
 
86
87
  web-access providers --pretty
87
88
  web-access doctor --pretty
@@ -98,7 +99,7 @@ The CLI provides the `search` and `extract` capability commands, the `providers`
98
99
  - `--pretty`: Pretty-print JSON. By default, JSON is written on a single line.
99
100
  - `--help`, `--version`: Print standard CLI help or version text.
100
101
 
101
- Except for help and version output, stdout always contains exactly one JSON envelope. Diagnostic messages are never mixed into stdout.
102
+ Extract succeeds with Markdown by default. Its YAML front matter contains only the final Instance ID (`provider`), the returned document URL (`url`), and a non-empty trimmed title (`title`). Add `extract --json` when a schema v2 envelope is required; `--pretty` and `--debug` also select JSON. Extract failures, including `partial` results, always remain JSON envelopes. Search, diagnostics, and `config edit` continue to output JSON. Diagnostic messages are never mixed into stdout.
102
103
 
103
104
  ## Configuration
104
105
 
@@ -108,11 +109,13 @@ The default path on every platform is `~/.config/web-access-cli/config.json`, wh
108
109
 
109
110
  `web-access config edit` creates parent directories and a complete default configuration when the file is missing, then opens it with the operating system's default application for JSON files. An existing file is opened byte-for-byte as-is, even when it is temporarily invalid JSON; it is never overwritten or reformatted. The command waits only for the operating system to accept the open request, not for the editor to close. On success, envelope `data` contains the absolute `path`, `created`, and `opened: true`. A default-application launch failure returns `open_failed`; a configuration file that was created successfully remains available for manual editing.
110
111
 
112
+ `config edit` itself never rewrites existing content. After an `auto` capability call succeeds or encounters fallback-eligible failures, the CLI atomically updates `search._providers` or `extract._providers`. If the default configuration does not exist, the first `auto` call that produces a learning result creates the complete default configuration. `_providers` is CLI-managed and cannot enable an instance outside `providers`. Missing, malformed, duplicate, unknown, or membership-mismatched internal entries reset the whole capability to the declared route. Delete `_providers` to restore the user-declared initial order manually.
113
+
111
114
  The complete JSON Schema is available at [schemas/config.schema.json](schemas/config.schema.json). Example:
112
115
 
113
116
  ```json
114
117
  {
115
- "$schema": "https://unpkg.com/web-access-cli@0.1.0/schemas/config.schema.json",
118
+ "$schema": "https://unpkg.com/web-access-cli@0.3.0/schemas/config.schema.json",
116
119
  "providers": [
117
120
  {
118
121
  "id": "exa_team",
@@ -150,14 +153,16 @@ The complete JSON Schema is available at [schemas/config.schema.json](schemas/co
150
153
  }
151
154
  ```
152
155
 
153
- The built-in instances are `tavily`, `exa`, `brave`, `searxng`, `firecrawl`, `jina`, `http`, `anysearch`, and `xcrawl`. A configuration entry with the same ID overrides fields on the built-in instance. A custom ID creates another instance of the selected type. An instance is enabled only when it appears in the corresponding `providers` route. AnySearch defaults to `https://api.anysearch.com` and supports anonymous calls; XCrawl defaults to `https://run.xcrawl.com` and requires an API key.
156
+ The built-in instances are `tavily`, `exa`, `bocha`, `brave`, `searxng`, `firecrawl`, `jina`, `http`, `anysearch`, `xcrawl`, and `deepseek`. A configuration entry with the same ID overrides fields on the built-in instance. A custom ID creates another instance of the selected type. An instance is enabled only when it appears in the corresponding `providers` route. Bocha defaults to `https://api.bocha.cn` and requires an API key; AnySearch defaults to `https://api.anysearch.com` and supports anonymous calls; XCrawl defaults to `https://run.xcrawl.com` and requires an API key; DeepSeek defaults to `https://api.deepseek.com/anthropic/v1` and requires an API key.
154
157
 
155
158
  Default routes:
156
159
 
157
- - Search: `tavily -> exa -> brave -> searxng -> anysearch -> xcrawl`
160
+ - Search: `tavily -> exa -> bocha -> brave -> searxng -> anysearch -> xcrawl -> deepseek`
158
161
  - Extract: `firecrawl -> jina -> exa -> anysearch -> xcrawl -> http`
159
162
 
160
- The default routes include every built-in instance that supports the corresponding capability. Custom instance IDs are merged into the instance list but must still be added to routes explicitly. An omitted route uses the defaults above, while an explicit empty array disables that capability. In `auto` mode, incompletely configured instances are skipped; AnySearch can be called anonymously with its default base URL, and XCrawl is skipped until an API key is configured. Both instance types accept `searchFilterMode`: `strict` (default; freshness skips the provider) or `best_effort` (rewrites freshness into a query fragment). Domain constraints are rewritten into the query and strictly re-applied locally. XCrawl Extract always uses synchronous Scrape with Markdown output; Map, Crawl, and asynchronous jobs are outside the current CLI capabilities.
163
+ The default routes include every built-in instance that supports the corresponding capability. Custom instance IDs are merged into the instance list but must still be added to routes explicitly. An omitted route uses the defaults above, while an explicit empty array disables that capability. In `auto` mode, incompletely configured instances are skipped; AnySearch can be called anonymously with its default base URL, while XCrawl and DeepSeek are skipped until their API keys are configured. AnySearch and XCrawl accept `searchFilterMode`: `strict` (default; freshness skips the provider) or `best_effort` (rewrites freshness into a query fragment). Domain constraints are rewritten into the query and strictly re-applied locally. XCrawl Extract always uses synchronous Scrape with Markdown output; Map, Crawl, and asynchronous jobs are outside the current CLI capabilities.
164
+
165
+ DeepSeek Search performs a full Anthropic-compatible Messages model turn with the native `web_search_20250305` server tool, so it can have higher latency and cost than a dedicated search endpoint. It is last in the default route and is reached only after earlier providers are unavailable, return a final non-2xx HTTP response, or otherwise fail recoverably. The adapter accepts URLs only from structured `web_search_tool_result` blocks, joins citation excerpts by URL, and never extracts URLs from model prose. Domain constraints are rewritten into the query and strictly re-applied locally; `freshness` is unsupported and skips DeepSeek with a recoverable error. Redirects are rejected without contacting the `Location` target. Provider-private `encrypted_content` payloads are omitted from `raw` because they are opaque, cannot be displayed or decoded by the CLI, and add no diagnostic value.
161
166
 
162
167
  ### Credentials and URLs
163
168
 
@@ -167,6 +172,7 @@ Environment variables take precedence over plaintext keys in JSON. Built-in inst
167
172
  | --- | --- | --- |
168
173
  | Tavily | `TAVILY_API_KEY` | `TAVILY_BASE_URL` |
169
174
  | Exa | `EXA_API_KEY` | `EXA_BASE_URL` |
175
+ | Bocha | `BOCHA_API_KEY` | `BOCHA_BASE_URL`, default `https://api.bocha.cn` |
170
176
  | Brave | `BRAVE_API_KEY` | `BRAVE_BASE_URL` |
171
177
  | SearXNG | None | `SEARXNG_BASE_URL`, required |
172
178
  | Firecrawl | `FIRECRAWL_API_KEY` | `FIRECRAWL_BASE_URL` |
@@ -174,17 +180,23 @@ Environment variables take precedence over plaintext keys in JSON. Built-in inst
174
180
  | HTTP | None | None |
175
181
  | AnySearch | `ANYSEARCH_API_KEY`, optional | `ANYSEARCH_BASE_URL`, default `https://api.anysearch.com` |
176
182
  | XCrawl | `XCRAWL_API_KEY` | `XCRAWL_BASE_URL`, default `https://run.xcrawl.com` |
183
+ | DeepSeek | `DEEPSEEK_API_KEY` | No standard environment variable; default `https://api.deepseek.com/anthropic/v1` |
177
184
 
178
185
  Custom instances use `apiKeyEnv` and `baseUrlEnv` to name their environment variables. Every remote provider can define a `baseUrl` and additional `headers`. The public `api.firecrawl.dev` service requires a key; a self-hosted Firecrawl v2 instance can run without one.
179
186
 
180
187
  ## Execution and fallback
181
188
 
182
- `--provider auto` follows the configured route order:
189
+ `--provider auto` follows the effective `_providers` order, or starts from `providers` when the internal field is absent or invalid:
183
190
 
184
191
  1. An instance with incomplete configuration is recorded as a failed attempt and skipped.
185
- 2. Recoverable errors, including network errors, timeouts, rate limits, 5xx responses, oversized responses, and missing usable content, cause the router to try the next instance.
186
- 3. Non-recoverable errors, including authentication errors, invalid input, and unsupported content, stop execution immediately.
187
- 4. If every instance fails, the command returns `provider_exhausted`. The best short content produced during extraction is preserved in `partial`.
192
+ 2. A final provider HTTP response outside 2xx, including authentication, request, rate-limit, and server errors, causes the router to try the next instance.
193
+ 3. Other recoverable errors, including network errors, timeouts, oversized responses, and missing usable content, also cause the router to continue.
194
+ 4. Non-recoverable failures without a final non-2xx response, including invalid input and unsupported content detected in a successful response, stop execution immediately.
195
+ 5. If every instance fails, the command returns `provider_exhausted`. The best short content produced during extraction is preserved in `partial`.
196
+
197
+ After each `auto` call, a successful instance moves to the front, untried instances remain in the middle, and instances with the fallback-eligible failures above move stably to the back. Relative order within each group is preserved. The order stays unchanged when every instance fails. Search and Extract learn independently; explicit instances, non-fallback failures, and user cancellation never update the order. Writes use atomic replacement, and the last writer wins across concurrent processes.
198
+
199
+ An attempt keeps the provider's original error code, HTTP status, and `retryable` value. `retryable` describes whether the original operation can be retried against the same provider; it is not the sole condition for switching to the next provider in an automatic route.
188
200
 
189
201
  Selecting an instance explicitly executes it strictly without fallback. If the instance is not in the route, the command returns `provider_disabled`.
190
202
 
@@ -192,39 +204,13 @@ Default limits are a 60-second total timeout and a 20-second per-attempt timeout
192
204
 
193
205
  ## Output contract
194
206
 
195
- Successful response example:
207
+ Capability commands use output schema version 2. Their default envelope is intentionally small:
196
208
 
197
209
  ```json
198
210
  {
199
- "schemaVersion": 1,
211
+ "schemaVersion": 2,
200
212
  "ok": true,
201
- "command": "search",
202
- "durationMs": 183,
203
- "request": {
204
- "query": "Agent-neutral web CLI",
205
- "provider": "auto",
206
- "limit": 5,
207
- "includeDomains": [],
208
- "excludeDomains": []
209
- },
210
- "provider": { "id": "exa", "type": "exa" },
211
- "attempts": [
212
- {
213
- "provider": { "id": "tavily", "type": "tavily" },
214
- "status": "failed",
215
- "durationMs": 0,
216
- "error": {
217
- "code": "provider_unavailable",
218
- "message": "tavily 未完成 search 所需配置",
219
- "retryable": true
220
- }
221
- },
222
- {
223
- "provider": { "id": "exa", "type": "exa" },
224
- "status": "success",
225
- "durationMs": 183
226
- }
227
- ],
213
+ "provider": "exa",
228
214
  "data": {
229
215
  "results": [
230
216
  {
@@ -234,14 +220,27 @@ Successful response example:
234
220
  "snippet": "Example snippet"
235
221
  }
236
222
  ]
237
- },
238
- "raw": {}
223
+ }
239
224
  }
240
225
  ```
241
226
 
242
- The Chinese `message` value above matches the CLI's current error output contract.
227
+ Default capability output contains only `schemaVersion`, `ok`, the final Instance ID and normalized `data`. Failures contain a compact `error` and, when providers were attempted, `attempts` entries with only Instance ID, error code and optional HTTP status. Extract quality failures may include a `partial` document without raw provider data. If the learned order cannot be written, either a success or failure envelope additionally contains `warnings: [{ "code": "provider_order_update_failed", "message": "..." }]` without replacing the primary result. Search and extract input errors use the same compact failure shape.
228
+
229
+ For human-readable extraction, the default Markdown output starts with YAML front matter and then the provider-normalized Markdown body. Metadata values use JSON double-quoted scalar encoding and the body is not modified. Existing scripts and other machine consumers should append `--json` explicitly.
230
+
231
+ Example default extract output:
232
+
233
+ ```markdown
234
+ ---
235
+ provider: "local_http"
236
+ url: "https://example.com/article"
237
+ title: "Example title"
238
+ ---
239
+
240
+ # Article content
241
+ ```
243
242
 
244
- `raw` contains only the response body from the final success or best failure. It never contains requests, response headers, or every historical response. API keys are redacted from serializable `raw` data and error messages. Stable schemas are available in [schemas](schemas).
243
+ Use `--debug` on `search` or `extract` only for protocol troubleshooting. It adds a nested `debug` object containing the request, durations, complete attempts and the final or best-failure raw response. Raw data remains recursively redacted and is not part of normal Agent calls. `providers`, `doctor`, and `config edit` retain their detailed diagnostic envelopes; all envelopes use schema version 2. Stable schemas are available in [schemas](schemas).
245
244
 
246
245
  Exit codes:
247
246
 
@@ -250,7 +249,7 @@ Exit codes:
250
249
  - `1`: Runtime, provider, doctor, or default-application launch failure
251
250
  - `130`: User cancellation
252
251
 
253
- `providers` lists each instance's type, capabilities, route status, credential source, and base URL source. `doctor` performs local configuration checks only and does not call remote APIs. If an enabled route contains an unconfigured instance, the command returns `doctor_failed` with exit code `1`.
252
+ `providers` lists each instance's type, capabilities, route status, credential source, and base URL source. Its `searchRoute` and `extractRoute` fields show the effective order for the next `auto` call. `doctor` performs local configuration checks only and does not call remote APIs. If an enabled route contains an unconfigured instance, the command returns `doctor_failed` with exit code `1`.
254
253
 
255
254
  ## HTTP and security boundaries
256
255
 
package/README_CN.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![CI](https://github.com/Juexe/web-access-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Juexe/web-access-cli/actions/workflows/ci.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
 
8
- 一个 Agent-neutral 的网页能力 CLI。它把“搜索”和“网页正文提取”定义为稳定能力,把 Tavily、Exa、Brave、SearXNG、AnySearch、XCrawl、Firecrawl、Jina 等差异收敛到内部统一 schema。
8
+ 一个 Agent-neutral 的网页能力 CLI。它把“搜索”和“网页正文提取”定义为稳定能力,把 Tavily、Exa、Bocha、Brave、SearXNG、AnySearch、XCrawl、DeepSeek、Firecrawl、Jina 等差异收敛到内部统一 schema。
9
9
 
10
10
  CLI 是主要产品形态,不绑定 Pi、Claude Code、Codex、Cursor、OpenCode 或其他 Agent。Skill、MCP 和 Agent 插件只能作为 CLI 上层 adapter 接入,不能污染核心能力和 Provider 实现。
11
11
 
@@ -13,35 +13,35 @@ CLI 是主要产品形态,不绑定 Pi、Claude Code、Codex、Cursor、OpenCo
13
13
 
14
14
  | 能力 | Provider Type | 统一输出 |
15
15
  | --- | --- | --- |
16
- | `search` | Tavily、Exa、Brave、SearXNG、AnySearch、XCrawl | `rank`、`title`、`url`、`snippet` |
16
+ | `search` | Tavily、Exa、Bocha、Brave、SearXNG、AnySearch、XCrawl、DeepSeek | `rank`、`title`、`url`、`snippet` |
17
17
  | `extract` | Firecrawl v2、Jina Reader、Exa Contents、AnySearch、XCrawl、HTTP | Markdown `Document` |
18
18
 
19
- Provider Type 描述实现类型;Provider Instance 是一份可配置实例。一个 Type 可以有多个 Instance,例如 `exa_team` 和 `exa_personal`。Route 是有序 Instance ID 数组,既决定启用状态,也决定 `auto` 的尝试顺序。
19
+ Provider Type 描述实现类型;Provider Instance 是一份可配置实例。一个 Type 可以有多个 Instance,例如 `exa_team` 和 `exa_personal`。`providers` Route 是有序 Instance ID 数组,决定启用状态和 `auto` 的初始顺序;CLI 会把学习后的实际顺序保存在同级 `_providers`。
20
20
 
21
21
  ## 安装
22
22
 
23
23
  要求 Node.js 22.19 或更高版本。
24
24
 
25
- npm 包尚未发布。当前请从源码构建并运行:
25
+ 从 npm 全局安装:
26
26
 
27
27
  ```sh
28
- git clone https://github.com/Juexe/web-access-cli.git
29
- cd web-access-cli
30
- pnpm install --frozen-lockfile
31
- pnpm build
32
- node dist/cli.js --help
28
+ npm install --global web-access-cli
29
+ web-access --help
33
30
  ```
34
31
 
35
- 构建完成后,也可以使用仓库绝对路径安装为全局命令:
32
+ 也可以使用 pnpm 安装:
36
33
 
37
34
  ```sh
38
- pnpm add --global <web-access-cli 仓库绝对路径>
35
+ pnpm add --global web-access-cli
39
36
  web-access --help
40
37
  ```
41
38
 
42
- 在仓库中开发:
39
+ 从源码开发:
43
40
 
44
41
  ```sh
42
+ git clone https://github.com/juexe/web-access-cli.git
43
+ cd web-access-cli
44
+ pnpm install --frozen-lockfile
45
45
  pnpm check
46
46
  pnpm pack:check
47
47
  ```
@@ -82,6 +82,7 @@ web-access search "browser automation" --include-domain example.com --exclude-do
82
82
 
83
83
  web-access extract https://example.com/article
84
84
  web-access extract https://example.com/article --provider http --timeout 30000
85
+ web-access extract https://example.com/article --json
85
86
 
86
87
  web-access providers --pretty
87
88
  web-access doctor --pretty
@@ -98,7 +99,7 @@ CLI 提供 `search`、`extract` 两个能力命令,`providers`、`doctor` 两
98
99
  - `--pretty`:格式化 JSON;默认输出单行 JSON。
99
100
  - `--help`、`--version`:输出常规 CLI 帮助或版本文本。
100
101
 
101
- 除帮助与版本外,命令的 stdout 始终只有一个 JSON envelope。诊断信息不会混入 stdout。
102
+ `extract` 成功时默认输出 Markdown,文首 YAML front matter 只包含最终 Instance ID(`provider`)、Provider 返回的文档 URL(`url`)和非空的去除首尾空白标题(`title`)。需要 schema v2 envelope 时显式追加 `extract --json`;`--pretty` 和 `--debug` 也会选择 JSON。提取失败(包括 `partial`)始终输出 JSON envelope。`search`、诊断命令和 `config edit` 继续输出 JSON。诊断信息不会混入 stdout。
102
103
 
103
104
  ## 配置
104
105
 
@@ -108,11 +109,13 @@ CLI 提供 `search`、`extract` 两个能力命令,`providers`、`doctor` 两
108
109
 
109
110
  `web-access config edit` 会在配置文件缺失时创建父目录和完整默认配置,再用系统为 JSON 文件关联的默认应用打开;已有文件会原样打开,即使内容暂时不是有效 JSON 也不会被覆盖或格式化。命令只等待系统接受打开请求,不等待编辑器关闭。成功 envelope 的 `data` 包含绝对 `path`、是否新建的 `created` 和 `opened: true`。无法启动默认应用时返回 `open_failed`;如果配置刚刚创建成功,文件仍会保留以便手动打开。
110
111
 
112
+ `config edit` 本身不会改写已有内容;能力命令使用 `auto` 成功或发生可回退失败后,会原子更新 `search._providers` 或 `extract._providers`。默认配置尚不存在时,首次产生学习结果的 `auto` 调用会创建完整默认配置。`_providers` 由 CLI 管理,不会启用 `providers` 之外的 Instance;其内容缺失、格式错误、重复、引用未知 ID 或与 `providers` 成员集合不一致时会整组重置。删除 `_providers` 可以手动恢复用户声明的初始顺序。
113
+
111
114
  完整 JSON Schema 位于 [schemas/config.schema.json](schemas/config.schema.json)。示例:
112
115
 
113
116
  ```json
114
117
  {
115
- "$schema": "https://unpkg.com/web-access-cli@0.1.0/schemas/config.schema.json",
118
+ "$schema": "https://unpkg.com/web-access-cli@0.3.0/schemas/config.schema.json",
116
119
  "providers": [
117
120
  {
118
121
  "id": "exa_team",
@@ -150,14 +153,16 @@ CLI 提供 `search`、`extract` 两个能力命令,`providers`、`doctor` 两
150
153
  }
151
154
  ```
152
155
 
153
- 内置 Instance 为 `tavily`、`exa`、`brave`、`searxng`、`firecrawl`、`jina`、`http`、`anysearch`、`xcrawl`。配置同 ID 时会覆盖内置实例的字段;自定义 ID 可以创建同 Type 的额外实例。只有出现在对应 `providers` Route 中的实例才启用。AnySearch 默认 base URL 为 `https://api.anysearch.com`,支持匿名调用;XCrawl 默认 base URL 为 `https://run.xcrawl.com`,必须配置 API key。
156
+ 内置 Instance 为 `tavily`、`exa`、`bocha`、`brave`、`searxng`、`firecrawl`、`jina`、`http`、`anysearch`、`xcrawl`、`deepseek`。配置同 ID 时会覆盖内置实例的字段;自定义 ID 可以创建同 Type 的额外实例。只有出现在对应 `providers` Route 中的实例才启用。Bocha 默认 base URL 为 `https://api.bocha.cn`,必须配置 API key;AnySearch 默认 base URL 为 `https://api.anysearch.com`,支持匿名调用;XCrawl 默认 base URL 为 `https://run.xcrawl.com`,必须配置 API key;DeepSeek 默认 base URL 为 `https://api.deepseek.com/anthropic/v1`,必须配置 API key。
154
157
 
155
158
  默认 Route:
156
159
 
157
- - Search:`tavily -> exa -> brave -> searxng -> anysearch -> xcrawl`
160
+ - Search:`tavily -> exa -> bocha -> brave -> searxng -> anysearch -> xcrawl -> deepseek`
158
161
  - Extract:`firecrawl -> jina -> exa -> anysearch -> xcrawl -> http`
159
162
 
160
- 默认 Route 包含支持对应 Capability 的全部内置 Instance。自定义 ID 会合并到 Instance 列表,但仍需显式加入 Route;省略 Route 时使用上述默认值,显式空数组则禁用对应能力。`auto` 会跳过未完成配置的 Instance;AnySearch 使用默认 base URL 时可匿名调用,XCrawl 则在配置 API key 前被跳过。两类 Instance 均可设置 `searchFilterMode`:`strict`(默认,遇到 freshness 时跳过)或 `best_effort`(将日期改写为查询片段)。域名条件会改写查询并在本地再次严格过滤。XCrawl Extract 固定使用同步 Scrape 的 Markdown 输出;Map、Crawl 和异步任务不属于当前 CLI 能力。
163
+ 默认 Route 包含支持对应 Capability 的全部内置 Instance。自定义 ID 会合并到 Instance 列表,但仍需显式加入 Route;省略 Route 时使用上述默认值,显式空数组则禁用对应能力。`auto` 会跳过未完成配置的 Instance;AnySearch 使用默认 base URL 时可匿名调用,XCrawl 和 DeepSeek 则在配置 API key 前被跳过。AnySearch 与 XCrawl 可设置 `searchFilterMode`:`strict`(默认,遇到 freshness 时跳过)或 `best_effort`(将日期改写为查询片段)。域名条件会改写查询并在本地再次严格过滤。XCrawl Extract 固定使用同步 Scrape 的 Markdown 输出;Map、Crawl 和异步任务不属于当前 CLI 能力。
164
+
165
+ DeepSeek Search 通过 Anthropic-compatible Messages API 调用原生 `web_search_20250305` server tool,一次搜索是完整模型轮次,因此延迟和成本可能高于专用搜索 endpoint。它位于默认 Route 末尾,仅在前序 Provider 未配置、返回最终非 2xx HTTP 响应或发生其他可恢复失败后触发。Adapter 只接受 `web_search_tool_result` 中的结构化 URL,按 URL 合并 citation 摘要,绝不从模型 prose 中猜测 URL。域名条件会改写查询并在本地再次严格过滤;DeepSeek 不支持 `freshness`,遇到该参数时以可恢复错误跳过;重定向会被严格拒绝,且不会访问 `Location` 目标。Provider 私有的 `encrypted_content` 是 CLI 无法展示或解码的 opaque payload,不具备诊断价值,因此会从 `raw` 中移除。
161
166
 
162
167
  ### 凭据和 URL
163
168
 
@@ -167,6 +172,7 @@ CLI 提供 `search`、`extract` 两个能力命令,`providers`、`doctor` 两
167
172
  | --- | --- | --- |
168
173
  | Tavily | `TAVILY_API_KEY` | `TAVILY_BASE_URL` |
169
174
  | Exa | `EXA_API_KEY` | `EXA_BASE_URL` |
175
+ | Bocha | `BOCHA_API_KEY` | `BOCHA_BASE_URL`,默认 `https://api.bocha.cn` |
170
176
  | Brave | `BRAVE_API_KEY` | `BRAVE_BASE_URL` |
171
177
  | SearXNG | 无 | `SEARXNG_BASE_URL`,必需 |
172
178
  | Firecrawl | `FIRECRAWL_API_KEY` | `FIRECRAWL_BASE_URL` |
@@ -174,17 +180,23 @@ CLI 提供 `search`、`extract` 两个能力命令,`providers`、`doctor` 两
174
180
  | HTTP | 无 | 无 |
175
181
  | AnySearch | `ANYSEARCH_API_KEY`,可选 | `ANYSEARCH_BASE_URL`,默认 `https://api.anysearch.com` |
176
182
  | XCrawl | `XCRAWL_API_KEY` | `XCRAWL_BASE_URL`,默认 `https://run.xcrawl.com` |
183
+ | DeepSeek | `DEEPSEEK_API_KEY` | 无标准环境变量;默认 `https://api.deepseek.com/anthropic/v1` |
177
184
 
178
185
  自定义 Instance 使用 `apiKeyEnv` 和 `baseUrlEnv` 指定自己的环境变量。所有远端 Provider 都可以设置 `baseUrl` 和附加 `headers`。公共 `api.firecrawl.dev` 需要 key;自托管 Firecrawl v2 可以不设置 key。
179
186
 
180
187
  ## 执行与回退
181
188
 
182
- `--provider auto` 按 Route 顺序执行:
189
+ `--provider auto` 按 `_providers` 的有效顺序执行;字段不存在或无效时从 `providers` 开始:
183
190
 
184
191
  1. 未完成配置的实例会记录为失败 attempt 并跳过。
185
- 2. 网络错误、超时、限流、5xx、响应过大、无可用正文等可恢复错误会尝试下一个实例。
186
- 3. 鉴权错误、无效输入、不支持的内容等不可恢复错误会立即停止。
187
- 4. 所有实例失败时返回 `provider_exhausted`。提取过程中产生的最佳短正文会保留在 `partial`。
192
+ 2. Provider 返回的最终 HTTP 响应不在 2xx 范围时,包括鉴权、请求、限流和服务器错误,Router 会尝试下一个实例。
193
+ 3. 网络错误、超时、响应过大、无可用正文等其他可恢复错误也会继续 Route。
194
+ 4. 没有最终非 2xx 响应的不可恢复错误,例如无效输入或在成功响应中检测到不支持的内容,会立即停止。
195
+ 5. 所有实例失败时返回 `provider_exhausted`。提取过程中产生的最佳短正文会保留在 `partial`。
196
+
197
+ 每次 `auto` 调用完成后,成功 Instance 移到队头,未尝试 Instance 保持在中间,发生上述可回退失败的 Instance 稳定移到队尾;三组内部顺序不变。所有 Instance 都失败时顺序不变。Search 与 Extract 独立学习;显式 Instance、不可回退错误和用户取消不会更新顺序。写入采用原子替换,并发进程由最后写入者生效。
198
+
199
+ Attempt 会保留 Provider 的原始错误码、HTTP status 和 `retryable` 值。`retryable` 表示原始操作是否适合对同一 Provider 重试,不再是自动 Route 切换到下一 Provider 的唯一条件。
188
200
 
189
201
  显式指定 Instance 时严格执行,不触发 fallback。若实例不在 Route 中,返回 `provider_disabled`。
190
202
 
@@ -192,39 +204,13 @@ CLI 提供 `search`、`extract` 两个能力命令,`providers`、`doctor` 两
192
204
 
193
205
  ## 输出契约
194
206
 
195
- 成功示例:
207
+ 能力命令使用 output schema v2,默认 envelope 刻意保持精简:
196
208
 
197
209
  ```json
198
210
  {
199
- "schemaVersion": 1,
211
+ "schemaVersion": 2,
200
212
  "ok": true,
201
- "command": "search",
202
- "durationMs": 183,
203
- "request": {
204
- "query": "Agent-neutral web CLI",
205
- "provider": "auto",
206
- "limit": 5,
207
- "includeDomains": [],
208
- "excludeDomains": []
209
- },
210
- "provider": { "id": "exa", "type": "exa" },
211
- "attempts": [
212
- {
213
- "provider": { "id": "tavily", "type": "tavily" },
214
- "status": "failed",
215
- "durationMs": 0,
216
- "error": {
217
- "code": "provider_unavailable",
218
- "message": "tavily 未完成 search 所需配置",
219
- "retryable": true
220
- }
221
- },
222
- {
223
- "provider": { "id": "exa", "type": "exa" },
224
- "status": "success",
225
- "durationMs": 183
226
- }
227
- ],
213
+ "provider": "exa",
228
214
  "data": {
229
215
  "results": [
230
216
  {
@@ -234,12 +220,27 @@ CLI 提供 `search`、`extract` 两个能力命令,`providers`、`doctor` 两
234
220
  "snippet": "Example snippet"
235
221
  }
236
222
  ]
237
- },
238
- "raw": {}
223
+ }
239
224
  }
240
225
  ```
241
226
 
242
- `raw` 只包含最终成功或最佳失败的 response body,不包含请求、响应 headers 或所有历史响应。API key 会从可序列化的 `raw` 和错误消息中脱敏。稳定 schema 位于 [schemas](schemas)。
227
+ 能力命令成功时默认只有 `schemaVersion`、`ok`、最终 Instance ID 和规范化 `data`。失败时包含精简 `error`;实际尝试过 Provider 时,`attempts` 只保留 Instance ID、错误码和可选 HTTP status。提取质量失败可包含不含 raw 的 `partial` 文档。排序无法写回时,成功或失败 envelope 会额外包含 `warnings: [{ "code": "provider_order_update_failed", "message": "..." }]`,但不会覆盖主要结果。搜索和提取输入错误也使用相同的精简失败结构。
228
+
229
+ 面向人的提取默认输出以 YAML front matter 开头、随后是 Provider 规范化的 Markdown 正文。元数据值统一使用 JSON 双引号标量编码,正文内容不改写。已有脚本和其他机器消费者应显式追加 `--json`。
230
+
231
+ 默认提取输出示例:
232
+
233
+ ```markdown
234
+ ---
235
+ provider: "local_http"
236
+ url: "https://example.com/article"
237
+ title: "Example title"
238
+ ---
239
+
240
+ # Article content
241
+ ```
242
+
243
+ 只有协议排障时才在 `search` 或 `extract` 上使用 `--debug`。该选项会增加嵌套 `debug`,其中包含 request、耗时、完整 attempts 以及最终成功或最佳失败的 raw 响应;raw 仍会递归脱敏,常规 Agent 调用不应使用此选项。`providers`、`doctor` 和 `config edit` 保留详细诊断 envelope;所有 envelope 使用 schema version 2。稳定 schema 位于 [schemas](schemas)。
243
244
 
244
245
  退出码:
245
246
 
@@ -248,7 +249,7 @@ CLI 提供 `search`、`extract` 两个能力命令,`providers`、`doctor` 两
248
249
  - `1`:运行时/provider/doctor/默认应用启动失败
249
250
  - `130`:用户取消
250
251
 
251
- `providers` 会列出每个 Instance 的 Type、能力、Route 启用状态、凭据来源和 base URL 来源。`doctor` 只做本地配置检查,不主动调用远端 API;当已启用 Route 中存在未配置 Instance 时,命令返回 `doctor_failed` 和退出码 `1`。
252
+ `providers` 会列出每个 Instance 的 Type、能力、Route 启用状态、凭据来源和 base URL 来源;`searchRoute` 与 `extractRoute` 是下一次 `auto` 使用的有效顺序。`doctor` 只做本地配置检查,不主动调用远端 API;当已启用 Route 中存在未配置 Instance 时,命令返回 `doctor_failed` 和退出码 `1`。
252
253
 
253
254
  ## HTTP 与安全边界
254
255
 
@@ -33,16 +33,31 @@
33
33
 
34
34
  Pi-specific 的工具注册、UI、Agent 生命周期与协议代码没有进入本项目。
35
35
 
36
+ ## DeepSeek Harness web-search-deepseek
37
+
38
+ DeepSeek Search Provider 的 Anthropic-compatible Messages wire shape、结构化结果解析与 citation 映射参考并改写自:
39
+
40
+ - 项目:`deepseek-harness`
41
+ - 仓库:`https://github.com/deepseek-ai/deepseek-harness.git`
42
+ - 上游路径:`packages/web/web-search-deepseek`
43
+ - 参考提交:`99f6f02fecdb7dff40c3fbc9470f5907c29f74ca`
44
+ - Copyright 2026 DeepSeek
45
+ - 许可证:MIT(适用的许可文本与上文所列 MIT 文本相同)
46
+
47
+ 动态 Settings、Credential Service、Session event、LLM seam 和 Agent 集成代码没有进入本项目。
48
+
36
49
  ## npm 依赖
37
50
 
38
51
  发行包直接使用以下开源组件:
39
52
 
40
53
  - `@mozilla/readability`:Apache-2.0
41
54
  - `commander`:MIT
55
+ - `jsonc-parser`:MIT
42
56
  - `linkedom`:ISC
43
57
  - `open`:MIT
44
58
  - `turndown`:MIT
45
59
  - `typebox`:MIT
46
60
  - `undici`:MIT
61
+ - `write-file-atomic`:ISC
47
62
 
48
63
  开发依赖包括 Biome、tsx、TypeScript 及对应类型包。各组件的完整许可证和版权声明以随 npm 包发布的许可证文件及其上游仓库为准。本文件不替代任何第三方许可证。
@@ -0,0 +1,18 @@
1
+ function yamlScalar(value) {
2
+ return JSON.stringify(value);
3
+ }
4
+ export function formatExtractMarkdown(envelope) {
5
+ const { document } = envelope.data;
6
+ const lines = [
7
+ "---",
8
+ `provider: ${yamlScalar(envelope.provider)}`,
9
+ `url: ${yamlScalar(document.sourceUrl)}`,
10
+ ];
11
+ const title = document.title.trim();
12
+ if (title)
13
+ lines.push(`title: ${yamlScalar(title)}`);
14
+ lines.push("---", "", "");
15
+ const content = document.content;
16
+ return `${lines.join("\n")}${content.endsWith("\n") ? content : `${content}\n`}`;
17
+ }
18
+ //# sourceMappingURL=cli-output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-output.js","sourceRoot":"","sources":["../src/cli-output.ts"],"names":[],"mappings":"AAIA,SAAS,UAAU,CAAC,KAAa;IAChC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,qBAAqB,CACpC,QAAgC;IAEhC,MAAM,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC;IACnC,MAAM,KAAK,GAAG;QACb,KAAK;QACL,aAAa,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAC5C,QAAQ,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE;KACxC,CAAC;IACF,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACpC,IAAI,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrD,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC1B,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IACjC,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,EAAE,CAAC;AAClF,CAAC"}
package/dist/cli.js CHANGED
@@ -2,8 +2,10 @@
2
2
  import { realpathSync } from "node:fs";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { Command, CommanderError, InvalidArgumentError, Option, } from "commander";
5
+ import { formatExtractMarkdown } from "./cli-output.js";
5
6
  import { loadConfig } from "./config/config.js";
6
7
  import { executeConfigEdit } from "./config/edit.js";
8
+ import { createProviderOrderWriter } from "./config/provider-order.js";
7
9
  import { executeDoctor, executeProviders } from "./core/diagnostics.js";
8
10
  import { asWebAccessError, WebAccessError } from "./core/errors.js";
9
11
  import { errorEnvelope, executeExtract, executeSearch } from "./core/router.js";
@@ -48,6 +50,32 @@ function exitCode(envelope) {
48
50
  function writeEnvelope(envelope, pretty) {
49
51
  process.stdout.write(`${JSON.stringify(envelope, null, pretty ? 2 : undefined)}\n`);
50
52
  }
53
+ function writeOutput(envelope, mode, pretty) {
54
+ if (mode === "markdown" && isExtractSuccessEnvelope(envelope)) {
55
+ process.stdout.write(formatExtractMarkdown(envelope));
56
+ return;
57
+ }
58
+ writeEnvelope(envelope, pretty);
59
+ }
60
+ function isExtractSuccessEnvelope(envelope) {
61
+ return (envelope.ok &&
62
+ "provider" in envelope &&
63
+ typeof envelope.provider === "string" &&
64
+ "data" in envelope &&
65
+ typeof envelope.data === "object" &&
66
+ envelope.data !== null &&
67
+ "document" in envelope.data &&
68
+ typeof envelope.data.document === "object" &&
69
+ envelope.data.document !== null &&
70
+ "sourceUrl" in envelope.data.document &&
71
+ typeof envelope.data.document.sourceUrl === "string" &&
72
+ "title" in envelope.data.document &&
73
+ typeof envelope.data.document.title === "string" &&
74
+ "content" in envelope.data.document &&
75
+ typeof envelope.data.document.content === "string" &&
76
+ "contentType" in envelope.data.document &&
77
+ envelope.data.document.contentType === "text/markdown");
78
+ }
51
79
  export function createProgram(run, dependencies = {}) {
52
80
  const program = new Command();
53
81
  const runConfigEdit = dependencies.executeConfigEdit ?? executeConfigEdit;
@@ -56,7 +84,7 @@ export function createProgram(run, dependencies = {}) {
56
84
  .description("Agent-neutral 的网页搜索与内容提取 CLI")
57
85
  .version(VERSION)
58
86
  .option("--config <path>", "指定 JSON 配置文件")
59
- .option("--pretty", "格式化 JSON 输出", false)
87
+ .option("--pretty", "格式化 JSON 输出;extract 同时选择 JSON 模式", false)
60
88
  .showSuggestionAfterError();
61
89
  program.configureOutput({ writeErr: () => { }, outputError: () => { } });
62
90
  program.exitOverride();
@@ -74,6 +102,7 @@ export function createProgram(run, dependencies = {}) {
74
102
  .option("--include-domain <domain>", "仅包含域名,可重复", collect, [])
75
103
  .option("--exclude-domain <domain>", "排除域名,可重复", collect, [])
76
104
  .option("--timeout <milliseconds>", "总超时毫秒数", integer)
105
+ .option("--debug", "输出完整路由和脱敏原始响应", false)
77
106
  .action((query, options) => {
78
107
  run(async () => {
79
108
  const globals = program.opts();
@@ -94,18 +123,23 @@ export function createProgram(run, dependencies = {}) {
94
123
  return executeSearch(request, {
95
124
  loaded,
96
125
  signal: processSignal.signal,
126
+ debug: options.debug,
127
+ persistProviderOrder: createProviderOrderWriter(loaded),
97
128
  });
98
129
  });
99
130
  });
100
131
  program
101
132
  .command("extract")
102
- .description("提取网页正文并转换为 Markdown")
133
+ .description("提取网页正文并转换为 Markdown(--json/--pretty/--debug 输出 JSON)")
103
134
  .argument("<url>", "HTTP(S) URL")
104
135
  .option("-p, --provider <id>", "provider instance id,或 auto", "auto")
105
136
  .option("--timeout <milliseconds>", "总超时毫秒数", integer)
137
+ .option("--json", "输出 JSON envelope", false)
138
+ .option("--debug", "输出完整路由和脱敏原始响应", false)
106
139
  .action((url, options) => {
140
+ const globals = program.opts();
141
+ const outputMode = options.json || options.debug || globals.pretty ? "json" : "markdown";
107
142
  run(async () => {
108
- const globals = program.opts();
109
143
  const loaded = loadConfig(globals.config);
110
144
  const request = {
111
145
  url: httpUrl(url),
@@ -115,8 +149,10 @@ export function createProgram(run, dependencies = {}) {
115
149
  return executeExtract(request, {
116
150
  loaded,
117
151
  signal: processSignal.signal,
152
+ debug: options.debug,
153
+ persistProviderOrder: createProviderOrderWriter(loaded),
118
154
  });
119
- });
155
+ }, outputMode);
120
156
  });
121
157
  program
122
158
  .command("providers")
@@ -148,7 +184,9 @@ const processSignal = new AbortController();
148
184
  process.once("SIGINT", () => processSignal.abort());
149
185
  async function main() {
150
186
  let pending;
151
- const program = createProgram((task) => {
187
+ let pendingOutputMode = "json";
188
+ const program = createProgram((task, mode = "json") => {
189
+ pendingOutputMode = mode;
152
190
  pending = Promise.resolve().then(task);
153
191
  });
154
192
  if (process.argv.length <= 2) {
@@ -162,7 +200,7 @@ async function main() {
162
200
  if (!pending)
163
201
  throw new WebAccessError("invalid_input", "必须指定命令");
164
202
  const envelope = await pending;
165
- writeEnvelope(envelope, !!program.opts().pretty);
203
+ writeOutput(envelope, pendingOutputMode, !!program.opts().pretty);
166
204
  process.exitCode = exitCode(envelope);
167
205
  }
168
206
  catch (caught) {