web-access-cli 0.6.0 → 0.7.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/README.md CHANGED
@@ -5,334 +5,174 @@ 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, Bocha, Yuanbao Search, Brave, SearXNG, AnySearch, XCrawl, DeepSeek, Firecrawl, Jina, and other providers behind a unified internal schema.
8
+ Two entry points over one core: a **CLI** for direct invocation and a **DeepSeek-compatible HTTP gateway** for long-running agent runtimes. Both expose the same two capabilities — search and content extraction — and normalize Tavily, Exa, Bocha, Yuanbao Search, Brave, SearXNG, AnySearch, XCrawl, DeepSeek, Firecrawl, Jina, and others behind one schema.
9
9
 
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.
10
+ Neither entry point is tied to Pi, Claude Code, Codex, Cursor, OpenCode, or any other agent. Skills, MCP integrations, and agent plugins are adapters on top; they never enter the core. Requires Node.js 22.19+.
11
11
 
12
- ## Capabilities and providers
12
+ ## Capabilities
13
13
 
14
14
  | Capability | Provider types | Normalized output |
15
15
  | --- | --- | --- |
16
16
  | `search` | Tavily, Exa, Bocha, Yuanbao Search, Brave, SearXNG, AnySearch, XCrawl, DeepSeek, xAI x_search, xAI web_search | `rank`, `title`, `url`, `snippet` |
17
17
  | `extract` | Firecrawl v2, Jina Reader, Exa Contents, AnySearch, XCrawl, DeepSeek Fetch, 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`. Each capability has a primary `providers` route and a `providers_fallback` route. Both arrays determine enabled instances and their current `auto` order; the CLI only reorders members within their existing group.
19
+ A provider type is an implementation; a provider instance is a configured instance of that type, so `exa_team` and `exa_personal` can coexist. Each capability has a primary `providers` route and a `providers_fallback` route; both define the enabled instances and their `auto` order.
20
20
 
21
- ## Installation
22
-
23
- Node.js 22.19 or later is required.
24
-
25
- Install the CLI from npm:
21
+ ## Install
26
22
 
27
23
  ```sh
28
- npm install --global web-access-cli
24
+ npm install --global web-access-cli # or: pnpm add --global web-access-cli
29
25
  web-access --help
30
26
  ```
31
27
 
32
- Alternatively, install it with pnpm:
28
+ From a checkout: `pnpm install --frozen-lockfile && pnpm check`. The gateway runs from the same checkout (`node --import tsx src/server.ts`) or built output (`pnpm start:gateway`); production deployment uses the GHCR image, whose Compose template and secret conventions are in [deploy/README.md](deploy/README.md).
33
29
 
34
- ```sh
35
- pnpm add --global web-access-cli
36
- web-access --help
37
- ```
30
+ Layout: `src/cli.ts` and `src/server.ts` are the two entries, `src/gateway/` holds the tool protocol, and `src/core/`, `src/providers/`, `src/transport/` are shared by both. Adapters map protocols only; fallback, deadlines, envelopes, and exit codes live in `src/core`.
38
31
 
39
- For development from source:
32
+ ## CLI
40
33
 
41
34
  ```sh
42
- git clone https://github.com/juexe/web-access-cli.git
43
- cd web-access-cli
44
- pnpm install --frozen-lockfile
45
- pnpm check
46
- pnpm pack:check
47
- ```
35
+ web-access search "TypeScript release" --provider exa --limit 10 --freshness month
36
+ web-access search "browser automation" --include-domain example.com --exclude-domain blocked.example.com
37
+ web-access extract https://example.com/article --provider http --timeout 30000
38
+ web-access extract https://example.com/article --json
48
39
 
49
- ## Project structure
50
-
51
- ```text
52
- src/cli.ts Commander CLI and exit codes
53
- src/server.ts Standalone DeepSeek-compatible gateway entry point
54
- src/config/ Configuration files, environment variables, and route merging
55
- src/core/ Public types, schemas, errors, routing, and diagnostics
56
- src/gateway/ Gateway protocol schema, service mapping, and HTTP boundary
57
- src/providers/ Provider adapters, registry, and HTTP/RSC extraction
58
- src/transport/ Proxies, timeouts, redirects, and response size limits
59
- schemas/ Generated JSON Schemas
60
- test/ node:test unit tests and local HTTP integration tests
61
- deploy/ Public gateway Compose template, env/config examples, operations guide
40
+ web-access providers # route, capability, credential and URL status
41
+ web-access doctor # local configuration checks, no network
42
+ web-access config init # scaffold from credential env vars
43
+ web-access config edit # open in $VISUAL/$EDITOR
62
44
  ```
63
45
 
64
- Provider adapters should handle protocol mapping only. Fallback, deadlines, attempts, envelopes, and exit codes are managed centrally by `src/core`. Do not add lifecycle or tool registration code for Pi, Claude Code, Codex, or any other agent to an adapter.
46
+ Global options: `--config <path>`, `--json`, `--help`, `--version`. Capability commands print human-readable Markdown, `config init` prints the created path, and every failure prints one JSON envelope. Exit codes: `0` success, `2` invalid input/config or a disabled provider, `1` runtime or doctor failure, `130` cancelled.
65
47
 
66
- ## Development workflow
48
+ Not provided: batch/all, answer, a PDF-only path, a Node SDK, or generic MCP. An optional [Agent Skill](skills/web-access-cli/SKILL.md) exists for source checkouts and is not published to npm.
67
49
 
68
- ```sh
69
- pnpm install
70
- pnpm lint
71
- pnpm typecheck
72
- pnpm test
73
- pnpm build
74
- pnpm pack:check
75
- pnpm test:docker
76
- ```
50
+ ## HTTP gateway
77
51
 
78
- `pnpm check` runs linting, type checking, tests, and the build in that order. After changing `src/core/schema.ts`, run `pnpm build` again and commit the updated `schemas/*.schema.json` files. Provider tests should use the mock transport or local HTTP server from `test/helpers.ts`; they must not depend on real API keys or external network reliability.
52
+ A long-running process that serves the same providers, routing and transport over DeepSeek's [Search and Fetch API](https://api-docs.deepseek.com/zh-cn/guides/search): same endpoints, auth header, request fields and error envelope, so a client written for the official service only changes its base URL. It changes nothing about the CLI contract.
79
53
 
80
- ## Commands
54
+ Run it as a Docker image — `ghcr.io/juexe/web-access-cli`, multi-arch `linux/amd64` and `linux/arm64`, published on every GitHub Release with exact version tags plus `latest` for stable releases. Deployment needs Docker only, no Node.js or pnpm:
81
55
 
82
56
  ```sh
83
- web-access search "Agent-neutral web CLI"
84
- web-access search "TypeScript release" --provider exa --limit 10 --freshness month
85
- web-access search "browser automation" --include-domain example.com --exclude-domain blocked.example.com
86
-
87
- web-access extract https://example.com/article
88
- web-access extract https://example.com/article --provider http --timeout 30000
89
- web-access extract https://example.com/article --json
57
+ docker pull ghcr.io/juexe/web-access-cli:0.7.0
58
+ docker compose -f deploy/compose.yaml --env-file personal/deploy.env up -d --wait gateway
59
+ ```
90
60
 
91
- web-access providers
92
- web-access doctor
61
+ Or from source; it listens on `127.0.0.1:8080` by default:
93
62
 
94
- web-access config edit
95
- web-access --config "/path/to/config.json" config edit
96
- web-access config init
97
- web-access --config "/path/to/config.json" config init --json
63
+ ```sh
64
+ # Source
65
+ WEB_ACCESS_CONFIG=~/.config/web-access-cli/config.json \
66
+ WEB_ACCESS_GATEWAY_API_KEY_FILE=/path/to/gateway-api-key \
67
+ node --import tsx src/server.ts
98
68
  ```
99
69
 
100
- The CLI provides the `search` and `extract` capability commands, the `providers` and `doctor` diagnostic commands, and the `config init` and `config edit` configuration commands. The current version does not provide batch/all, answer, a dedicated PDF path, a Node.js SDK, or a generic MCP integration. The repository includes an optional [web-access-cli Agent Skill](skills/web-access-cli/SKILL.md) for source-checkout use; it is not included in the npm package.
101
-
102
- ### Global options
70
+ | Method | Path | Notes |
71
+ | --- | --- | --- |
72
+ | `POST` | `/v1/tools/search` | DeepSeek Search compatible |
73
+ | `POST` | `/v1/tools/fetch` | DeepSeek Fetch compatible |
74
+ | `GET` | `/healthz`, `/readyz` | Liveness and configuration readiness, no auth |
75
+ | `GET` | `/panel` | Admin UI shell, no auth and no data |
76
+ | `GET`/`DELETE` | `/admin/api/records` | Request records, Bearer key required |
77
+ | `GET` | `/admin/api/overview` | Runtime stats, route readiness, per-instance counters |
78
+ | `PUT` | `/admin/api/config` | Patch routes, budgets, instances; hot-applies and persists |
79
+ | `POST` | `/admin/api/config/reload` | Reload the config file from disk |
80
+
81
+ - The Bearer key is read from a file only, compared as a fixed-length digest, and duplicate `Authorization` headers are rejected. Requests are single-user private, so the gateway adds no local admission, body size limit, or body read deadline and never returns 413 or 408 itself. Responses use `Cache-Control: no-store`.
82
+ - Search fields: `query` (1–128 characters), `count` (1–50, default 10), `ori_question` (1–128 when set), `localization` (an IP address when non-empty), `need_content` (boolean, default `false`). Fetch takes one absolute HTTP(S) URL of at most 8192 characters. Unknown fields are ignored and cannot override provider, credentials, timeout, or configuration.
83
+ - Responses are protocol-compatible, not equal to the official engine: `summary` reuses `snippet`, `rerank_score` is always `0`, `authority` is always `-1`, and `site_name`/`site_icon`/`date` are `null`. Clients filtering on score or authority must change strategy, and no result count, ranking, or quality is promised.
84
+ - `ori_question` and `localization` are validated but have no core equivalent, so they never reach providers.
85
+ - `need_content=true` backfills by rank with two concurrent tasks, a 30-second window, 10 seconds per item, and a 3-second attempt cap; failures keep the result with empty content. Backfill and responses have no size cap, and **backfill spends extract quota**. Fetch and backfill derive their own config with a 1-character minimum so short pages succeed, while the source Markdown probe still requires 500 characters. Client disconnects cancel in-flight work.
86
+ - Errors are always `{ "error": { "message", "type": "…", "param": null, "code" } }`: 400 `invalid_request_error`, 401 `invalid_api_key`, 402 `insufficient_quota`, 404 `no_usable_content` or an unknown route, 405 wrong method, 503 while shutting down, 504 `upstream_timeout`, and 500 for provider, credential, or configuration failures. 429 only ever means the upstream provider rate-limited.
87
+ - Tool requests never rewrite provider order: concurrent traffic does not trigger config write-back. The panel's config workspace and `/admin/api/config*` are the only writers, and they hot-apply plus persist atomically (with a read-only fallback when the file is mounted as a secret).
88
+ - The panel keeps the last 500 requests in memory: status, duration, error, provider, query/URL, attempt chain, snippets, and truncated content previews (2 000 chars for search results, 4 000 for Fetch) — never full bodies, headers, or credentials. A `#apikey=<key>` fragment logs in, is stripped from the address bar, and stores the key in `sessionStorage`.
89
+
90
+ ### Use it from DSH
91
+
92
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) ships `@deepseek-ai/dsh-web-deepseek-api`, a provider that speaks exactly this API, so pointing `web_search` and `web_fetch` here needs no extra plugin — only a base URL. In `~/.dsh/cordis.patch.yml`:
93
+
94
+ ```yaml
95
+ - id: web
96
+ name: "@deepseek-ai/dsh-web"
97
+ config:
98
+ searchProvider: deepseek-search-api
99
+ fetchProvider: deepseek-fetch-api
100
+ - id: web-deepseek-api
101
+ name: "@deepseek-ai/dsh-web-deepseek-api"
102
+ config:
103
+ apiKeyEnv: DEEPSEEK_API_KEY # must hold the gateway key, not a DeepSeek key
104
+ baseURL: http://127.0.0.1:8080/v1/tools
105
+ ```
103
106
 
104
- - `--config <path>`: Explicitly select a configuration file.
105
- - `--json`: Output a compact schema v2 JSON envelope. Without it, successful commands write human-readable Markdown; all failures always write JSON.
106
- - `--help`, `--version`: Print standard CLI help or version text.
107
+ `baseURL` is the full base including the path prefix and excluding `/search` or `/fetch`; the default `apiKeyEnv` is `DEEPSEEK_API_KEY`, so point it at a variable holding the gateway key. `web_search` and `web_fetch` then hit this gateway with no other change — only the placeholder score/authority fields differ, as noted above. Because the gateway is private, keep it on a trusted network rather than exposing it publicly.
107
108
 
108
- Successful commands default to Markdown for human readers. `search` writes a YAML front matter block followed by a numbered result list; `extract` writes YAML front matter followed by the normalized Markdown body; `providers`, `doctor`, and `config edit` write a heading followed by pretty-printed JSON data; `config init` writes the created absolute path on one line. Add `--json` when a schema v2 envelope is required. Extract `partial` results and all failures, including invalid input and runtime errors, always remain JSON envelopes. Diagnostic messages are never mixed into stdout.
109
+ Deployment, secrets, upgrades and rollback: [deploy/README.md](deploy/README.md). Decision record: [ADR 0019](docs/adr/0019-deepseek-tool-gateway.md).
109
110
 
110
111
  ## Configuration
111
112
 
112
- Configuration files use strict JSON; unknown fields are rejected. The CLI reads only an explicit path or a user-level path and does not search parent project directories for configuration files.
113
-
114
- The default path on every platform is `~/.config/web-access-cli/config.json`, where `~` is the current user's home directory. The `WEB_ACCESS_CONFIG` environment variable can select another path. The `--config` command-line option takes precedence.
115
-
116
- `web-access config edit` creates parent directories and a complete default configuration when the file is missing, then opens it with the command in `VISUAL`, or `EDITOR` when `VISUAL` is empty. The value may contain a quoted executable path and arguments; it is parsed without shell evaluation and the configuration path is appended as a separate argument. 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 editor process to start, not for it to close. When neither variable is set, the command returns `open_failed`; a configuration file that was created successfully remains available for manual editing.
113
+ Strict JSON; unknown fields are rejected. Resolution order is `--config`, then `WEB_ACCESS_CONFIG`, then `~/.config/web-access-cli/config.json`. A missing default file falls back to built-in defaults; a missing explicit file is a `config_error`. No parent directories are scanned.
117
114
 
118
- `config init` creates a new configuration from standard credential environment variables and refuses to overwrite an existing file. It keeps all built-in instance definitions, writes detected Search providers to the primary route, leaves Search fallback empty, writes detected remote Extract providers to the primary route, and places `http` in the Extract fallback route. After an `auto` capability call succeeds or encounters fallback-eligible failures, the CLI atomically reorders the matching `providers` or `providers_fallback` array in place. Members never move between groups. The legacy `_providers` and `_providers_fallback` fields are rejected as unknown configuration fields.
119
-
120
- The complete JSON Schema is available at [schemas/config.schema.json](schemas/config.schema.json). Example:
115
+ `config edit` creates a full default file when absent and opens it with `VISUAL`, else `EDITOR`, without shell evaluation or reformatting; `config init` scaffolds from standard credential environment variables and refuses to overwrite. Environment variables override plaintext keys. The full schema is [schemas/config.schema.json](schemas/config.schema.json):
121
116
 
122
117
  ```json
123
118
  {
124
- "$schema": "https://unpkg.com/web-access-cli@0.6.0/schemas/config.schema.json",
119
+ "$schema": "https://unpkg.com/web-access-cli@0.7.0/schemas/config.schema.json",
125
120
  "providers": [
126
- {
127
- "id": "exa_team",
128
- "type": "exa",
129
- "apiKeyEnv": "TEAM_EXA_API_KEY",
130
- "headers": {
131
- "X-Team": "docs"
132
- }
133
- },
134
- {
135
- "id": "searx_local",
136
- "type": "searxng",
137
- "baseUrl": "http://127.0.0.1:8080"
138
- },
139
- {
140
- "id": "firecrawl_local",
141
- "type": "firecrawl",
142
- "baseUrl": "http://127.0.0.1:3002"
143
- }
121
+ { "id": "exa_team", "type": "exa", "apiKeyEnv": "TEAM_EXA_API_KEY" },
122
+ { "id": "firecrawl_local", "type": "firecrawl", "baseUrl": "http://127.0.0.1:3002" }
144
123
  ],
145
- "search": {
146
- "providers": ["searx_local", "exa_team", "brave"],
147
- "providers_fallback": ["deepseek"],
148
- "limit": 5,
149
- "timeoutMs": 120000,
150
- "attemptTimeoutMs": 60000,
151
- "maxResponseBytes": 5242880
152
- },
153
- "extract": {
154
- "providers": ["firecrawl_local", "jina", "exa_team"],
155
- "providers_fallback": ["http"],
156
- "timeoutMs": 120000,
157
- "attemptTimeoutMs": 45000,
158
- "maxResponseBytes": 5242880,
159
- "minContentCharacters": 500
160
- }
124
+ "search": { "providers": ["exa_team"], "providers_fallback": ["brave"] },
125
+ "extract": { "providers": ["firecrawl_local"], "providers_fallback": ["http"] }
161
126
  }
162
127
  ```
163
128
 
164
- The built-in instances are `tavily`, `exa`, `bocha`, `yuanbao`, `brave`, `searxng`, `firecrawl`, `jina`, `http`, `anysearch`, `xcrawl`, `deepseek`, `xai_x_search`, and `xai_web_search`. 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 when it appears in either corresponding route. Bocha defaults to `https://api.bocha.cn` and requires an API key; Yuanbao Search defaults to `https://api.wsa.cloud.tencent.com` and requires a service 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` and requires an API key.
165
-
166
- Default routes:
167
-
168
- - Search primary: `tavily -> exa -> bocha -> yuanbao -> brave -> searxng -> anysearch -> xcrawl -> deepseek`
169
- - Search fallback: `xai_x_search -> xai_web_search`
170
- - Extract primary: `firecrawl -> jina -> exa -> anysearch -> xcrawl -> deepseek`
171
- - Extract fallback: `http`
172
-
173
- The default routes split built-in providers into a primary group and a fallback group. Custom instance IDs are merged into the instance list but must still be added to a route explicitly. An omitted capability route uses the defaults above; an explicit Search primary empty array is invalid, while Extract may be explicitly disabled with an empty primary route. In `auto` mode, incompletely configured instances are skipped. 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.
174
-
175
- Yuanbao Search calls Tencent Cloud `POST /SearchPro` with a bearer service API key and a lower-case request body (`Query`, plus optional `Site` and `Freshness`). One `site:` include becomes `Site=<domain>`; up to five excludes become `Site=exclude:<domain>|<domain>`; other combinations are filtered locally only. Each entry of `Pages` is a JSON string and is mapped to `title`/`url`/`snippet`, preferring the standard `passage` summary over the dynamic `content`. Errors returned inside a successful HTTP response, such as `UnauthorizedOperation` or `ResourceNotFound`, stay fallback-eligible so `auto` can continue to the next provider. Only Search is supported; `Cnt` is omitted while `--limit` stays at or below 10 so the lite edition keeps working, and is rounded up to the next multiple of 10 (maximum 50) above that.
129
+ Built-in instances: `tavily`, `exa`, `bocha`, `yuanbao`, `brave`, `searxng`, `anysearch`, `xcrawl`, `deepseek`, `firecrawl`, `jina`, `http`, `xai_x_search`, `xai_web_search`. Reusing a built-in ID overrides that instance; a new ID adds another of the same type. An instance is enabled only when listed in a route. Default routes:
176
130
 
177
- DeepSeek uses the official tool API: Search posts to `{baseUrl}/v1/tools/search` and Fetch posts to `{baseUrl}/v1/tools/fetch`, both with a Bearer `DEEPSEEK_API_KEY`. Search sends `count` and `need_content: false` and maps `title`/`url`/`snippet` from `results`, falling back to the longer `summary` only when `snippet` is empty; the remaining fields stay in `raw`. `freshness` has no request field, so it is rewritten into the documented `after:` search syntax, and domain constraints use `site:`/`-site:`. Both are hints only and are strictly re-applied locally; if the combined query would exceed the 128-character limit, the remote hints are dropped and the original query kept. Fetch returns `content` as the Markdown body and `title` as the title; HTTP 404 or an empty body becomes fallback-eligible `no_usable_content`. 401 is `auth_error`, 402 is `quota_exceeded`, 429 is `rate_limited`, and other statuses are `provider_error`; 400/401/402/403 are not retryable against the same provider.
131
+ - Search: `tavily -> exa -> bocha -> yuanbao -> brave -> searxng -> anysearch -> xcrawl -> deepseek`, fallback `xai_x_search -> xai_web_search`
132
+ - Extract: `firecrawl -> jina -> exa -> anysearch -> xcrawl -> deepseek`, fallback `http`
178
133
 
179
- XAI hosted Search reads an external OAuth JSON file from `XAI_AUTH_JSON` on every call and uses only its `access_token`; an external CLIProxyAPI process owns refresh and write-back. Instance fields `authJson`/`authJsonEnv` and `model`/`modelEnv` override the path and model. `xai_web_search` accepts at most five allow or exclude domains (mutually exclusive); `xai_x_search` rejects `freshness`. Model-backed search can take longer than ordinary API searches; Search defaults allow up to 60 seconds per attempt and 120 seconds total.
180
-
181
- ### Credentials and URLs
182
-
183
- Environment variables take precedence over plaintext keys in JSON. Built-in instances support these standard variables:
134
+ An explicit empty Search primary route is invalid; Extract can be disabled with an empty primary route. AnySearch and XCrawl take `searchFilterMode`: `strict` (skip on freshness) or `best_effort` (rewrite freshness into the query). Yuanbao posts `Query`/`Site`/`Freshness` to `/SearchPro` and keeps in-body errors fallback-eligible. DeepSeek uses `/v1/tools/search` and `/v1/tools/fetch`, rewriting freshness and domains into `after:`/`site:` hints that are re-applied locally. xAI reads an OAuth JSON from `XAI_AUTH_JSON` without refreshing it and needs a raised `attemptTimeoutMs`.
184
135
 
185
136
  | Type | API key | Base URL |
186
137
  | --- | --- | --- |
187
- | Tavily | `TAVILY_API_KEY` | `TAVILY_BASE_URL` |
188
- | Exa | `EXA_API_KEY` | `EXA_BASE_URL` |
138
+ | Tavily / Exa / Brave / Firecrawl / XCrawl | `<NAME>_API_KEY` | `<NAME>_BASE_URL` |
189
139
  | Bocha | `BOCHA_API_KEY` | `BOCHA_BASE_URL`, default `https://api.bocha.cn` |
190
- | Brave | `BRAVE_API_KEY` | `BRAVE_BASE_URL` |
191
140
  | SearXNG | None | `SEARXNG_BASE_URL`, required |
192
- | Firecrawl | `FIRECRAWL_API_KEY` | `FIRECRAWL_BASE_URL` |
193
- | Jina | `JINA_API_KEY`, optional | `JINA_BASE_URL` |
194
- | HTTP | None | None |
195
- | AnySearch | `ANYSEARCH_API_KEY`, optional | `ANYSEARCH_BASE_URL`, default `https://api.anysearch.com` |
196
- | XCrawl | `XCRAWL_API_KEY` | `XCRAWL_BASE_URL`, default `https://run.xcrawl.com` |
197
- | DeepSeek | `DEEPSEEK_API_KEY` | No standard environment variable; default `https://api.deepseek.com` |
198
- | xAI Search | `XAI_AUTH_JSON` | No standard API-key variable; default `https://cli-chat-proxy.grok.com/v1`, model `grok-4.6` |
199
-
200
- 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.
201
-
202
- ## Execution and fallback
141
+ | Jina / AnySearch | `JINA_API_KEY` / `ANYSEARCH_API_KEY`, optional | `<NAME>_BASE_URL` |
142
+ | DeepSeek | `DEEPSEEK_API_KEY` | fixed `https://api.deepseek.com` |
143
+ | xAI Search | `XAI_AUTH_JSON` (OAuth file) | default `https://cli-chat-proxy.grok.com/v1`, `grok-4.6` |
203
144
 
204
- `--provider auto` follows the configured `providers` order first and enters `providers_fallback` only after the complete primary group fails with a fallback-eligible result:
145
+ Custom instances name their variables with `apiKeyEnv`/`baseUrlEnv` and may set `baseUrl` and `headers`. Public Firecrawl Cloud needs a key; self-hosted Firecrawl v2 does not.
205
146
 
206
- 1. An instance with incomplete configuration is recorded as a failed attempt and skipped.
207
- 2. A final provider HTTP response outside 2xx, including authentication, request, rate-limit, and server errors, causes the router to try the next instance.
208
- 3. Other recoverable errors, including network errors, timeouts, oversized responses, and missing usable content, also cause the router to continue.
209
- 4. Non-recoverable failures without a final non-2xx response, including invalid input and unsupported content detected in a successful response, stop execution immediately.
210
- 5. If every instance in both groups fails, the command returns `provider_exhausted`. The best short content produced during extraction is preserved in `partial`.
147
+ ## Routing and limits
211
148
 
212
- Before those routes run, `extract --provider auto` checks one Markdown variant when an HTTP instance is enabled in either group. It appends `.md` to file-like paths or uses `index.md` for directory paths, requests `text/markdown` and `text/plain`, and accepts only a sufficiently long non-HTML text response. A miss or request failure is transparent and falls through to the configured routes. A hit is reported as the enabled HTTP instance; it does not change provider order. Explicit non-HTTP selections are unchanged, while an explicit HTTP selection checks the Markdown variant before fetching the original URL.
149
+ Both entry points call the same router and differ only in output shape, per-command options, and the gateway's fixed Fetch/backfill thresholds.
213
150
 
214
- After each `auto` call, a successful instance moves to the front of its group, 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; no provider crosses between primary and fallback. The order stays unchanged when every instance in a group 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.
151
+ `auto` walks the primary route, then the fallback route once the primary group has failed in a way that is fallback-eligible. Incomplete instances are recorded and skipped; non-2xx provider responses and recoverable errors (network, timeout, oversized, no usable content) continue to the next instance; invalid input and unsupported content stop immediately; full exhaustion returns `provider_exhausted` with the best short body in `partial`. An explicit `--provider` runs once with no fallback and returns `provider_disabled` if not routed. `retryable` describes retrying the same provider, not the switch condition.
215
152
 
216
- 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.
153
+ `extract --provider auto` first probes one Markdown variant when HTTP is enabled — `.md` for file-like paths, `index.md` for directories — accepting only sufficiently long non-HTML text. Misses fall through transparently; hits are attributed to the HTTP instance without reordering.
217
154
 
218
- Selecting an instance explicitly executes it strictly without fallback. If the instance is not in the route, the command returns `provider_disabled`.
155
+ In the CLI, after each `auto` call the successful instance moves to the front of its group and fallback-eligible failures move to the back; nothing crosses groups. Search and Extract learn independently. Explicit instances, non-fallback failures, and cancellations never reorder, and writes are atomic with last-writer-wins. The gateway never rewrites routes.
219
156
 
220
- Default limits are a 120-second total timeout and a 60-second per-attempt timeout for Search, and 120 seconds and 45 seconds respectively for Extract. Each response has a hard limit of 5 MiB. `--timeout` overrides only the total timeout for the current command.
157
+ Defaults: 120 s total and 60 s per attempt for Search; 120 s and 45 s for Extract; 5 MiB per response. `--timeout` overrides only the total timeout.
221
158
 
222
- ## Output contract
159
+ ## Output
223
160
 
224
- Capability commands use output schema version 2. Their default envelope is intentionally small:
161
+ Capability commands emit schema version 2. With `--json`, output contains `schemaVersion`, `ok`, the final instance and `data`; failures add a compact `error`, `attempts` (instance, code, optional status), and possibly a `partial` document. A failed order write adds `warnings` without replacing the result. Human-readable output is Markdown: a `provider` front matter block plus either a numbered result list or the extracted document; diagnostics print a heading plus JSON. Scripts should pass `--json`.
225
162
 
226
163
  ```json
227
- {
228
- "schemaVersion": 2,
229
- "ok": true,
230
- "provider": "exa",
231
- "data": {
232
- "results": [
233
- {
234
- "rank": 1,
235
- "title": "Example",
236
- "url": "https://example.com/",
237
- "snippet": "Example snippet"
238
- }
239
- ]
240
- }
241
- }
242
- ```
243
-
244
- With `--json`, 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.
245
-
246
- For human-readable output, extraction starts with YAML front matter and then the provider-normalized Markdown body. Search results use a numbered Markdown list; diagnostics and config edit use a heading plus pretty-printed JSON data. Metadata values use JSON double-quoted scalar encoding and the body is not modified. Existing scripts and other machine consumers should append `--json` explicitly.
247
-
248
- Example default extract output:
249
-
250
- ```markdown
251
- ---
252
- provider: "local_http"
253
- url: "https://example.com/article"
254
- title: "Example title"
255
- ---
256
-
257
- # Article content
164
+ { "schemaVersion": 2, "ok": true, "provider": "exa",
165
+ "data": { "results": [{ "rank": 1, "title": "Example", "url": "https://example.com/", "snippet": "..." }] } }
258
166
  ```
259
167
 
260
- All success and failure envelopes use schema version 2. Stable schemas are available in [schemas](schemas).
261
-
262
- Exit codes:
263
-
264
- - `0`: Success
265
- - `2`: Invalid input or configuration, or an unknown or disabled provider
266
- - `1`: Runtime, provider, doctor, or editor launch failure
267
- - `130`: User cancellation
268
-
269
- `providers` lists each instance's type, capabilities, route status, credential source, and base URL source. Its `searchRoute`/`searchFallbackRoute` and `extractRoute`/`extractFallbackRoute` fields show the two effective orders 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`.
270
-
271
- ## DeepSeek-compatible HTTP gateway
272
-
273
- The repository also ships a **separate** HTTP service that wraps the same provider, route and transport implementations in the DeepSeek tool protocol. It is not a replacement for the CLI: commands, exit codes, the schema v2 envelope and the npm bin contract are unchanged.
274
-
275
- ```sh
276
- # From source (listens on 127.0.0.1:8080 by default)
277
- WEB_ACCESS_CONFIG=~/.config/web-access-cli/config.json \
278
- WEB_ACCESS_GATEWAY_API_KEY_FILE=/path/to/gateway-api-key \
279
- pnpm start:gateway # or node --import tsx src/server.ts
280
-
281
- # Built output
282
- node dist/server.js
283
- ```
284
-
285
- | Method | Path | Notes |
286
- | --- | --- | --- |
287
- | `POST` | `/v1/tools/search` | DeepSeek Search compatible |
288
- | `POST` | `/v1/tools/fetch` | DeepSeek Fetch compatible |
289
- | `GET` | `/`, `/panel` | Request records panel (HTML, no data in the page itself) |
290
- | `GET` | `/admin/api/records` | Recent request records (requires the Bearer key) |
291
- | `GET` | `/healthz` | Process liveness, no authentication |
292
- | `GET` | `/readyz` | Configuration/lifecycle readiness, no authentication |
293
-
294
- The gateway reads its own Bearer key from a file only (`WEB_ACCESS_GATEWAY_API_KEY_FILE`), compares fixed-length digests with Node crypto, and rejects duplicate `Authorization` headers. Request bodies are counted as a stream with a 64 KiB cap and a 10-second read timeout; at most four tool requests may be in flight, and excess requests receive 429. Success and error responses use `Cache-Control: no-store`.
295
-
296
- Request fields match the official contract:
297
-
298
- | Field | Behavior |
299
- | --- | --- |
300
- | `query` | Required, 1–128 Unicode characters, not entirely whitespace; search operators are preserved |
301
- | `count` | Integer 1–50, default 10 |
302
- | `ori_question` | Optional or `null`; when non-null, 1–128 characters |
303
- | `localization` | Optional, default empty string; when non-empty it must be an IP address |
304
- | `need_content` | Optional, default `false`; must be a boolean |
305
-
306
- Fetch accepts only a non-empty absolute HTTP(S) URL of at most 8192 characters with a hostname and no userinfo. Unknown request fields are ignored and cannot select a provider, credential, timeout or configuration override.
307
-
308
- **Responses are protocol-compatible, not equivalent to the official search engine**: `summary` reuses `snippet`, `rerank_score` is always `0`, `authority` is always `-1`, and `site_name`/`site_icon`/`date` are always `null`. The placeholder score is **not** a DeepSeek quality score; clients filtering on `rerank_score >= 1.5` or `authority >= 3` must change strategy or they will drop every result. Result count, ranking and summary quality are not promised, and the gateway never paginates or merges providers to fill `count`.
309
-
310
- `ori_question` and `localization` are validated and accepted, but the neutral core has no equivalent capability, so they do not affect provider requests and are not folded into `query`.
311
-
312
- With `need_content=true`, content is backfilled in rank order with at most two concurrent tasks, a 30-second total window, 10 seconds per item, and a 3-second per-provider attempt cap. Failed, timed-out or over-limit content stays empty while the result is kept; one content value is capped at 256 KiB JSON-encoded UTF-8 and the complete response at 8 MiB, clearing content from the lowest ranks when needed. **Backfill spends extract provider quota.**
313
-
314
- The gateway derives its own config for Fetch and backfill with a 1-character minimum, so valid short pages succeed, while the source Markdown probe still requires at least 500 characters (or the original threshold, whichever is larger) so a short `Not found` page cannot win. Search and Fetch each use the configured total timeout; backfill has an independent 30-second window, and client disconnects cancel in-flight work.
315
-
316
- Errors are always `{ "error": { "message": "...", "type": "...", "param": null, "code": "..." } }`: 400 invalid input, 401 `invalid_api_key`, 402 insufficient quota, 404 no usable content or an unavailable direct target, 408 body read timeout, 413 body too large, 429 rate limited (with `Retry-After`), 504 upstream timeout, and 500 for credential/configuration failures, unknown failures or oversized output. Target-site 401/403 means Fetch is unavailable; cloud-provider 401/403 returns 500.
317
-
318
- The gateway never writes provider order and never modifies configuration: it uses the snapshot loaded at startup. Deployment, Docker Compose templates, secret mounts, diagnostics, upgrades and rollback are documented in [deploy/README.md](deploy/README.md); the architectural decision is in [docs/adr/0019-deepseek-tool-gateway.md](docs/adr/0019-deepseek-tool-gateway.md).
319
-
320
- Open the gateway root or `/panel` in a browser and enter the gateway key to inspect the last 500 tool requests (status, duration, error code, provider, query/URL and the first five result titles). Records are in memory only, are cleared on restart, and never contain response bodies, headers or credentials; unauthenticated requests are excluded. The panel HTML carries no data, and `/admin/api/records` uses the same Bearer key as the tool endpoints. A bookmark with an `#apikey=<key>` fragment logs in directly; the script strips it from the address bar immediately. Do not pass the key in a query string.
321
-
322
- ## HTTP and security boundaries
323
-
324
- - Honors `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY` by default.
325
- - Follows up to five redirects. Cross-origin redirects remove authentication, Cookie, and common token headers.
326
- - Enforces a streaming byte limit on every response instead of buffering an unbounded response first.
327
- - HTTP Extract uses LinkeDOM, Mozilla Readability, and Turndown, with fallback parsing for Next.js RSC payloads.
328
- - HTTP Extract prefers an enabled source Markdown variant (`.md` or directory `index.md`) before provider routes when that variant returns usable Markdown.
329
- - Returns `unsupported_content` for PDF, image, audio, video, zip, and general binary content.
330
- - By design, the CLI does not block localhost, private network addresses, or cloud metadata URLs. For untrusted input, callers must enforce their own URL allowlist, network isolation, or outbound proxy policy.
168
+ `providers` reports each instance's type, capabilities, route status, and credential/URL sources together with the effective orders for the next call; `doctor` fails with `1` when an enabled route contains an unconfigured instance.
331
169
 
332
- ## Project scope
170
+ ## Security boundaries
333
171
 
334
- This project draws heavily from the provider and content extraction implementations in `pi-web-access`, but does not include Pi-specific registration, tool protocols, or UI code.
172
+ - Honors `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY`; follows up to five redirects and strips authentication, Cookie, and token headers across origins.
173
+ - Enforces a streaming 5 MiB limit rather than buffering first. HTTP extraction uses LinkeDOM, Readability, and Turndown with an RSC fallback; binaries and media return `unsupported_content`.
174
+ - Neither entry point blocks localhost, private addresses, or cloud metadata URLs by design. Untrusted callers must add their own allowlist, isolation, or egress policy; the gateway targets a private single-user deployment behind its Bearer key.
335
175
 
336
- Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) before opening an issue or pull request. Report security vulnerabilities privately according to [SECURITY.md](SECURITY.md).
176
+ ## Scope
337
177
 
338
- Licensed under MIT. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for third-party and upstream attribution.
178
+ Provider and extraction implementations draw heavily on `pi-web-access`; Pi-specific registration, tool protocols, and UI are excluded. See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md). Licensed under MIT with attribution in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).