@tonydua/dsh-web-search-exa 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +48 -10
- package/README.zh.md +41 -10
- package/cordis.patch.yml +17 -0
- package/lib/index.js +66 -22
- package/lib/types/index.d.ts +26 -10
- package/package.json +17 -9
package/README.i18n.yaml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record.
|
|
4
|
-
README.md:
|
|
5
|
-
README.zh.md:
|
|
4
|
+
README.md: 9dc939e621c5ceacc4d0dffbc0eb920f039e2cf1
|
|
5
|
+
README.zh.md: 4caca6729a7fd25894ecd9ddc6c8c3565e4e8605
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
[](https://github.com/TonyDua/dsh-web-search-exa)
|
|
9
9
|
[](https://github.com/TonyDua/dsh-web-search-exa)
|
|
10
10
|
[](package.json)
|
|
11
|
+
[](https://www.npmjs.com/package/@deepseek-ai/dsh)
|
|
11
12
|
|
|
12
13
|
> Zero-config [Exa](https://exa.ai) web search for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh):
|
|
13
14
|
> **no API key required** — a `WebSearchProvider` for the `ctx.web` seam with an
|
|
@@ -15,6 +16,20 @@
|
|
|
15
16
|
|
|
16
17
|
Built with [deepseek-v4-flash](https://api-docs.deepseek.com) inside DeepSeek Harness (dsh).
|
|
17
18
|
|
|
19
|
+
## Supported versions
|
|
20
|
+
|
|
21
|
+
`@tonydua/dsh-web-search-exa@0.1.4` is tested and supported with:
|
|
22
|
+
|
|
23
|
+
- `@deepseek-ai/dsh` `0.1.2-rc.1` (the current npm `latest` release)
|
|
24
|
+
- `@deepseek-ai/dsh-web` `0.1.2-rc.1`
|
|
25
|
+
- `@deepseek-ai/dsh-settings` `0.1.2-rc.1` (optional; enables live Settings integration)
|
|
26
|
+
- `@deepseek-ai/dsh-launch-environment` `0.1.2-rc.1`
|
|
27
|
+
- `@deepseek-ai/cordis` `4.0.2`
|
|
28
|
+
- Node.js `>=18`
|
|
29
|
+
|
|
30
|
+
The dsh `0.1.2-rc.1` API is the compatibility baseline. The `0.1.5-alpha.1`
|
|
31
|
+
alpha line is not part of this release's tested support matrix.
|
|
32
|
+
|
|
18
33
|
## Features
|
|
19
34
|
|
|
20
35
|
- 🆓 **Zero-config, keyless by default** — searches route through Exa's hosted MCP
|
|
@@ -45,7 +60,7 @@ the official one does not have, and keeps the same keyed REST behavior.
|
|
|
45
60
|
| Zero-config install | ❌ | ✅ |
|
|
46
61
|
| Provider id | `exa` (fixed) | `exa` by default, **configurable via `providerId`** |
|
|
47
62
|
| Cordis plugin name | `web-search-exa` | `web-search-exa` |
|
|
48
|
-
| Config keys | `apiKey`, `baseURL`, `searchType`, `numResults`, `highlightsPerResult` | `apiKey`, `apiKeyEnv`, `
|
|
63
|
+
| Config keys | `apiKey`, `baseURL`, `searchType`, `numResults`, `highlightsPerResult` | `apiKey`, `apiKeyEnv`, `baseURL`, `apiURL` (legacy), `mcpURL`, `searchType`, `numResults`, `highlightsPerResult`, `providerId` |
|
|
49
64
|
|
|
50
65
|
## Which one should I use?
|
|
51
66
|
|
|
@@ -61,7 +76,7 @@ the official one does not have, and keeps the same keyed REST behavior.
|
|
|
61
76
|
|
|
62
77
|
| Condition | Path | Endpoint |
|
|
63
78
|
|---|---|---|
|
|
64
|
-
| `apiKey` / `EXA_API_KEY` set | REST `POST /search` with `Authorization: Bearer` | `https://api.exa.ai/search` (configurable) |
|
|
79
|
+
| `apiKey` / `EXA_API_KEY` set | REST `POST /search` with `Authorization: Bearer` | `https://api.exa.ai/search` (`baseURL` configurable) |
|
|
65
80
|
| No key configured | Anonymous MCP `tools/call web_search_exa` (JSON-RPC 2.0, no credentials) | `https://mcp.exa.ai/mcp` (configurable) |
|
|
66
81
|
|
|
67
82
|
The anonymous MCP path sends no credentials; attribution rides the
|
|
@@ -73,6 +88,29 @@ key (which also switches to the REST path automatically).
|
|
|
73
88
|
|
|
74
89
|
## Installation (into a dsh profile)
|
|
75
90
|
|
|
91
|
+
**One command from npm** (v0.1.4+ ships the `dsh.bundle` manifest — the bundle
|
|
92
|
+
patch inserts the provider row, so no manual patch editing is needed):
|
|
93
|
+
|
|
94
|
+
```powershell
|
|
95
|
+
dsh plugin --profile web add @tonydua/dsh-web-search-exa
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Restart `dsh web`. **Without an API key** the official DeepSeek search
|
|
99
|
+
provider is unavailable, so the seam auto-selects this provider — fully
|
|
100
|
+
zero-config. **With a key configured**, select Exa explicitly in your own
|
|
101
|
+
`$DSH_HOME/profiles/web/cordis.patch.yml` (applied after bundle patches):
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
- id: web
|
|
105
|
+
name: '@deepseek-ai/dsh-web'
|
|
106
|
+
config:
|
|
107
|
+
searchProvider: exa
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
…or at runtime with the environment variable `$DSH_WEB_SEARCH_PROVIDER=exa`.
|
|
111
|
+
|
|
112
|
+
**Local development checkout:**
|
|
113
|
+
|
|
76
114
|
```powershell
|
|
77
115
|
dsh plugin --profile web add ../plugins/dsh-web-search-exa
|
|
78
116
|
```
|
|
@@ -91,12 +129,6 @@ Then enable the provider and select it. Either merge into
|
|
|
91
129
|
searchProvider: exa
|
|
92
130
|
```
|
|
93
131
|
|
|
94
|
-
…or pass `patches/exa-anon-search.patch.yml` as an overlay:
|
|
95
|
-
|
|
96
|
-
```powershell
|
|
97
|
-
dsh --profile web --patch patches/exa-anon-search.patch.yml
|
|
98
|
-
```
|
|
99
|
-
|
|
100
132
|
Alternatively, select the provider at runtime with the environment variable
|
|
101
133
|
`$DSH_WEB_SEARCH_PROVIDER=exa` (no config edit needed).
|
|
102
134
|
|
|
@@ -121,7 +153,8 @@ error such as `Cannot read properties of undefined (reading 'prepare')`.
|
|
|
121
153
|
| `providerId` | `exa` | Provider id registered into `ctx.web`. Only change it when both this and the official package are installed (see next section). |
|
|
122
154
|
| `apiKey` | unset | Literal Exa API key. Empty/missing enables the anonymous MCP path. |
|
|
123
155
|
| `apiKeyEnv` | `EXA_API_KEY` | Environment variable consulted when no literal `apiKey` is set. |
|
|
124
|
-
| `
|
|
156
|
+
| `baseURL` | `https://api.exa.ai` | Exa API base URL; `/search` is appended for the keyed REST path. Matches the official dsh provider. |
|
|
157
|
+
| `apiURL` | unset | Deprecated full REST endpoint alias. If set, it takes precedence over `baseURL`. |
|
|
125
158
|
| `mcpURL` | `https://mcp.exa.ai/mcp` | Exa hosted MCP endpoint (anonymous path). |
|
|
126
159
|
| `searchType` | `auto` | REST retrieval mode: `auto` / `keyword` / `neural`. |
|
|
127
160
|
| `numResults` | unset | Default result count when the request carries no `maxResults`. |
|
|
@@ -172,7 +205,7 @@ plugin namespaces. What is true today:
|
|
|
172
205
|
as `web-search-exa` (`@tonydua/dsh-web-search-exa`) once enabled — the
|
|
173
206
|
inventory reads the live Cordis loader, no extra code needed.
|
|
174
207
|
- **Settings namespace** (server-side): the plugin registers the
|
|
175
|
-
`web-search-exa` section via `
|
|
208
|
+
`web-search-exa` section via the current `ctx.settings.installSection` API, so the data layer is
|
|
176
209
|
writable — but **no client card binds to it**, so nothing shows in the UI.
|
|
177
210
|
The built-in "Web search" card edits the official
|
|
178
211
|
`web-search-deepseek` namespace, not this plugin.
|
|
@@ -210,6 +243,11 @@ only; a UI card is planned for the next version. Configure through
|
|
|
210
243
|
`cordis.patch.yml` or environment variables for now (see
|
|
211
244
|
[In the Web panel](#in-the-web-panel)).
|
|
212
245
|
|
|
246
|
+
**Q: Which dsh versions are supported?**
|
|
247
|
+
This release supports dsh `0.1.2-rc.1` and its matching `dsh-web`,
|
|
248
|
+
`dsh-settings`, and `dsh-launch-environment` packages. The `0.1.5-alpha.1`
|
|
249
|
+
line is not tested by this release.
|
|
250
|
+
|
|
213
251
|
## Acknowledgements
|
|
214
252
|
|
|
215
253
|
The anonymous MCP integration follows the `web_search` implementation in
|
package/README.zh.md
CHANGED
|
@@ -8,12 +8,26 @@
|
|
|
8
8
|
[](https://github.com/TonyDua/dsh-web-search-exa)
|
|
9
9
|
[](https://github.com/TonyDua/dsh-web-search-exa)
|
|
10
10
|
[](package.json)
|
|
11
|
+
[](https://www.npmjs.com/package/@deepseek-ai/dsh)
|
|
11
12
|
|
|
12
13
|
> 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)提供**零配置**的 [Exa](https://exa.ai) 网页搜索:
|
|
13
14
|
> **无需 API key** —— 一个 `ctx.web` seam 的 `WebSearchProvider`,内置匿名 MCP 兜底 + 带 key 的 REST 路径。
|
|
14
15
|
|
|
15
16
|
使用 [deepseek-v4-flash](https://api-docs.deepseek.com) 在 DeepSeek Harness(dsh)内开发。
|
|
16
17
|
|
|
18
|
+
## 当前支持的版本
|
|
19
|
+
|
|
20
|
+
`@tonydua/dsh-web-search-exa@0.1.4` 已针对以下版本测试并支持:
|
|
21
|
+
|
|
22
|
+
- `@deepseek-ai/dsh` `0.1.2-rc.1`(当前 npm `latest` 发布线)
|
|
23
|
+
- `@deepseek-ai/dsh-web` `0.1.2-rc.1`
|
|
24
|
+
- `@deepseek-ai/dsh-settings` `0.1.2-rc.1`(可选;用于启用实时 Settings 集成)
|
|
25
|
+
- `@deepseek-ai/dsh-launch-environment` `0.1.2-rc.1`
|
|
26
|
+
- `@deepseek-ai/cordis` `4.0.2`
|
|
27
|
+
- Node.js `>=18`
|
|
28
|
+
|
|
29
|
+
dsh `0.1.2-rc.1` 是本版本的兼容基线;`0.1.5-alpha.1` alpha 线不在本版本的测试支持矩阵内。
|
|
30
|
+
|
|
17
31
|
## 特性
|
|
18
32
|
|
|
19
33
|
- 🆓 **零配置、默认免 key** —— 搜索经由 Exa 官方托管的 MCP 服务器(`mcp.exa.ai/mcp`),**完全不携带凭据**(Exa 官方提供的免认证公共 MCP,有限流)。
|
|
@@ -34,7 +48,7 @@ DeepSeek Harness 自带官方 Exa 提供方 [`@deepseek-ai/dsh-web-search-exa`](
|
|
|
34
48
|
| 零配置安装 | ❌ | ✅ |
|
|
35
49
|
| Provider id | `exa`(固定) | 默认 `exa`,**可用 `providerId` 配置** |
|
|
36
50
|
| Cordis 插件名 | `web-search-exa` | `web-search-exa` |
|
|
37
|
-
| 配置键 | `apiKey`、`baseURL`、`searchType`、`numResults`、`highlightsPerResult` | `apiKey`、`apiKeyEnv`、`apiURL
|
|
51
|
+
| 配置键 | `apiKey`、`baseURL`、`searchType`、`numResults`、`highlightsPerResult` | `apiKey`、`apiKeyEnv`、`baseURL`、`apiURL`(旧版)、`mcpURL`、`searchType`、`numResults`、`highlightsPerResult`、`providerId` |
|
|
38
52
|
|
|
39
53
|
## 我该用哪个?
|
|
40
54
|
|
|
@@ -46,13 +60,32 @@ DeepSeek Harness 自带官方 Exa 提供方 [`@deepseek-ai/dsh-web-search-exa`](
|
|
|
46
60
|
|
|
47
61
|
| 条件 | 路径 | 端点 |
|
|
48
62
|
|---|---|---|
|
|
49
|
-
| 配置了 `apiKey` / `EXA_API_KEY` | REST `POST /search`,`Authorization: Bearer` | `https://api.exa.ai/search
|
|
63
|
+
| 配置了 `apiKey` / `EXA_API_KEY` | REST `POST /search`,`Authorization: Bearer` | `https://api.exa.ai/search`(可用 `baseURL` 配置) |
|
|
50
64
|
| 未配置任何 key | 匿名 MCP `tools/call web_search_exa`(JSON-RPC 2.0,无凭据) | `https://mcp.exa.ai/mcp`(可配置) |
|
|
51
65
|
|
|
52
66
|
匿名 MCP 路径不发送任何凭据,来源标识通过 `x-exa-source: dsh-anything` 头携带。结果按 seam 的 `WebSearchSource` 形状规范化(`url`、`title`、`snippet`、`publishedAt`),`maxResults` 由 seam 在返回路径上强制执行。匿名使用受 Exa 限流:HTTP 429 会以 `WEB_PROVIDER_ERROR` 呈现,并提示配置 API key(配置后自动切换到 REST 路径)。
|
|
53
67
|
|
|
54
68
|
## 安装(装入 dsh profile)
|
|
55
69
|
|
|
70
|
+
**一条命令从 npm 安装**(v0.1.4+ 自带 `dsh.bundle` manifest——bundle patch 会自动插入 provider 行,无需手动改 patch):
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
dsh plugin --profile web add @tonydua/dsh-web-search-exa
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
重启 `dsh web` 生效。**无 API key 时**官方 DeepSeek 搜索提供方不可用,seam 会自动选中本插件——完全零配置。**配了 key 时**,需在你的 `$DSH_HOME/profiles/web/cordis.patch.yml`(在 bundle patch 之后应用)里显式选中 Exa:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
- id: web
|
|
80
|
+
name: '@deepseek-ai/dsh-web'
|
|
81
|
+
config:
|
|
82
|
+
searchProvider: exa
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
…或用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa` 在运行时选中。
|
|
86
|
+
|
|
87
|
+
**本地开发目录:**
|
|
88
|
+
|
|
56
89
|
```powershell
|
|
57
90
|
dsh plugin --profile web add ../plugins/dsh-web-search-exa
|
|
58
91
|
```
|
|
@@ -70,12 +103,6 @@ dsh plugin --profile web add ../plugins/dsh-web-search-exa
|
|
|
70
103
|
searchProvider: exa
|
|
71
104
|
```
|
|
72
105
|
|
|
73
|
-
或把 `patches/exa-anon-search.patch.yml` 作为覆盖层传入:
|
|
74
|
-
|
|
75
|
-
```powershell
|
|
76
|
-
dsh --profile web --patch patches/exa-anon-search.patch.yml
|
|
77
|
-
```
|
|
78
|
-
|
|
79
106
|
也可以不修改配置,直接用环境变量 `$DSH_WEB_SEARCH_PROVIDER=exa` 在运行时选中该提供方。
|
|
80
107
|
|
|
81
108
|
重启 `dsh web` 生效。模型侧的 `web_search` 工具随即走该提供方,无需改任何工具配置。
|
|
@@ -91,7 +118,8 @@ dsh --profile web --patch patches/exa-anon-search.patch.yml
|
|
|
91
118
|
| `providerId` | `exa` | 注册进 `ctx.web` 的提供方 id。仅当本包与官方包同时安装时才需要改(见下一节)。 |
|
|
92
119
|
| `apiKey` | 未设置 | Exa API 密钥字面值。为空/缺失时启用匿名 MCP 路径。 |
|
|
93
120
|
| `apiKeyEnv` | `EXA_API_KEY` | 未设置字面 `apiKey` 时读取的环境变量名。 |
|
|
94
|
-
| `
|
|
121
|
+
| `baseURL` | `https://api.exa.ai` | Exa API 基础 URL;带 key 的 REST 路径会追加 `/search`,与官方 dsh 提供方一致。 |
|
|
122
|
+
| `apiURL` | 未设置 | 已弃用的完整 REST 端点别名;设置后优先于 `baseURL`。 |
|
|
95
123
|
| `mcpURL` | `https://mcp.exa.ai/mcp` | Exa 托管 MCP 端点(匿名路径使用)。 |
|
|
96
124
|
| `searchType` | `auto` | REST 检索模式:`auto` / `keyword` / `neural`。 |
|
|
97
125
|
| `numResults` | 未设置 | 请求未携带 `maxResults` 时的默认结果数。 |
|
|
@@ -126,7 +154,7 @@ dsh --profile web --patch patches/exa-anon-search.patch.yml
|
|
|
126
154
|
**状态:本版本的配置入口在 profile 补丁层,不在 Web UI —— 没有可编辑的界面入口。** Settings UI 只渲染客户端插件为固定命名空间(`shell`、`agent-loop`、`web-search-deepseek`)手工注册的卡片,对任意插件命名空间没有通用表单。当前实际情况:
|
|
127
155
|
|
|
128
156
|
- **插件清单**(Settings → Plugins):启用后自动出现 `web-search-exa`(`@tonydua/dsh-web-search-exa`)条目 —— 清单直接读取 Cordis loader 的实时条目,无需额外代码。
|
|
129
|
-
-
|
|
157
|
+
- **设置命名空间**(服务端):插件通过当前的 `ctx.settings.installSection` API 注册了 `web-search-exa` 段,数据层可写——但**没有任何客户端卡片绑定它**,所以界面上不显示。内置的 "Web search" 卡片编辑的是官方 `web-search-deepseek` 命名空间,与本插件无关。
|
|
130
158
|
- **现在怎么改配置**:编辑 `$DSH_HOME/profiles/web/cordis.patch.yml` 里本插件的 `config`(字段与默认值见上方配置表),重启 `dsh web`;或用环境变量 `EXA_API_KEY` / `$DSH_WEB_SEARCH_PROVIDER`。`apiKey` 标记了 `role('secret')`,任何 `describe()` 响应都不会暴露其值。
|
|
131
159
|
- **搜索结果卡片**:`web_search` 调用经 `dsh-tool-web` 照常渲染 `web` 结果卡片(来源、摘要、日期),与提供方无关 —— 匿名 Exa 的结果与 DeepSeek 搜索显示完全一致。
|
|
132
160
|
|
|
@@ -146,6 +174,9 @@ dsh --profile web --patch patches/exa-anon-search.patch.yml
|
|
|
146
174
|
**Q: 为什么 Web UI 里没有设置入口?**
|
|
147
175
|
本版本只在服务端注册了 `web-search-exa` 设置命名空间;UI 卡片计划在下一版本提供。现阶段通过 `cordis.patch.yml` 或环境变量配置(见[在 Web 面板中的呈现](#在-web-面板中的呈现))。
|
|
148
176
|
|
|
177
|
+
**Q: 支持哪些 dsh 版本?**
|
|
178
|
+
本版本支持 dsh `0.1.2-rc.1` 及其匹配的 `dsh-web`、`dsh-settings`、`dsh-launch-environment` 包;`0.1.5-alpha.1` 线未经本版本测试。
|
|
179
|
+
|
|
149
180
|
## 致谢(Acknowledgements)
|
|
150
181
|
|
|
151
182
|
匿名 MCP 接入方式参考了 [can1357/oh-my-pi](https://github.com/can1357/oh-my-pi) 的 `web_search` 实现(`packages/coding-agent/src/web/search/providers/exa.ts` 与 `src/exa/mcp-client.ts`)以及 [`@oh-my-pi/exa`](https://www.npmjs.com/package/@oh-my-pi/exa) 插件:同样的"有 key 走 REST、无 key 走免凭据 `mcp.exa.ai/mcp`"策略、同样的 `x-exa-source` 来源头、同样的 `Title:` 分节响应解析。感谢 oh-my-pi(omp)项目率先打通了零配置的 Exa 接入。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# The dsh-web-search-exa bundle patch.
|
|
2
|
+
#
|
|
3
|
+
# Inserts the zero-config Exa search provider into the profile. It must NOT
|
|
4
|
+
# re-define rows owned by official bundle patches (e.g. dsh-base's `- id: web`
|
|
5
|
+
# row): duplicate ids inside the bundle group fail the loader, while the
|
|
6
|
+
# user's own profile cordis.patch.yml (applied last) may override rows by id.
|
|
7
|
+
#
|
|
8
|
+
# Selection: with no API key the official deepseek provider is unavailable, so
|
|
9
|
+
# the seam auto-selects this provider (zero-config). Keyed users who prefer Exa
|
|
10
|
+
# select it explicitly in their own profile patch or via
|
|
11
|
+
# DSH_WEB_SEARCH_PROVIDER=exa (see README).
|
|
12
|
+
- insert:
|
|
13
|
+
- id: web-search-exa
|
|
14
|
+
name: '@tonydua/dsh-web-search-exa'
|
|
15
|
+
config:
|
|
16
|
+
apiKeyEnv: EXA_API_KEY
|
|
17
|
+
# providerId: exa-anon # uncomment to coexist with the official package
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* search routes through Exa's hosted MCP server (`https://mcp.exa.ai/mcp`) via
|
|
7
7
|
* JSON-RPC 2.0 with no credentials — Exa's documented unauthenticated public
|
|
8
8
|
* MCP fallback (rate-limited). With a key, the lighter REST endpoint
|
|
9
|
-
* (`POST {
|
|
9
|
+
* (`POST {baseURL}/search`) is used instead, mirroring
|
|
10
|
+
* `@deepseek-ai/dsh-web-search-exa`.
|
|
10
11
|
*
|
|
11
12
|
* The anonymous-MCP strategy and its response parsing follow the `web_search`
|
|
12
13
|
* implementation in can1357/oh-my-pi (see README acknowledgements).
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
* through the profile patch layer (see README "In the Web panel").
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
|
-
import {
|
|
23
|
+
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
23
24
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
24
25
|
import z from "@deepseek-ai/schemastery";
|
|
25
26
|
|
|
@@ -28,7 +29,9 @@ const DEFAULT_PROVIDER_ID = "exa";
|
|
|
28
29
|
/** Backward-compatible alias for the default provider id. */
|
|
29
30
|
const PROVIDER_ID = DEFAULT_PROVIDER_ID;
|
|
30
31
|
/** Exa REST search endpoint; used only when an API key is configured. */
|
|
31
|
-
const
|
|
32
|
+
const DEFAULT_BASE_URL = "https://api.exa.ai";
|
|
33
|
+
/** Legacy full REST endpoint; `baseURL` is the canonical dsh-compatible option. */
|
|
34
|
+
const DEFAULT_API_URL = `${DEFAULT_BASE_URL}/search`;
|
|
32
35
|
/** Exa hosted MCP endpoint; the anonymous fallback path. */
|
|
33
36
|
const DEFAULT_MCP_URL = "https://mcp.exa.ai/mcp";
|
|
34
37
|
/** Environment variable consulted when no literal `apiKey` is configured. */
|
|
@@ -42,11 +45,11 @@ const MCP_TOOL = "web_search_exa";
|
|
|
42
45
|
/** Attribution header sent on anonymous MCP requests. Bump with the version. */
|
|
43
46
|
const MCP_SOURCE = "dsh-anything";
|
|
44
47
|
/** User agent for REST requests. */
|
|
45
|
-
const USER_AGENT = "deepseek-harness-exa/0.1.
|
|
48
|
+
const USER_AGENT = "deepseek-harness-exa/0.1.4";
|
|
46
49
|
/** Snippet cap for text-derived snippets (matching oh-my-pi's choice). */
|
|
47
50
|
const MAX_SNIPPET_CHARS = 500;
|
|
48
51
|
/** Settings namespace carrying this provider's configuration. */
|
|
49
|
-
const SETTINGS_NAMESPACE =
|
|
52
|
+
const SETTINGS_NAMESPACE = "web-search-exa";
|
|
50
53
|
|
|
51
54
|
/** True for a positive whole number (cheap local config check). */
|
|
52
55
|
function isPositiveInteger(value) {
|
|
@@ -69,13 +72,24 @@ function throwIfAborted(signal) {
|
|
|
69
72
|
* Resolve the API key: literal config first, then the environment variable.
|
|
70
73
|
* `undefined` means the anonymous MCP path is used.
|
|
71
74
|
*/
|
|
72
|
-
function
|
|
75
|
+
function resolveApiKeyFromProcess(options) {
|
|
73
76
|
if (options.apiKey != null && options.apiKey.length > 0) return options.apiKey;
|
|
74
77
|
const fromEnv = process.env[options.apiKeyEnv];
|
|
75
78
|
if (fromEnv != null && fromEnv.length > 0) return fromEnv;
|
|
76
79
|
return undefined;
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a key against dsh's immutable launch-environment snapshot. The
|
|
84
|
+
* process fallback keeps direct library use and older hosts working.
|
|
85
|
+
*/
|
|
86
|
+
function resolveApiKey(options, environment) {
|
|
87
|
+
if (options.apiKey != null && options.apiKey.length > 0) return options.apiKey;
|
|
88
|
+
const fromEnvironment = environment?.get(options.apiKeyEnv)?.value;
|
|
89
|
+
if (fromEnvironment != null && fromEnvironment.length > 0) return fromEnvironment;
|
|
90
|
+
return resolveApiKeyFromProcess(options);
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
// ── REST path (with API key) ────────────────────────────────────────────────
|
|
80
94
|
|
|
81
95
|
/**
|
|
@@ -210,11 +224,13 @@ function mapMcpSections(sections) {
|
|
|
210
224
|
* take effect on the next search.
|
|
211
225
|
*/
|
|
212
226
|
function resolveOptions(section) {
|
|
227
|
+
const baseURL = section.baseURL ?? DEFAULT_BASE_URL;
|
|
213
228
|
return {
|
|
214
229
|
providerId: section.providerId ?? DEFAULT_PROVIDER_ID,
|
|
215
230
|
apiKey: section.apiKey ?? "",
|
|
216
231
|
apiKeyEnv: section.apiKeyEnv ?? DEFAULT_API_KEY_ENV,
|
|
217
|
-
|
|
232
|
+
baseURL,
|
|
233
|
+
apiURL: section.apiURL ?? `${baseURL.replace(/\/+$/, "")}/search`,
|
|
218
234
|
mcpURL: section.mcpURL ?? DEFAULT_MCP_URL,
|
|
219
235
|
searchType: section.searchType ?? DEFAULT_SEARCH_TYPE,
|
|
220
236
|
numResults: section.numResults,
|
|
@@ -222,8 +238,16 @@ function resolveOptions(section) {
|
|
|
222
238
|
};
|
|
223
239
|
}
|
|
224
240
|
|
|
241
|
+
/** Resolve the keyed REST endpoint from either the current or legacy option shape. */
|
|
242
|
+
function resolveSearchURL(options) {
|
|
243
|
+
if (options.apiURL != null) return options.apiURL;
|
|
244
|
+
const baseURL = options.baseURL ?? DEFAULT_BASE_URL;
|
|
245
|
+
return `${baseURL.replace(/\/+$/, "")}/search`;
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
class ExaSearchProvider {
|
|
226
249
|
resolveOptions;
|
|
250
|
+
resolveApiKey;
|
|
227
251
|
id;
|
|
228
252
|
|
|
229
253
|
/**
|
|
@@ -231,22 +255,28 @@ class ExaSearchProvider {
|
|
|
231
255
|
* operation, snapshotted once at each operation's entry so one search
|
|
232
256
|
* never mixes two settings sections (same pattern as the official
|
|
233
257
|
* DeepSeek provider).
|
|
258
|
+
* @param resolveApiKey - optional key resolver; dsh hosts pass their
|
|
259
|
+
* launch-environment snapshot while direct users retain process.env fallback.
|
|
234
260
|
*/
|
|
235
|
-
constructor(resolveOptions) {
|
|
261
|
+
constructor(resolveOptions, resolveApiKey = resolveApiKeyFromProcess) {
|
|
236
262
|
this.resolveOptions = resolveOptions;
|
|
263
|
+
this.resolveApiKey = resolveApiKey;
|
|
237
264
|
this.id = resolveOptions().providerId ?? PROVIDER_ID;
|
|
238
265
|
}
|
|
239
266
|
|
|
240
|
-
/** The anonymous MCP path needs no credentials, so
|
|
267
|
+
/** The anonymous MCP path needs no credentials, so only local options gate use. */
|
|
241
268
|
available() {
|
|
242
269
|
const options = this.resolveOptions();
|
|
243
|
-
return URL.canParse(
|
|
270
|
+
return URL.canParse(resolveSearchURL(options))
|
|
271
|
+
&& URL.canParse(options.mcpURL)
|
|
272
|
+
&& isPositiveInteger(options.highlightsPerResult)
|
|
273
|
+
&& (options.numResults === undefined || isPositiveInteger(options.numResults));
|
|
244
274
|
}
|
|
245
275
|
|
|
246
276
|
async search(request, signal) {
|
|
247
277
|
throwIfAborted(signal);
|
|
248
278
|
const options = this.resolveOptions();
|
|
249
|
-
const apiKey = resolveApiKey(options);
|
|
279
|
+
const apiKey = this.resolveApiKey(options);
|
|
250
280
|
return apiKey !== undefined
|
|
251
281
|
? await this.#restSearch(request, apiKey, options, signal)
|
|
252
282
|
: await this.#anonymousMcpSearch(request, options, signal);
|
|
@@ -256,9 +286,10 @@ class ExaSearchProvider {
|
|
|
256
286
|
async #restSearch(request, apiKey, options, signal) {
|
|
257
287
|
throwIfAborted(signal);
|
|
258
288
|
const numResults = request.maxResults ?? options.numResults;
|
|
289
|
+
const apiURL = resolveSearchURL(options);
|
|
259
290
|
let response;
|
|
260
291
|
try {
|
|
261
|
-
response = await fetch(
|
|
292
|
+
response = await fetch(apiURL, {
|
|
262
293
|
method: "POST",
|
|
263
294
|
redirect: "error",
|
|
264
295
|
headers: {
|
|
@@ -387,8 +418,13 @@ const Config = z.object({
|
|
|
387
418
|
apiKey: z.string().role("secret"),
|
|
388
419
|
/** Environment variable consulted when no literal `apiKey` is configured. */
|
|
389
420
|
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
390
|
-
/**
|
|
391
|
-
|
|
421
|
+
/** Exa API base URL; `/search` is appended for the keyed REST path. */
|
|
422
|
+
baseURL: z.string().default(DEFAULT_BASE_URL),
|
|
423
|
+
/**
|
|
424
|
+
* Legacy full REST endpoint. When set, it takes precedence over `baseURL`;
|
|
425
|
+
* new configurations should use `baseURL` to match the official provider.
|
|
426
|
+
*/
|
|
427
|
+
apiURL: z.string(),
|
|
392
428
|
/** Exa hosted MCP endpoint, used by the anonymous fallback. */
|
|
393
429
|
mcpURL: z.string().default(DEFAULT_MCP_URL),
|
|
394
430
|
/** REST retrieval mode: `auto`, `keyword`, or `neural`. */
|
|
@@ -400,24 +436,32 @@ const Config = z.object({
|
|
|
400
436
|
});
|
|
401
437
|
|
|
402
438
|
/**
|
|
403
|
-
* Register the Exa search provider with `ctx.web` and
|
|
404
|
-
*
|
|
439
|
+
* Register the Exa search provider with `ctx.web` and, when the optional
|
|
440
|
+
* settings service is mounted, install its Settings section using the dsh
|
|
441
|
+
* 0.1.2 API.
|
|
405
442
|
*/
|
|
406
443
|
function apply(ctx, config) {
|
|
407
444
|
let current = () => config;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
445
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
446
|
+
settingsCtx.settings.installSection(ctx, SETTINGS_NAMESPACE, Config, config, {
|
|
447
|
+
setSource: (source) => {
|
|
448
|
+
current = source;
|
|
449
|
+
},
|
|
450
|
+
onChange: () => {},
|
|
451
|
+
});
|
|
413
452
|
});
|
|
414
|
-
|
|
453
|
+
const environment = launchEnvironmentOf(ctx);
|
|
454
|
+
ctx.web.registerSearchProvider(new ExaSearchProvider(
|
|
455
|
+
() => resolveOptions(current()),
|
|
456
|
+
(options) => resolveApiKey(options, environment),
|
|
457
|
+
));
|
|
415
458
|
}
|
|
416
459
|
|
|
417
460
|
export {
|
|
418
461
|
Config,
|
|
419
462
|
DEFAULT_API_KEY_ENV,
|
|
420
463
|
DEFAULT_API_URL,
|
|
464
|
+
DEFAULT_BASE_URL,
|
|
421
465
|
DEFAULT_HIGHLIGHTS_PER_RESULT,
|
|
422
466
|
DEFAULT_MCP_URL,
|
|
423
467
|
DEFAULT_PROVIDER_ID,
|
package/lib/types/index.d.ts
CHANGED
|
@@ -7,11 +7,15 @@ import type { WebSearchProvider } from '@deepseek-ai/dsh-web';
|
|
|
7
7
|
|
|
8
8
|
/** Config schema type (fields are optional at the load boundary). */
|
|
9
9
|
export interface ExaSearchProviderConfig {
|
|
10
|
+
/** Provider id registered into `ctx.web`; defaults to `exa`. */
|
|
11
|
+
providerId?: string;
|
|
10
12
|
/** Literal Exa API key; empty/missing enables the anonymous MCP path. */
|
|
11
13
|
apiKey?: string;
|
|
12
14
|
/** Environment variable consulted when no literal `apiKey` is configured. */
|
|
13
15
|
apiKeyEnv?: string;
|
|
14
|
-
/**
|
|
16
|
+
/** Exa API base URL; `/search` is appended for the keyed REST path. */
|
|
17
|
+
baseURL?: string;
|
|
18
|
+
/** @deprecated Use `baseURL`; this full endpoint remains supported for compatibility. */
|
|
15
19
|
apiURL?: string;
|
|
16
20
|
/** Exa hosted MCP endpoint, used by the anonymous fallback. */
|
|
17
21
|
mcpURL?: string;
|
|
@@ -23,18 +27,26 @@ export interface ExaSearchProviderConfig {
|
|
|
23
27
|
highlightsPerResult?: number;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
/** Fully resolved options accepted by the provider constructor. */
|
|
31
|
+
export interface ExaSearchProviderOptions {
|
|
32
|
+
providerId?: string;
|
|
33
|
+
apiKey: string;
|
|
34
|
+
apiKeyEnv: string;
|
|
35
|
+
baseURL: string;
|
|
36
|
+
apiURL?: string;
|
|
37
|
+
mcpURL: string;
|
|
38
|
+
searchType: 'auto' | 'keyword' | 'neural';
|
|
39
|
+
numResults?: number;
|
|
40
|
+
highlightsPerResult: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
/** The Exa-backed provider with anonymous MCP fallback. */
|
|
27
44
|
export class ExaSearchProvider implements WebSearchProvider {
|
|
28
45
|
readonly id: string;
|
|
29
|
-
constructor(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
mcpURL: string;
|
|
34
|
-
searchType: 'auto' | 'keyword' | 'neural';
|
|
35
|
-
numResults?: number;
|
|
36
|
-
highlightsPerResult: number;
|
|
37
|
-
});
|
|
46
|
+
constructor(
|
|
47
|
+
resolveOptions: () => ExaSearchProviderOptions,
|
|
48
|
+
resolveApiKey?: (options: ExaSearchProviderOptions) => string | undefined,
|
|
49
|
+
);
|
|
38
50
|
available(): boolean;
|
|
39
51
|
search(request: { query: string; maxResults?: number }, signal?: AbortSignal): Promise<{
|
|
40
52
|
sources: ReadonlyArray<{ url: string; title?: string; snippet?: string; publishedAt?: string }>;
|
|
@@ -50,5 +62,9 @@ export const inject: readonly ['web'];
|
|
|
50
62
|
export const Config: import('@deepseek-ai/schemastery').Schema<ExaSearchProviderConfig>;
|
|
51
63
|
/** Settings namespace for the Web panel section. */
|
|
52
64
|
export const SETTINGS_NAMESPACE: string;
|
|
65
|
+
/** Default Exa API base URL; `/search` is appended. */
|
|
66
|
+
export const DEFAULT_BASE_URL: string;
|
|
67
|
+
/** Legacy default full REST endpoint. */
|
|
68
|
+
export const DEFAULT_API_URL: string;
|
|
53
69
|
/** Register the Exa search provider with `ctx.web` and install its Settings section. */
|
|
54
70
|
export function apply(ctx: Context, config: ExaSearchProviderConfig): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonydua/dsh-web-search-exa",
|
|
3
3
|
"description": "Zero-config Exa web search provider for DeepSeek Harness (dsh): keyless anonymous MCP fallback (mcp.exa.ai/mcp) plus keyed REST search — a drop-in WebSearchProvider for the ctx.web seam, no API key required.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
@@ -36,8 +36,14 @@
|
|
|
36
36
|
"./package.json": "./package.json"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
|
-
"lib"
|
|
39
|
+
"lib",
|
|
40
|
+
"cordis.patch.yml"
|
|
40
41
|
],
|
|
42
|
+
"dsh": {
|
|
43
|
+
"bundle": {
|
|
44
|
+
"patch": "./cordis.patch.yml"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
41
47
|
"publishConfig": {
|
|
42
48
|
"access": "public"
|
|
43
49
|
},
|
|
@@ -45,17 +51,19 @@
|
|
|
45
51
|
"test": "node --test test/index.test.js"
|
|
46
52
|
},
|
|
47
53
|
"peerDependencies": {
|
|
48
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
55
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.2-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-web": "^0.1.2-rc.1"
|
|
51
58
|
},
|
|
52
59
|
"dependencies": {
|
|
53
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
60
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
54
61
|
},
|
|
55
62
|
"devDependencies": {
|
|
56
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
63
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
64
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.2-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-web": "^0.1.2-rc.1"
|
|
59
67
|
},
|
|
60
68
|
"engines": {
|
|
61
69
|
"node": ">=18"
|