@hy-sde-org/dsh-web-search-public 0.1.0-rc.6
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/CONTRIBUTING.md +88 -0
- package/LICENSE +23 -0
- package/README.md +226 -0
- package/SECURITY.md +25 -0
- package/THIRD-PARTY-NOTICES.md +44 -0
- package/cordis.patch.yml +12 -0
- package/dist/engines/duckduckgo.d.ts +18 -0
- package/dist/engines/duckduckgo.d.ts.map +1 -0
- package/dist/engines/duckduckgo.js +77 -0
- package/dist/engines/duckduckgo.js.map +1 -0
- package/dist/engines/ecosia.d.ts +16 -0
- package/dist/engines/ecosia.d.ts.map +1 -0
- package/dist/engines/ecosia.js +42 -0
- package/dist/engines/ecosia.js.map +1 -0
- package/dist/engines/google.d.ts +19 -0
- package/dist/engines/google.d.ts.map +1 -0
- package/dist/engines/google.js +82 -0
- package/dist/engines/google.js.map +1 -0
- package/dist/engines/html.d.ts +41 -0
- package/dist/engines/html.d.ts.map +1 -0
- package/dist/engines/html.js +154 -0
- package/dist/engines/html.js.map +1 -0
- package/dist/engines/http.d.ts +22 -0
- package/dist/engines/http.d.ts.map +1 -0
- package/dist/engines/http.js +31 -0
- package/dist/engines/http.js.map +1 -0
- package/dist/engines/index.d.ts +8 -0
- package/dist/engines/index.d.ts.map +1 -0
- package/dist/engines/index.js +30 -0
- package/dist/engines/index.js.map +1 -0
- package/dist/engines/mojeek.d.ts +16 -0
- package/dist/engines/mojeek.d.ts.map +1 -0
- package/dist/engines/mojeek.js +56 -0
- package/dist/engines/mojeek.js.map +1 -0
- package/dist/engines/startpage.d.ts +19 -0
- package/dist/engines/startpage.d.ts.map +1 -0
- package/dist/engines/startpage.js +78 -0
- package/dist/engines/startpage.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/invariant.d.ts +16 -0
- package/dist/invariant.d.ts.map +1 -0
- package/dist/invariant.js +22 -0
- package/dist/invariant.js.map +1 -0
- package/dist/provider.d.ts +34 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +87 -0
- package/dist/provider.js.map +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/docs/operations.md +57 -0
- package/package.json +88 -0
- package/scripts/release-public.sh +83 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- Node.js 22.19 or newer
|
|
6
|
+
- npm
|
|
7
|
+
|
|
8
|
+
Install exactly from the lockfile:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm ci
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Validation
|
|
15
|
+
|
|
16
|
+
Every behavioral change needs a direct regression test covering normal, edge, and failure
|
|
17
|
+
behavior. Run the same checks as CI:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run check
|
|
21
|
+
npm test
|
|
22
|
+
npm run build
|
|
23
|
+
npm run lint:package
|
|
24
|
+
npm pack --dry-run
|
|
25
|
+
npm audit --omit=dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Parser tests must use fixture HTML, never the live web — a challenged or redesigned engine
|
|
29
|
+
page must not make the suite red. Live scraping is covered by the opt-in real test only:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
DSH_WEB_SEARCH_REAL_E2E=1 npm run test:real
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The real test needs outbound network access, tolerates engine bot challenges (including the
|
|
36
|
+
403 response Ecosia returns for headless clients), and treats a changed organic-result set as
|
|
37
|
+
expected — record the observed engine behavior (per-engine result counts, challenge states)
|
|
38
|
+
in the commit message when you validate a parser change against live sites.
|
|
39
|
+
|
|
40
|
+
## Public release
|
|
41
|
+
|
|
42
|
+
The package is published as a prebuilt public npm artifact. Do not publish TypeScript sources
|
|
43
|
+
or ask users to approve a Git dependency’s build script.
|
|
44
|
+
|
|
45
|
+
Run the non-publishing release gate from a clean worktree:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run release:check
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The release owner must be a member of the npm `deepseek-ai` organization, enable 2FA, and log
|
|
52
|
+
in locally. Never send an npm password, OTP, or token through chat or commit it to the
|
|
53
|
+
repository. To run the same gate and publish the exact `package.json` version:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm login
|
|
57
|
+
npm run release:publish
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The script refuses a dirty worktree, an unexpected package name, a missing npm login, and a
|
|
61
|
+
version that already exists. It runs the complete validation suite, shows the tarball
|
|
62
|
+
contents, prompts for confirmation, and publishes with `--access public` to the public npm
|
|
63
|
+
registry.
|
|
64
|
+
|
|
65
|
+
After publication, verify the registry artifact rather than the local checkout:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm view @hy-sde-org/dsh-web-search-public version
|
|
69
|
+
dsh plugin --profile web add @hy-sde-org/dsh-web-search-public
|
|
70
|
+
dsh web --dump-config
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Design constraints
|
|
74
|
+
|
|
75
|
+
- Keep the chain credential-free. Never add an API-key path, even as a fallback.
|
|
76
|
+
- Keep engines strictly sequential and fail-forward: zero results, a timed-out engine, and a
|
|
77
|
+
failed request all advance to the next engine; only an all-engine failure may throw.
|
|
78
|
+
- Keep every engine secret-free and header-lean: the browser-shaped `userAgent` constant and
|
|
79
|
+
an `accept` header, nothing more.
|
|
80
|
+
- Keep parsers tolerant: a challenged or redesigned page must parse to zero organic results,
|
|
81
|
+
never throw into the chain.
|
|
82
|
+
- Keep tests fixture-based; the live suite is opt-in (`DSH_WEB_SEARCH_REAL_E2E=1`).
|
|
83
|
+
- Preserve the web seam contracts: providers return fully-formed `WebSearchSource[]` and the
|
|
84
|
+
consumer sets `truncated`; never mark results truncated yourself.
|
|
85
|
+
- Preserve `redirect: "error"` on every engine fetch.
|
|
86
|
+
- Preserve the oh-my-pi attribution and its MIT notice ([THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)) whenever engine code is changed or extended.
|
|
87
|
+
|
|
88
|
+
Changes to these constraints require updating this file and [docs/operations.md](docs/operations.md).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mario Zechner
|
|
4
|
+
Copyright (c) 2025-2026 Can Bölük
|
|
5
|
+
Copyright (c) 2026 hy-sde
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# dsh-web-search-public — credential-free web search for DeepSeek Harness
|
|
2
|
+
|
|
3
|
+
A zero-API-key web search provider for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
|
|
4
|
+
The model-facing `web_search` tool runs a **sequential chain — Startpage first, then
|
|
5
|
+
DuckDuckGo → Ecosia → Google → Mojeek** — and returns the first engine that yields organic
|
|
6
|
+
results. No API keys, no accounts, no fetch provider, no setup.
|
|
7
|
+
|
|
8
|
+
| Identity | Value |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| Package | `@hy-sde-org/dsh-web-search-public` |
|
|
11
|
+
| Plugin id | `web-search-public` |
|
|
12
|
+
| Provider id | `public` |
|
|
13
|
+
| Engines (in order) | `startpage`, `duckduckgo`, `ecosia`, `google`, `mojeek` |
|
|
14
|
+
> **Based on [oh-my-pi](https://github.com/can1357/oh-my-pi)** — the credential-free engine
|
|
15
|
+
> chain and per-engine scrapers are ported from oh-my-pi’s `web/search/providers`
|
|
16
|
+
> (`public.ts`, `startpage.ts`, `duckduckgo.ts`, `ecosia.ts`, `google.ts`, `mojeek.ts`),
|
|
17
|
+
> adapted to the DeepSeek Harness `ctx.web` seam with sequential fail-forward semantics.
|
|
18
|
+
> oh-my-pi is MIT-licensed (Mario Zechner, Can Bölük); see
|
|
19
|
+
> [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
DeepSeek Harness ships search providers that need an API key (DeepSeek, Exa, Perplexity).
|
|
24
|
+
This bundle is the credential-free default: each search is an anonymous request to a public
|
|
25
|
+
search engine, and the chain fails forward. If Startpage is down, rate-limited, or serves a
|
|
26
|
+
bot challenge, the chain automatically tries DuckDuckGo, then Ecosia, then Google, then
|
|
27
|
+
Mojeek. The first engine that returns at least one organic result wins; engines return
|
|
28
|
+
zero results (rather than errors) when challenged, so the chain keeps moving instead of
|
|
29
|
+
failing.
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
- Node.js 22.19 or newer with npm and pnpm on `PATH`;
|
|
34
|
+
- DeepSeek Harness `0.1.0-rc.6` including the standard `dsh` CLI;
|
|
35
|
+
- no API keys — nothing else.
|
|
36
|
+
|
|
37
|
+
Install the Harness CLI and pnpm before continuing:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install --global @deepseek-ai/dsh@0.1.0-rc.6 pnpm
|
|
41
|
+
dsh --version
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
### Route A — published npm package (recommended)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
dsh plugin --profile web add @hy-sde-org/dsh-web-search-public
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Route B — from source (validate this checkout or hack on the plugin)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone <this-repository-url> dsh-web-search-public
|
|
56
|
+
cd dsh-web-search-public
|
|
57
|
+
npm ci
|
|
58
|
+
PACKAGE_TARBALL="$(npm pack --silent)"
|
|
59
|
+
dsh plugin --profile web add "$PWD/$PACKAGE_TARBALL"
|
|
60
|
+
cd ..
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`npm pack` runs the normal `prepack` build and produces a tarball containing `dist/`. A direct
|
|
64
|
+
`github:<this-repo>` dependency does not contain built output and is not a supported install
|
|
65
|
+
path — always install the built tarball (or the published package).
|
|
66
|
+
|
|
67
|
+
### Verify the composed configuration
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
dsh web --dump-config
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The composed tree must show `web.config.searchProvider: public` and a `web-search-public`
|
|
74
|
+
plugin row loading `@hy-sde-org/dsh-web-search-public`.
|
|
75
|
+
|
|
76
|
+
### Run
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
dsh web
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Ask the agent to `web_search` something. The tool returns rendered sources from the first
|
|
83
|
+
engine that answered.
|
|
84
|
+
|
|
85
|
+
### Uninstall
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
dsh plugin --profile web remove @hy-sde-org/dsh-web-search-public
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> **Already shipped?** If your harness checkout already mounts `web-search-public` in its base
|
|
92
|
+
> bundle (a future release may adopt this package as the shipped default), skip installation
|
|
93
|
+
> — it is already the active provider. Adding this bundle on top would duplicate the loader
|
|
94
|
+
> row and fail at boot (“duplicate loader entry id”).
|
|
95
|
+
|
|
96
|
+
## What the bundle does
|
|
97
|
+
|
|
98
|
+
The package declares a DSH bundle (`dsh.bundle.patch` → `cordis.patch.yml`), so `dsh plugin`
|
|
99
|
+
installs it and applies its patch layer to the profile:
|
|
100
|
+
|
|
101
|
+
1. sets `web.config.searchProvider` to `public` (making the credential-free chain the default),
|
|
102
|
+
2. inserts the `web-search-public` plugin row.
|
|
103
|
+
|
|
104
|
+
The keyed DeepSeek route (`web-search-deepseek`, provider id `deepseek-official`) is **not**
|
|
105
|
+
disabled — deployments that prefer it keep their explicit choice by setting
|
|
106
|
+
`web.searchProvider: deepseek-official` and providing the `DEEPSEEK_API_KEY` credential.
|
|
107
|
+
No fetch provider is mounted and `tool-web` keeps `fetch: false`: the model can search, not
|
|
108
|
+
fetch arbitrary URLs, which defers SSRF exposure entirely.
|
|
109
|
+
|
|
110
|
+
## How the chain works
|
|
111
|
+
|
|
112
|
+
`PublicSearchProvider.search()` walks `engines` in configured order:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
startpage ──► duckduckgo ──► ecosia ──► google ──► mojeek
|
|
116
|
+
│ │ │ │ │
|
|
117
|
+
▼ ▼ ▼ ▼ ▼
|
|
118
|
+
1+ results 1+ results 1+ results 1+ results 1+ results → return them
|
|
119
|
+
│ │ │ │ │
|
|
120
|
+
▼ ▼ ▼ ▼ ▼
|
|
121
|
+
0 0 0 0 0 → all failed → WEB_PROVIDER_ERROR
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
On each step the provider opens a fresh `AbortSignal.timeout(timeoutMs)` race, so a hung
|
|
125
|
+
engine cannot pin the call past its budget. The worst case for the default order is
|
|
126
|
+
`5 × timeoutMs` = 50 seconds; `dsh-tool-web` books a 60-second search budget for that.
|
|
127
|
+
|
|
128
|
+
- `available()` is `true` whenever at least one engine is configured — the chain is always
|
|
129
|
+
usable, which is the point.
|
|
130
|
+
- A caller abort (`signal.aborted`) aborts the in-flight engine and throws `WEB_ABORTED`.
|
|
131
|
+
- Only if **every** engine fails does the provider throw `WEB_PROVIDER_ERROR` with each
|
|
132
|
+
engine’s reason (`id: <reason>; …`). Zero organic results, a timed-out engine, and a failed
|
|
133
|
+
HTTP request all count as “this engine has nothing” and move to the next one.
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
All options are optional; constants fill the defaults. Configure via the plugin row’s
|
|
138
|
+
`config:` in your profile patch, or programmatically via `ctx.plugin(...)`.
|
|
139
|
+
|
|
140
|
+
| Option | Default | Purpose |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| `engines` | `startpage, duckduckgo, ecosia, google, mojeek` | engine ids tried in order; unlisted engines stay disabled |
|
|
143
|
+
| `timeoutMs` | `10000` | per-engine transport timeout; the chain worst case is `engines × timeoutMs` |
|
|
144
|
+
| `userAgent` | browser-shaped constant | User-Agent sent to the engines (deliberately not the product UA) |
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
- id: web-search-public
|
|
148
|
+
name: '@hy-sde-org/dsh-web-search-public'
|
|
149
|
+
config:
|
|
150
|
+
engines: [startpage, duckduckgo, google, mojeek] # drop Ecosia, reorder
|
|
151
|
+
timeoutMs: 8000
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`PublicEngineId` values are the exported engine ids; any id outside the known set is dropped
|
|
155
|
+
at startup, and a zero-engine config makes `available()` false (surfacing
|
|
156
|
+
`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`).
|
|
157
|
+
|
|
158
|
+
## Errors
|
|
159
|
+
|
|
160
|
+
Search failures use the web seam’s `WEB_*` codes:
|
|
161
|
+
|
|
162
|
+
| Code | Meaning |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `WEB_PROVIDER_ERROR` | every engine failed; message lists `id: reason; …` |
|
|
165
|
+
| `WEB_ABORTED` | caller aborted mid-search |
|
|
166
|
+
| `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` | configured provider not available |
|
|
167
|
+
| `WEB_PROVIDER_CONFIGURED_MISSING` | configured provider id not registered |
|
|
168
|
+
|
|
169
|
+
## Privacy and security notes
|
|
170
|
+
|
|
171
|
+
- **No credentials.** The provider never reads a key, so a leak cannot happen. Engines
|
|
172
|
+
receive the query, the browser-shaped User-Agent, and nothing else.
|
|
173
|
+
- **Treat queries as public.** Each query is sent to public search engines as an anonymous
|
|
174
|
+
request; do not search for secrets or PII you would not paste into a public search box.
|
|
175
|
+
- **No fetch provider.** `tool-web` `fetch: false` stays: the model cannot be pointed at
|
|
176
|
+
arbitrary URLs, so SSRF and unsafe-content surfaces stay closed.
|
|
177
|
+
- **Redirect policy.** All engine fetches use `redirect: "error"` — redirects are treated as
|
|
178
|
+
failures and advance the chain rather than leak the caller onto an external location.
|
|
179
|
+
- **Challenges are expected.** Startpage, Ecosia, and Google frequently serve bot checks;
|
|
180
|
+
the parsers treat challenged pages as zero results and the chain advances. See
|
|
181
|
+
[docs/operations.md](docs/operations.md) for runtime expectations.
|
|
182
|
+
|
|
183
|
+
## Compatibility
|
|
184
|
+
|
|
185
|
+
| Component | Supported contract |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| Node.js | 22.19 or newer |
|
|
188
|
+
| DeepSeek Harness | `0.1.0-rc.6` (`@deepseek-ai/dsh-web`, `@deepseek-ai/cordis` peer range) |
|
|
189
|
+
| Seam | `ctx.web` `WebSearchProvider` (no key, no fetch provider) |
|
|
190
|
+
|
|
191
|
+
DeepSeek Harness is a developer preview. Upstream seam-contract changes require a new
|
|
192
|
+
package release and contract review.
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm ci
|
|
198
|
+
npm run check
|
|
199
|
+
npm test
|
|
200
|
+
npm run build
|
|
201
|
+
npm pack --dry-run
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The default suite runs parser and provider contract tests against fixture HTML (no network).
|
|
205
|
+
For live-network validation of the scrapers against current engine markup, run the opt-in
|
|
206
|
+
real test — it exercises Startpage, DuckDuckGo, and Mojeek for organic results and verifies
|
|
207
|
+
bot-challenged engines (Ecosia/Google) fail forward instead of crashing:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
DSH_WEB_SEARCH_REAL_E2E=1 npm run test:real
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the release checklist and design constraints, and
|
|
214
|
+
[docs/operations.md](docs/operations.md) for runtime behavior and troubleshooting.
|
|
215
|
+
|
|
216
|
+
## License and attribution
|
|
217
|
+
|
|
218
|
+
This package is licensed MIT — the same license as its upstream
|
|
219
|
+
[oh-my-pi](https://github.com/can1357/oh-my-pi). The credential-free engine chain and its
|
|
220
|
+
per-engine scrapers are ported from oh-my-pi (MIT License, © Mario Zechner 2025, © Can
|
|
221
|
+
Bölük 2025-2026); the upstream copyright holders are recorded in LICENSE next to this
|
|
222
|
+
package’s own notice, and the upstream notice text is reproduced in full in
|
|
223
|
+
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
|
|
224
|
+
|
|
225
|
+
This plugin is a separate installable package; the harness remains the property of its own
|
|
226
|
+
project.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please use GitHub’s private security-advisory flow for this repository. Do not open a public
|
|
6
|
+
issue containing credentials, unpublished exploit details, or customer data.
|
|
7
|
+
|
|
8
|
+
Include the affected version, a minimal reproduction, impact, and any suggested mitigation.
|
|
9
|
+
Rotate any credential that may have been exposed during testing.
|
|
10
|
+
|
|
11
|
+
## Deployment assumptions
|
|
12
|
+
|
|
13
|
+
- The plugin never reads, stores, or transmits credentials: searches are anonymous requests
|
|
14
|
+
to public search engines. There is nothing to leak.
|
|
15
|
+
- Queries are sent verbatim to the configured public engines, so treat a query as public
|
|
16
|
+
text. Do not search for secrets, tokens, or PII.
|
|
17
|
+
- No fetch provider is mounted (`tool-web` `fetch: false`), so the model cannot be pointed at
|
|
18
|
+
arbitrary URLs; SSRF surfaces stay closed.
|
|
19
|
+
- All engine fetches use `redirect: "error"`, so a hostile response cannot redirect the
|
|
20
|
+
provider onto an external location.
|
|
21
|
+
- Parsed snippets and titles are rendered to the model as citations; they are public
|
|
22
|
+
third-party text and are neither trusted nor executed.
|
|
23
|
+
|
|
24
|
+
See [docs/operations.md](docs/operations.md) for runtime expectations and troubleshooting.
|
|
25
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
This package includes code and design derived from
|
|
4
|
+
[oh-my-pi](https://github.com/can1357/oh-my-pi), specifically the credential-free engine
|
|
5
|
+
chain and per-engine scrapers originally at:
|
|
6
|
+
|
|
7
|
+
- `packages/coding-agent/src/web/search/providers/public.ts`
|
|
8
|
+
- `packages/coding-agent/src/web/search/providers/startpage.ts`
|
|
9
|
+
- `packages/coding-agent/src/web/search/providers/duckduckgo.ts`
|
|
10
|
+
- `packages/coding-agent/src/web/search/providers/ecosia.ts`
|
|
11
|
+
- `packages/coding-agent/src/web/search/providers/google.ts`
|
|
12
|
+
- `packages/coding-agent/src/web/search/providers/mojeek.ts`
|
|
13
|
+
|
|
14
|
+
The ported portions are adapted to the DeepSeek Harness `ctx.web` seam with sequential
|
|
15
|
+
fail-forward semantics (Startpage first, then DuckDuckGo → Ecosia → Google → Mojeek).
|
|
16
|
+
|
|
17
|
+
oh-my-pi is distributed under the following MIT License:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
MIT License
|
|
21
|
+
|
|
22
|
+
Copyright (c) 2025 Mario Zechner
|
|
23
|
+
Copyright (c) 2025-2026 Can Bölük
|
|
24
|
+
|
|
25
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
26
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
27
|
+
in the Software without restriction, including without limitation the rights
|
|
28
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
29
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
30
|
+
furnished to do so, subject to the following conditions:
|
|
31
|
+
|
|
32
|
+
The above copyright notice and this permission notice shall be included in all
|
|
33
|
+
copies or substantial portions of the Software.
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
36
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
37
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
38
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
39
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
40
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
41
|
+
SOFTWARE.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
All other code in this package is MIT (see LICENSE).
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Make the credential-free public search chain the default web-search provider:
|
|
2
|
+
# Startpage first, then DuckDuckGo -> Ecosia -> Google -> Mojeek as sequential
|
|
3
|
+
# fallbacks. The chain needs no API key and no fetch provider. The keyed DeepSeek
|
|
4
|
+
# route (web-search-deepseek) is left untouched, so deployments that explicitly
|
|
5
|
+
# choose their own provider via `web.searchProvider` keep that choice.
|
|
6
|
+
- id: web
|
|
7
|
+
config:
|
|
8
|
+
searchProvider: public
|
|
9
|
+
|
|
10
|
+
- insert:
|
|
11
|
+
- id: web-search-public
|
|
12
|
+
name: '@hy-sde-org/dsh-web-search-public'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DuckDuckGo engine: static HTML endpoint, no credentials, parseable without JS.
|
|
3
|
+
* @module @hy-sde-org/dsh-web-search-public/engines/duckduckgo
|
|
4
|
+
*/
|
|
5
|
+
import type { WebSearchRequest, WebSearchSource } from '@deepseek-ai/dsh-web';
|
|
6
|
+
import type { PublicEngine, PublicEngineId } from '../types.ts';
|
|
7
|
+
/** A `WebSearchSource` may use `title`/optional fields; built via guarded assignment. */
|
|
8
|
+
export declare class DuckDuckGoEngine implements PublicEngine {
|
|
9
|
+
private readonly userAgent;
|
|
10
|
+
readonly id: PublicEngineId;
|
|
11
|
+
constructor(userAgent: string);
|
|
12
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchSource[]>;
|
|
13
|
+
}
|
|
14
|
+
/** Unwrap DuckDuckGo's `uddg` redirect parameter; other http(s) hrefs pass through. */
|
|
15
|
+
export declare function unwrapDdgHref(href: string): string | undefined;
|
|
16
|
+
/** Parse DuckDuckGo's static HTML results page into sources. Exported for contract tests. */
|
|
17
|
+
export declare function parseDuckDuckGo(html: string, limit: number): WebSearchSource[];
|
|
18
|
+
//# sourceMappingURL=duckduckgo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duckduckgo.d.ts","sourceRoot":"","sources":["../../src/engines/duckduckgo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAM/D,yFAAyF;AACzF,qBAAa,gBAAiB,YAAW,YAAY;IAGvC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAFtC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAe;gBAEb,SAAS,EAAE,MAAM;IAExC,MAAM,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;CAU1F;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO9D;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,EAAE,CAY9E"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DuckDuckGo engine: static HTML endpoint, no credentials, parseable without JS.
|
|
3
|
+
* @module @hy-sde-org/dsh-web-search-public/engines/duckduckgo
|
|
4
|
+
*/
|
|
5
|
+
import { anchors, cleanText, elementText, elementsByClass, isUsableUrl, stripNoise } from "./html.js";
|
|
6
|
+
import { fetchHtml } from "./http.js";
|
|
7
|
+
const HTML_ENDPOINT = 'https://html.duckduckgo.com/html/';
|
|
8
|
+
/** A `WebSearchSource` may use `title`/optional fields; built via guarded assignment. */
|
|
9
|
+
export class DuckDuckGoEngine {
|
|
10
|
+
userAgent;
|
|
11
|
+
id = 'duckduckgo';
|
|
12
|
+
constructor(userAgent) {
|
|
13
|
+
this.userAgent = userAgent;
|
|
14
|
+
}
|
|
15
|
+
async search(request, signal) {
|
|
16
|
+
const html = await fetchHtml({
|
|
17
|
+
url: HTML_ENDPOINT,
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: new URLSearchParams({ q: request.query, kl: 'us-en' }).toString(),
|
|
20
|
+
userAgent: this.userAgent,
|
|
21
|
+
signal,
|
|
22
|
+
});
|
|
23
|
+
return parseDuckDuckGo(html, request.maxResults ?? 10);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Unwrap DuckDuckGo's `uddg` redirect parameter; other http(s) hrefs pass through. */
|
|
27
|
+
export function unwrapDdgHref(href) {
|
|
28
|
+
if (href.startsWith('//duckduckgo.com/l/?uddg=')) {
|
|
29
|
+
const target = new URL(href, 'https://duckduckgo.com').searchParams.get('uddg');
|
|
30
|
+
if (target === null)
|
|
31
|
+
return undefined;
|
|
32
|
+
return isUsableUrl(target) ? target : undefined;
|
|
33
|
+
}
|
|
34
|
+
return isUsableUrl(href) ? href : undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Parse DuckDuckGo's static HTML results page into sources. Exported for contract tests. */
|
|
37
|
+
export function parseDuckDuckGo(html, limit) {
|
|
38
|
+
const doc = stripNoise(html);
|
|
39
|
+
const rows = elementsByClass(doc, 'result', 'div');
|
|
40
|
+
const sources = [];
|
|
41
|
+
for (let i = 0; i < rows.length && sources.length < limit; i += 1) {
|
|
42
|
+
const row = rows[i];
|
|
43
|
+
if (row === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
const end = rows[i + 1]?.start ?? doc.length;
|
|
46
|
+
const source = parseDuckDuckGoRow(doc.slice(row.end, end));
|
|
47
|
+
if (source !== undefined)
|
|
48
|
+
sources.push(source);
|
|
49
|
+
}
|
|
50
|
+
return sources;
|
|
51
|
+
}
|
|
52
|
+
function parseDuckDuckGoRow(slice) {
|
|
53
|
+
const titleAnchor = anchors(slice).find(anchor => (anchor.attrs.class ?? '').split(/\s+/).includes('result__a'));
|
|
54
|
+
if (titleAnchor === undefined)
|
|
55
|
+
return undefined;
|
|
56
|
+
// Paid results arrive as `//duckduckgo.com/y.js?...` ad redirects carrying
|
|
57
|
+
// `ad_provider`/`ad_domain` query params; skip them, keeping only organic rows.
|
|
58
|
+
if (/^\/\/duckduckgo\.com\/y\.js\?/.test(titleAnchor.href)
|
|
59
|
+
|| /[?&](ad_provider|ad_domain)=/.test(titleAnchor.href))
|
|
60
|
+
return undefined;
|
|
61
|
+
const url = unwrapDdgHref(titleAnchor.href);
|
|
62
|
+
if (url === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const title = cleanText(titleAnchor.text);
|
|
65
|
+
const snippetTag = elementsByClass(slice, 'result__snippet').find(tag => tag.tag === 'a' || tag.tag === 'div');
|
|
66
|
+
const snippet = snippetTag === undefined ? undefined : elementText(slice, snippetTag);
|
|
67
|
+
const dateTag = elementsByClass(slice, 'result__timestamp').at(0);
|
|
68
|
+
const date = dateTag === undefined ? undefined : elementText(slice, dateTag);
|
|
69
|
+
const iso = date === undefined ? undefined : /^\d{4}-\d{2}-\d{2}/.exec(date.trim())?.[0];
|
|
70
|
+
return {
|
|
71
|
+
url,
|
|
72
|
+
...(title.length > 0 ? { title } : {}),
|
|
73
|
+
...(snippet !== undefined && snippet.length > 0 ? { snippet } : {}),
|
|
74
|
+
...(iso !== undefined ? { publishedAt: iso } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=duckduckgo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duckduckgo.js","sourceRoot":"","sources":["../../src/engines/duckduckgo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ecosia engine: credential-free Google-indexed results served as static HTML
|
|
3
|
+
* (browser-backed in friction: may be slower or bot-challenged).
|
|
4
|
+
* @module @hy-sde-org/dsh-web-search-public/engines/ecosia
|
|
5
|
+
*/
|
|
6
|
+
import type { WebSearchRequest, WebSearchSource } from '@deepseek-ai/dsh-web';
|
|
7
|
+
import type { PublicEngine, PublicEngineId } from '../types.ts';
|
|
8
|
+
export declare class EcosiaEngine implements PublicEngine {
|
|
9
|
+
private readonly userAgent;
|
|
10
|
+
readonly id: PublicEngineId;
|
|
11
|
+
constructor(userAgent: string);
|
|
12
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchSource[]>;
|
|
13
|
+
}
|
|
14
|
+
/** Parse Ecosia's server-rendered result page. Exported for contract tests. */
|
|
15
|
+
export declare function parseEcosia(html: string, limit: number): WebSearchSource[];
|
|
16
|
+
//# sourceMappingURL=ecosia.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ecosia.d.ts","sourceRoot":"","sources":["../../src/engines/ecosia.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAM/D,qBAAa,YAAa,YAAW,YAAY;IAGnC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAFtC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAW;gBAET,SAAS,EAAE,MAAM;IAExC,MAAM,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;CAK1F;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,EAAE,CAkB1E"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ecosia engine: credential-free Google-indexed results served as static HTML
|
|
3
|
+
* (browser-backed in friction: may be slower or bot-challenged).
|
|
4
|
+
* @module @hy-sde-org/dsh-web-search-public/engines/ecosia
|
|
5
|
+
*/
|
|
6
|
+
import { anchors, dedupeSources, elementText, elementsByClass, isUsableUrl, stripNoise } from "./html.js";
|
|
7
|
+
import { fetchHtml } from "./http.js";
|
|
8
|
+
const SEARCH_URL = 'https://www.ecosia.org/search';
|
|
9
|
+
export class EcosiaEngine {
|
|
10
|
+
userAgent;
|
|
11
|
+
id = 'ecosia';
|
|
12
|
+
constructor(userAgent) {
|
|
13
|
+
this.userAgent = userAgent;
|
|
14
|
+
}
|
|
15
|
+
async search(request, signal) {
|
|
16
|
+
const url = `${SEARCH_URL}?${new URLSearchParams({ q: request.query, method: 'index' })}`;
|
|
17
|
+
const html = await fetchHtml({ url, userAgent: this.userAgent, signal });
|
|
18
|
+
return parseEcosia(html, request.maxResults ?? 10);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Parse Ecosia's server-rendered result page. Exported for contract tests. */
|
|
22
|
+
export function parseEcosia(html, limit) {
|
|
23
|
+
const doc = stripNoise(html);
|
|
24
|
+
const links = anchors(doc)
|
|
25
|
+
.filter(anchor => isUsableUrl(anchor.href) && /(?:^|\s)result__a(?:$|\s)/.test(anchor.attrs.class ?? ''));
|
|
26
|
+
const snippetTags = elementsByClass(doc, 'result__quote');
|
|
27
|
+
const sources = [];
|
|
28
|
+
for (let i = 0; i < links.length && sources.length < limit; i += 1) {
|
|
29
|
+
const link = links[i];
|
|
30
|
+
if (link === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
const snippetTag = snippetTags[i];
|
|
33
|
+
const snippet = snippetTag === undefined ? undefined : elementText(doc, snippetTag);
|
|
34
|
+
sources.push({
|
|
35
|
+
url: link.href,
|
|
36
|
+
...(link.text.length > 0 ? { title: link.text } : {}),
|
|
37
|
+
...(snippet !== undefined && snippet.length > 0 ? { snippet } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return dedupeSources(sources);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=ecosia.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ecosia.js","sourceRoot":"","sources":["../../src/engines/ecosia.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACzG,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,MAAM,UAAU,GAAG,+BAA+B,CAAA;AAElD,MAAM,OAAO,YAAY;IAGM;IAFpB,EAAE,GAAmB,QAAQ,CAAA;IAEtC,YAA6B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;IAElD,KAAK,CAAC,MAAM,CAAC,OAAyB,EAAE,MAAoB;QAC1D,MAAM,GAAG,GAAG,GAAG,UAAU,IAAI,IAAI,eAAe,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAA;QACzF,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;QACxE,OAAO,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC,CAAA;IACpD,CAAC;CACF;AAED,+EAA+E;AAC/E,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,KAAa;IACrD,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC;SACvB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAA;IAC3G,MAAM,WAAW,GAAG,eAAe,CAAC,GAAG,EAAE,eAAe,CAAC,CAAA;IACzD,MAAM,OAAO,GAAsB,EAAE,CAAA;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QACrB,IAAI,IAAI,KAAK,SAAS;YAAE,SAAQ;QAChC,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;QACjC,MAAM,OAAO,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,UAAU,CAAC,CAAA;QACnF,OAAO,CAAC,IAAI,CAAC;YACX,GAAG,EAAE,IAAI,CAAC,IAAI;YACd,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpE,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,aAAa,CAAC,OAAO,CAAC,CAAA;AAC/B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google engine: credential-free scrape of the classic results page. Google is
|
|
3
|
+
* the most bot-challenged engine in the chain, so this engine is expected to
|
|
4
|
+
* frequently advance the chain rather than succeed.
|
|
5
|
+
* @module @hy-sde-org/dsh-web-search-public/engines/google
|
|
6
|
+
*/
|
|
7
|
+
import type { WebSearchRequest, WebSearchSource } from '@deepseek-ai/dsh-web';
|
|
8
|
+
import type { PublicEngine, PublicEngineId } from '../types.ts';
|
|
9
|
+
export declare class GoogleEngine implements PublicEngine {
|
|
10
|
+
private readonly userAgent;
|
|
11
|
+
readonly id: PublicEngineId;
|
|
12
|
+
constructor(userAgent: string);
|
|
13
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchSource[]>;
|
|
14
|
+
}
|
|
15
|
+
/** Resolve Google's `/url?q=` redirect wrapper; other usable URLs pass through. Exported for tests. */
|
|
16
|
+
export declare function unwrapGoogleHref(href: string): string | undefined;
|
|
17
|
+
/** Parse Google's classic results page. Exported for contract tests. */
|
|
18
|
+
export declare function parseGoogle(html: string, limit: number): WebSearchSource[];
|
|
19
|
+
//# sourceMappingURL=google.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../src/engines/google.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAS/D,qBAAa,YAAa,YAAW,YAAY;IAGnC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAFtC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAW;gBAET,SAAS,EAAE,MAAM;IAExC,MAAM,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;CAW1F;AAED,uGAAuG;AACvG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAWjE;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,EAAE,CAiC1E"}
|