@auggieteo/dsh-synthetic-web-search 0.2.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/LICENSE +21 -0
- package/README.md +143 -0
- package/examples/synthetic.cordis.yml +8 -0
- package/lib/client.d.ts +7 -0
- package/lib/client.js +159 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.js +54 -0
- package/lib/invariant.d.ts +6 -0
- package/lib/invariant.js +8 -0
- package/lib/provider.d.ts +38 -0
- package/lib/provider.js +162 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @auggieteo/dsh-synthetic-web-search
|
|
2
|
+
|
|
3
|
+
A [Synthetic Search](https://dev.synthetic.new/docs/synthetic/search)-backed `WebSearchProvider` and Settings card for the DeepSeek Harness `ctx.web` capability seam.
|
|
4
|
+
|
|
5
|
+
This is a **host-plane** plugin: it registers the `synthetic` provider into the Harness-owned `web` service. It does not provide `ctx.web` or a model-facing tool. Use it with the existing `@deepseek-ai/dsh-tool-web` row.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- DeepSeek Harness with the `web` profile.
|
|
10
|
+
- Node.js 22.12 or newer.
|
|
11
|
+
- A [Synthetic API key](https://dev.synthetic.new/docs/synthetic/search).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Install the package as a dependency of the profile that hosts `web` (normally `web`). Choose one route.
|
|
16
|
+
|
|
17
|
+
### npm registry
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
dsh plugin --profile web add @auggieteo/dsh-synthetic-web-search
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Public Git URL
|
|
24
|
+
|
|
25
|
+
After the public repository exists, install directly from Git:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
dsh plugin --profile web add git+https://github.com/auggie246/dsh-synthetic-web-search.git
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
#### pnpm `prepare` allowlist for Git installs
|
|
32
|
+
|
|
33
|
+
A Git install builds this package from source with its `prepare` script. DSH forwards the install to pnpm, and pnpm may block that script. If it does, the command prints the package/build key that pnpm requires. Copy that **exact printed key** into `$DSH_HOME/profiles/web/pnpm-workspace.yaml`, for example:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
allowBuilds:
|
|
37
|
+
# Replace this with the exact key pnpm printed for this install.
|
|
38
|
+
<exact-key-printed-by-pnpm>: true
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then rerun the same `dsh plugin --profile web add git+https://…` command. Do not guess or substitute a package name: pnpm requires the exact key it printed.
|
|
42
|
+
|
|
43
|
+
### Activate the provider
|
|
44
|
+
|
|
45
|
+
Add this top-level `insert` block to `$DSH_HOME/profiles/web/cordis.patch.yml` (create the file with this YAML array if it does not exist):
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
- insert:
|
|
49
|
+
- id: web-search-synthetic
|
|
50
|
+
name: '@auggieteo/dsh-synthetic-web-search'
|
|
51
|
+
config:
|
|
52
|
+
# Settings stores the key using this DSH credential reference.
|
|
53
|
+
apiKeyEnv: SYNTHETIC_API_KEY
|
|
54
|
+
# Optional; defaults to https://api.synthetic.new.
|
|
55
|
+
# baseURL: https://api.synthetic.new
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The row belongs in the **web host profile**, not an agent preset: `ctx.web` is process-wide and each provider must register once. The same example is in [`examples/synthetic.cordis.yml`](examples/synthetic.cordis.yml).
|
|
59
|
+
|
|
60
|
+
Restart the DSH web profile after changing its composition. Do not start a separate Vite server; it does not update an existing DSH GUI.
|
|
61
|
+
|
|
62
|
+
## Configure credentials and provider selection
|
|
63
|
+
|
|
64
|
+
Open **Settings → Plugins → Plugin configuration → Synthetic web search**, enter the API key, and select **Save**. DSH stores it in its credentials domain and does not return it to the browser after saving.
|
|
65
|
+
|
|
66
|
+
For headless use, the plugin also reads the launch environment reference (by default `SYNTHETIC_API_KEY`):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
export SYNTHETIC_API_KEY='…'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
When another usable search provider is installed, explicitly select Synthetic before launching DSH:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export DSH_WEB_SEARCH_PROVIDER=synthetic
|
|
76
|
+
dsh web
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Provider selection is intentionally unambiguous. The existing `@deepseek-ai/dsh-tool-web` agent-preset row exposes `web_search` to the model.
|
|
80
|
+
|
|
81
|
+
### Configuration
|
|
82
|
+
|
|
83
|
+
| Key | Default | Meaning |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `apiKey` | (unset) | Literal API key for non-interactive composition. Prefer the Settings card or environment reference; this secret is redacted from Settings responses. |
|
|
86
|
+
| `apiKeyEnv` | `SYNTHETIC_API_KEY` | Credential reference used by the Settings card and launch environment. |
|
|
87
|
+
| `baseURL` | `https://api.synthetic.new` | Synthetic API origin; the provider appends `/v2/search`. |
|
|
88
|
+
|
|
89
|
+
## Uninstall
|
|
90
|
+
|
|
91
|
+
1. Remove the `web-search-synthetic` row from `$DSH_HOME/profiles/web/cordis.patch.yml`.
|
|
92
|
+
2. Remove the dependency:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
dsh plugin --profile web remove @auggieteo/dsh-synthetic-web-search
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
3. Restart the DSH web profile.
|
|
99
|
+
4. If no other configuration uses it, remove `SYNTHETIC_API_KEY` from the process environment and remove the stored credential through your normal DSH credentials management.
|
|
100
|
+
|
|
101
|
+
## Compatibility
|
|
102
|
+
|
|
103
|
+
The public package name is `@auggieteo/dsh-synthetic-web-search`, replacing the former local `@deepseek-ai/dsh-web-search-synthetic` reference. Only the package reference changes. The Cordis row id (`web-search-synthetic`), Settings namespace (`web-search-synthetic`), and default credential reference (`SYNTHETIC_API_KEY`) remain unchanged, so existing persisted plugin settings and credentials continue to apply after the row is updated.
|
|
104
|
+
|
|
105
|
+
## Development and verification
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm ci
|
|
109
|
+
npm run verify
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`npm run verify` cleans generated output, type-checks, runs mocked provider tests, builds `lib/` from source (including the browser client bundle), and checks the npm package contents with `npm pack --dry-run`.
|
|
113
|
+
|
|
114
|
+
To test a local checkout without touching another profile, point a throwaway profile at its absolute path:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
dsh plugin --profile synthetic-smoke add /absolute/path/to/dsh-synthetic-web-search
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Behavior
|
|
121
|
+
|
|
122
|
+
The provider sends the documented request:
|
|
123
|
+
|
|
124
|
+
```http
|
|
125
|
+
POST https://api.synthetic.new/v2/search
|
|
126
|
+
Authorization: Bearer $SYNTHETIC_API_KEY
|
|
127
|
+
Content-Type: application/json
|
|
128
|
+
|
|
129
|
+
{ "query": "…" }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
It maps valid results into the Harness source vocabulary:
|
|
133
|
+
|
|
134
|
+
| Synthetic field | Harness field |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `url` | `url` |
|
|
137
|
+
| `title` | `title` |
|
|
138
|
+
| `text` | `snippet` |
|
|
139
|
+
| `published` | `publishedAt` |
|
|
140
|
+
|
|
141
|
+
Malformed or non-URL entries are ignored. The provider does not create a generated answer (`content`) and returns `truncated: false`; the `ctx.web` seam applies the caller's `maxResults` cap. Network, redirect, HTTP, and response-shape failures surface as `WEB_PROVIDER_ERROR`; aborted requests surface as `WEB_ABORTED`.
|
|
142
|
+
|
|
143
|
+
Synthetic's documented API currently exposes only `query`, so `maxResults` is intentionally not sent upstream.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Host-plane row: it registers a provider into the process-wide `web` service.
|
|
2
|
+
# Install the package into the target profile before adding this row.
|
|
3
|
+
- id: web-search-synthetic
|
|
4
|
+
name: '@auggieteo/dsh-synthetic-web-search'
|
|
5
|
+
config:
|
|
6
|
+
# The Settings card stores its key under this DSH credential reference.
|
|
7
|
+
# apiKeyEnv: SYNTHETIC_API_KEY
|
|
8
|
+
# baseURL: https://api.synthetic.new
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
2
|
+
|
|
3
|
+
/** Required client services for the Synthetic Settings card. */
|
|
4
|
+
export declare const inject: string[]
|
|
5
|
+
|
|
6
|
+
/** Register the Synthetic card in Settings > Plugins > Plugin configuration. */
|
|
7
|
+
export declare function apply(ctx: ClientContext): void
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@auggieteo/dsh-synthetic-web-search',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
const module = { exports: {} }
|
|
5
|
+
const exports = module.exports
|
|
6
|
+
const React = require('react')
|
|
7
|
+
|
|
8
|
+
const SETTINGS_NAMESPACE = 'web-search-synthetic'
|
|
9
|
+
const DEFAULT_API_KEY_ENV = 'SYNTHETIC_API_KEY'
|
|
10
|
+
const DEFAULT_BASE_URL = 'https://api.synthetic.new'
|
|
11
|
+
const inject = ['slots', 'settingsScope', 'connection']
|
|
12
|
+
|
|
13
|
+
function useScope(scope) {
|
|
14
|
+
const [snapshot, setSnapshot] = React.useState(() => scope.getSnapshot())
|
|
15
|
+
React.useEffect(() => scope.subscribe(() => setSnapshot(scope.getSnapshot())), [scope])
|
|
16
|
+
return snapshot
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createCard(scope, api) {
|
|
20
|
+
return function SyntheticSettingsCard() {
|
|
21
|
+
const snapshot = useScope(scope)
|
|
22
|
+
const configured = snapshot.value || {}
|
|
23
|
+
const reference = configured.apiKeyEnv || DEFAULT_API_KEY_ENV
|
|
24
|
+
const [apiKey, setApiKey] = React.useState('')
|
|
25
|
+
const [baseURL, setBaseURL] = React.useState('')
|
|
26
|
+
const [credentialState, setCredentialState] = React.useState({ configured: false, writable: true })
|
|
27
|
+
const [status, setStatus] = React.useState('')
|
|
28
|
+
const [saving, setSaving] = React.useState(false)
|
|
29
|
+
const [open, setOpen] = React.useState(false)
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
setBaseURL(configured.baseURL || '')
|
|
33
|
+
}, [configured.baseURL])
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
let active = true
|
|
37
|
+
api.credentials.describe({ refs: [reference] }).then((response) => {
|
|
38
|
+
if (!active || !response.result.ok) return
|
|
39
|
+
const credential = response.result.value.credentials[reference]
|
|
40
|
+
setCredentialState({
|
|
41
|
+
configured: credential?.configured === true,
|
|
42
|
+
writable: credential?.writable !== false,
|
|
43
|
+
})
|
|
44
|
+
}).catch(() => {})
|
|
45
|
+
return () => { active = false }
|
|
46
|
+
}, [api, reference])
|
|
47
|
+
|
|
48
|
+
if (snapshot.status === 'unavailable') return null
|
|
49
|
+
if (snapshot.status === 'loading') return React.createElement('p', null, 'Loading Synthetic search settings…')
|
|
50
|
+
|
|
51
|
+
const save = async () => {
|
|
52
|
+
setSaving(true)
|
|
53
|
+
setStatus('')
|
|
54
|
+
try {
|
|
55
|
+
const nextBaseURL = baseURL.trim()
|
|
56
|
+
if (nextBaseURL.length === 0) await scope.unset('baseURL')
|
|
57
|
+
else await scope.set('baseURL', nextBaseURL)
|
|
58
|
+
|
|
59
|
+
if (apiKey.trim().length > 0) {
|
|
60
|
+
await api.credentials.set({ ref: reference, value: apiKey.trim() })
|
|
61
|
+
setApiKey('')
|
|
62
|
+
setCredentialState((current) => ({ ...current, configured: true }))
|
|
63
|
+
}
|
|
64
|
+
setStatus('Saved.')
|
|
65
|
+
} catch {
|
|
66
|
+
setStatus('Could not save the Synthetic configuration. Check the URL and credential permissions.')
|
|
67
|
+
} finally {
|
|
68
|
+
setSaving(false)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const baseValue = configured.baseURL || ''
|
|
73
|
+
const dirty = apiKey.trim().length > 0 || baseURL !== baseValue
|
|
74
|
+
const discard = () => {
|
|
75
|
+
setApiKey('')
|
|
76
|
+
setBaseURL(baseValue)
|
|
77
|
+
setStatus('')
|
|
78
|
+
}
|
|
79
|
+
const body = open ? React.createElement('div', { className: 'YyYd_a_body' },
|
|
80
|
+
React.createElement('label', { className: 'At1oFq_field' },
|
|
81
|
+
React.createElement('span', { className: 'At1oFq_head' },
|
|
82
|
+
React.createElement('span', { className: 'At1oFq_label' }, 'API key'),
|
|
83
|
+
React.createElement('span', { className: credentialState.configured ? 'At1oFq_badge' : 'At1oFq_badgeMuted' }, credentialState.configured ? 'Configured' : 'Not configured'),
|
|
84
|
+
),
|
|
85
|
+
React.createElement('input', {
|
|
86
|
+
className: 'At1oFq_input',
|
|
87
|
+
type: 'password',
|
|
88
|
+
value: apiKey,
|
|
89
|
+
disabled: saving || !credentialState.writable,
|
|
90
|
+
placeholder: credentialState.configured ? 'Enter a replacement key' : 'Enter a Synthetic API key',
|
|
91
|
+
onChange: (event) => setApiKey(event.target.value),
|
|
92
|
+
}),
|
|
93
|
+
React.createElement('p', { className: 'At1oFq_hint' }, credentialState.configured ? 'A key is stored in DSH credentials.' : `No key is configured for ${reference}.`),
|
|
94
|
+
),
|
|
95
|
+
React.createElement('label', { className: 'At1oFq_field' },
|
|
96
|
+
React.createElement('span', { className: 'At1oFq_head' },
|
|
97
|
+
React.createElement('span', { className: 'At1oFq_label' }, 'API base URL'),
|
|
98
|
+
),
|
|
99
|
+
React.createElement('input', {
|
|
100
|
+
className: 'At1oFq_input',
|
|
101
|
+
type: 'url',
|
|
102
|
+
value: baseURL,
|
|
103
|
+
disabled: saving || !snapshot.writable,
|
|
104
|
+
placeholder: DEFAULT_BASE_URL,
|
|
105
|
+
onChange: (event) => setBaseURL(event.target.value),
|
|
106
|
+
}),
|
|
107
|
+
React.createElement('p', { className: 'At1oFq_hint' }, 'Leave blank to use https://api.synthetic.new.'),
|
|
108
|
+
),
|
|
109
|
+
React.createElement('div', { className: 'YyYd_a_footer' },
|
|
110
|
+
status.length > 0 ? React.createElement('p', { className: 'YyYd_a_failed', role: 'status' }, status) : null,
|
|
111
|
+
React.createElement('button', {
|
|
112
|
+
type: 'button',
|
|
113
|
+
className: 'YyYd_a_discard',
|
|
114
|
+
onClick: discard,
|
|
115
|
+
disabled: !dirty || saving,
|
|
116
|
+
}, 'Discard'),
|
|
117
|
+
React.createElement('button', {
|
|
118
|
+
type: 'button',
|
|
119
|
+
className: 'YyYd_a_save',
|
|
120
|
+
onClick: save,
|
|
121
|
+
disabled: !dirty || saving || !snapshot.writable,
|
|
122
|
+
}, saving ? 'Saving…' : 'Save'),
|
|
123
|
+
),
|
|
124
|
+
) : null
|
|
125
|
+
return React.createElement('li', {
|
|
126
|
+
className: open ? 'YyYd_a_card YyYd_a_cardOpen' : 'YyYd_a_card',
|
|
127
|
+
},
|
|
128
|
+
React.createElement('button', {
|
|
129
|
+
type: 'button',
|
|
130
|
+
className: 'YyYd_a_header',
|
|
131
|
+
'aria-expanded': open,
|
|
132
|
+
'aria-label': `${open ? 'Collapse' : 'Expand'}: Synthetic web search`,
|
|
133
|
+
onClick: () => setOpen(!open),
|
|
134
|
+
},
|
|
135
|
+
React.createElement('span', { className: 'YyYd_a_headText' },
|
|
136
|
+
React.createElement('span', { className: 'YyYd_a_name' }, 'Synthetic web search'),
|
|
137
|
+
React.createElement('span', { className: 'YyYd_a_description' }, 'The Synthetic search provider.'),
|
|
138
|
+
),
|
|
139
|
+
React.createElement('span', { className: open ? 'YyYd_a_chevron YyYd_a_chevronOpen' : 'YyYd_a_chevron', 'aria-hidden': true }, '⌄'),
|
|
140
|
+
),
|
|
141
|
+
body)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function apply(ctx) {
|
|
146
|
+
const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE })
|
|
147
|
+
const api = ctx.connection.api
|
|
148
|
+
const Card = createCard(scope, api)
|
|
149
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
150
|
+
name: 'settings.plugin.item',
|
|
151
|
+
key: SETTINGS_NAMESPACE,
|
|
152
|
+
}, Card))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
exports.inject = inject
|
|
156
|
+
exports.apply = apply
|
|
157
|
+
return module.exports
|
|
158
|
+
},
|
|
159
|
+
})
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Default Synthetic API origin. */
|
|
4
|
+
export declare const SYNTHETIC_DEFAULT_BASE_URL = "https://api.synthetic.new";
|
|
5
|
+
/** Default credential reference, shared by the Settings card and launch environment. */
|
|
6
|
+
export declare const SYNTHETIC_DEFAULT_API_KEY_ENV = "SYNTHETIC_API_KEY";
|
|
7
|
+
/** Namespace exposed to Settings > Plugins > Plugin configuration. */
|
|
8
|
+
export declare const SYNTHETIC_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
9
|
+
export { SyntheticSearchProvider, SYNTHETIC_DEFAULT_ENDPOINT, SYNTHETIC_PROVIDER_ID, mapSyntheticResponse, mapSyntheticResult, } from './provider.js';
|
|
10
|
+
export type { SyntheticSearchProviderOptions } from './provider.js';
|
|
11
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
12
|
+
export declare const name = "web-search-synthetic";
|
|
13
|
+
/** The provider registers into the host-owned web capability seam. */
|
|
14
|
+
export declare const inject: string[];
|
|
15
|
+
export interface Config {
|
|
16
|
+
/** Optional literal API key. Settings stores keys in the credentials domain instead. */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
/** Credential reference used by Settings and the launch environment. */
|
|
19
|
+
apiKeyEnv?: string;
|
|
20
|
+
/** Synthetic API origin. The provider appends `/v2/search`. */
|
|
21
|
+
baseURL?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Loader and Settings schema. Secret values are redacted from all Settings wire responses. */
|
|
24
|
+
export declare const Config: z<Schemastery.ObjectS<{
|
|
25
|
+
apiKey: z<string, string>;
|
|
26
|
+
apiKeyEnv: z<string, string>;
|
|
27
|
+
baseURL: z<string, string>;
|
|
28
|
+
}>, Schemastery.ObjectT<{
|
|
29
|
+
apiKey: z<string, string>;
|
|
30
|
+
apiKeyEnv: z<string, string>;
|
|
31
|
+
baseURL: z<string, string>;
|
|
32
|
+
}>>;
|
|
33
|
+
/** Register Synthetic as a search provider and expose its persisted Settings section. */
|
|
34
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
2
|
+
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
3
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
4
|
+
import z from '@deepseek-ai/schemastery';
|
|
5
|
+
import { SyntheticSearchProvider } from './provider.js';
|
|
6
|
+
/** Default Synthetic API origin. */
|
|
7
|
+
export const SYNTHETIC_DEFAULT_BASE_URL = 'https://api.synthetic.new';
|
|
8
|
+
/** Default credential reference, shared by the Settings card and launch environment. */
|
|
9
|
+
export const SYNTHETIC_DEFAULT_API_KEY_ENV = 'SYNTHETIC_API_KEY';
|
|
10
|
+
/** Namespace exposed to Settings > Plugins > Plugin configuration. */
|
|
11
|
+
export const SYNTHETIC_SETTINGS_NAMESPACE = settingsNamespace('web-search-synthetic');
|
|
12
|
+
export { SyntheticSearchProvider, SYNTHETIC_DEFAULT_ENDPOINT, SYNTHETIC_PROVIDER_ID, mapSyntheticResponse, mapSyntheticResult, } from './provider.js';
|
|
13
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
14
|
+
export const name = 'web-search-synthetic';
|
|
15
|
+
/** The provider registers into the host-owned web capability seam. */
|
|
16
|
+
export const inject = ['web'];
|
|
17
|
+
/** Loader and Settings schema. Secret values are redacted from all Settings wire responses. */
|
|
18
|
+
export const Config = z.object({
|
|
19
|
+
apiKey: z.string().role('secret'),
|
|
20
|
+
apiKeyEnv: z.string().role('credential-ref').default(SYNTHETIC_DEFAULT_API_KEY_ENV),
|
|
21
|
+
baseURL: z.string(),
|
|
22
|
+
});
|
|
23
|
+
/** Register Synthetic as a search provider and expose its persisted Settings section. */
|
|
24
|
+
export function apply(ctx, config) {
|
|
25
|
+
let current = () => config;
|
|
26
|
+
installSettingsSection(ctx, SYNTHETIC_SETTINGS_NAMESPACE, Config, config, {
|
|
27
|
+
setSource: (source) => {
|
|
28
|
+
current = source;
|
|
29
|
+
},
|
|
30
|
+
onChange: () => { },
|
|
31
|
+
});
|
|
32
|
+
ctx.web.registerSearchProvider(new SyntheticSearchProvider(() => resolveOptions(ctx, current())));
|
|
33
|
+
}
|
|
34
|
+
function resolveOptions(ctx, config) {
|
|
35
|
+
const apiKeyEnv = config.apiKeyEnv ?? SYNTHETIC_DEFAULT_API_KEY_ENV;
|
|
36
|
+
const ref = credentialRef(apiKeyEnv);
|
|
37
|
+
return {
|
|
38
|
+
apiKey: config.apiKey,
|
|
39
|
+
apiKeyEnv,
|
|
40
|
+
endpoint: searchEndpoint(config.baseURL ?? SYNTHETIC_DEFAULT_BASE_URL),
|
|
41
|
+
resolveApiKey: async () => {
|
|
42
|
+
const credential = await ctx.get('credentials')?.resolve(ref);
|
|
43
|
+
return credential?.value ?? launchEnvironmentOf(ctx).get(apiKeyEnv)?.value;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function searchEndpoint(baseURL) {
|
|
48
|
+
try {
|
|
49
|
+
return new URL('/v2/search', baseURL).toString();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return baseURL;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
/** Cordis plugin name for the package's invariant companion. */
|
|
3
|
+
export declare const name = "web-search-synthetic-invariant";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
/** Register package ownership; the provider has no independent runtime invariant. */
|
|
6
|
+
export declare function apply(ctx: Context): Promise<() => void>;
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const PACKAGE_NAME = '@auggieteo/dsh-synthetic-web-search';
|
|
2
|
+
/** Cordis plugin name for the package's invariant companion. */
|
|
3
|
+
export const name = 'web-search-synthetic-invariant';
|
|
4
|
+
export const inject = ['invariants'];
|
|
5
|
+
/** Register package ownership; the provider has no independent runtime invariant. */
|
|
6
|
+
export function apply(ctx) {
|
|
7
|
+
return Promise.resolve(ctx.invariants.register(PACKAGE_NAME, () => { }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WebSearchProvider, WebSearchRequest, WebSearchResult, WebSearchSource } from '@deepseek-ai/dsh-web';
|
|
2
|
+
/** Stable id used by the Harness web-provider registry. */
|
|
3
|
+
export declare const SYNTHETIC_PROVIDER_ID = "synthetic";
|
|
4
|
+
/** Synthetic's documented web-search endpoint. */
|
|
5
|
+
export declare const SYNTHETIC_DEFAULT_ENDPOINT = "https://api.synthetic.new/v2/search";
|
|
6
|
+
export interface SyntheticSearchProviderOptions {
|
|
7
|
+
/** Optional literal API key from the composition layer. */
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
/** Resolves the API key held by DSH credentials or the launch environment. */
|
|
10
|
+
resolveApiKey?: () => Promise<string | undefined>;
|
|
11
|
+
/** The credential reference shown in missing-key errors. */
|
|
12
|
+
apiKeyEnv?: string;
|
|
13
|
+
/** Complete Synthetic search endpoint. */
|
|
14
|
+
endpoint: string;
|
|
15
|
+
}
|
|
16
|
+
interface SyntheticSearchResult {
|
|
17
|
+
url: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
text?: string;
|
|
20
|
+
published?: string;
|
|
21
|
+
}
|
|
22
|
+
interface SyntheticSearchResponse {
|
|
23
|
+
results: SyntheticSearchResult[];
|
|
24
|
+
}
|
|
25
|
+
/** Convert a Synthetic result into the web seam's portable source shape. */
|
|
26
|
+
export declare function mapSyntheticResult(result: SyntheticSearchResult): WebSearchSource | undefined;
|
|
27
|
+
/** Convert a validated Synthetic response into the Harness result shape. */
|
|
28
|
+
export declare function mapSyntheticResponse(response: SyntheticSearchResponse): WebSearchResult;
|
|
29
|
+
/** A Synthetic-backed implementation of the Harness WebSearchProvider contract. */
|
|
30
|
+
export declare class SyntheticSearchProvider implements WebSearchProvider {
|
|
31
|
+
readonly id = "synthetic";
|
|
32
|
+
private readonly resolveOptions;
|
|
33
|
+
constructor(options: SyntheticSearchProviderOptions | (() => SyntheticSearchProviderOptions));
|
|
34
|
+
available(): boolean;
|
|
35
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
36
|
+
private apiKey;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
package/lib/provider.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { WebError } from '@deepseek-ai/dsh-web';
|
|
2
|
+
/** Stable id used by the Harness web-provider registry. */
|
|
3
|
+
export const SYNTHETIC_PROVIDER_ID = 'synthetic';
|
|
4
|
+
/** Synthetic's documented web-search endpoint. */
|
|
5
|
+
export const SYNTHETIC_DEFAULT_ENDPOINT = 'https://api.synthetic.new/v2/search';
|
|
6
|
+
const USER_AGENT = 'deepseek-harness-synthetic/0.1.0';
|
|
7
|
+
/** Convert a Synthetic result into the web seam's portable source shape. */
|
|
8
|
+
export function mapSyntheticResult(result) {
|
|
9
|
+
if (result.url.trim().length === 0 || !URL.canParse(result.url))
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
url: result.url,
|
|
13
|
+
...(nonBlankString(result.title) ? { title: result.title } : {}),
|
|
14
|
+
...(nonBlankString(result.text) ? { snippet: result.text } : {}),
|
|
15
|
+
...(nonBlankString(result.published) ? { publishedAt: result.published } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Convert a validated Synthetic response into the Harness result shape. */
|
|
19
|
+
export function mapSyntheticResponse(response) {
|
|
20
|
+
return {
|
|
21
|
+
sources: response.results
|
|
22
|
+
.map(mapSyntheticResult)
|
|
23
|
+
.filter((source) => source !== undefined),
|
|
24
|
+
truncated: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A Synthetic-backed implementation of the Harness WebSearchProvider contract. */
|
|
28
|
+
export class SyntheticSearchProvider {
|
|
29
|
+
id = SYNTHETIC_PROVIDER_ID;
|
|
30
|
+
resolveOptions;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.resolveOptions = typeof options === 'function' ? options : () => options;
|
|
33
|
+
}
|
|
34
|
+
available() {
|
|
35
|
+
const options = this.resolveOptions();
|
|
36
|
+
return (((options.apiKey?.trim().length ?? 0) > 0 || options.resolveApiKey !== undefined) &&
|
|
37
|
+
URL.canParse(options.endpoint));
|
|
38
|
+
}
|
|
39
|
+
async search(request, signal) {
|
|
40
|
+
const options = this.resolveOptions();
|
|
41
|
+
const apiKey = await this.apiKey(options, signal);
|
|
42
|
+
throwIfAborted(signal);
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
response = await fetch(options.endpoint, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
redirect: 'error',
|
|
48
|
+
headers: {
|
|
49
|
+
authorization: `Bearer ${apiKey}`,
|
|
50
|
+
accept: 'application/json',
|
|
51
|
+
'content-type': 'application/json',
|
|
52
|
+
'user-agent': USER_AGENT,
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({ query: request.query }),
|
|
55
|
+
...(signal === undefined ? {} : { signal }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (signal?.aborted === true || isAbortError(error))
|
|
60
|
+
throw aborted(signal, error);
|
|
61
|
+
throw new WebError(`Synthetic search request failed: ${String(error)}`, 'WEB_PROVIDER_ERROR', { cause: error });
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
throw new WebError(await errorMessage(response, signal), 'WEB_PROVIDER_ERROR');
|
|
65
|
+
try {
|
|
66
|
+
return mapSyntheticResponse(parseResponse(await response.json()));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (signal?.aborted === true || isAbortError(error))
|
|
70
|
+
throw aborted(signal, error);
|
|
71
|
+
if (error instanceof WebError)
|
|
72
|
+
throw error;
|
|
73
|
+
throw new WebError(`Synthetic returned an unprocessable response body: ${String(error)}`, 'WEB_PROVIDER_ERROR', { cause: error });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async apiKey(options, signal) {
|
|
77
|
+
throwIfAborted(signal);
|
|
78
|
+
if (nonBlankString(options.apiKey))
|
|
79
|
+
return options.apiKey;
|
|
80
|
+
let resolved;
|
|
81
|
+
try {
|
|
82
|
+
resolved = await abortable(options.resolveApiKey?.() ?? Promise.resolve(undefined), signal);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (signal?.aborted === true || isAbortError(error))
|
|
86
|
+
throw aborted(signal, error);
|
|
87
|
+
throw new WebError(`Synthetic search credential resolution failed: ${String(error)}`, 'WEB_PROVIDER_ERROR', { cause: error });
|
|
88
|
+
}
|
|
89
|
+
if (nonBlankString(resolved))
|
|
90
|
+
return resolved;
|
|
91
|
+
throw new WebError(`Synthetic search has no API key for "${options.apiKeyEnv ?? 'SYNTHETIC_API_KEY'}"; configure it in Settings > Plugins > Plugin configuration, export it in the launching environment, or set a literal "apiKey" in the web-search-synthetic config`, 'WEB_PROVIDER_CREDENTIAL_MISSING');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function nonBlankString(value) {
|
|
95
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
96
|
+
}
|
|
97
|
+
function parseResponse(value) {
|
|
98
|
+
if (!isRecord(value) || !Array.isArray(value.results)) {
|
|
99
|
+
throw new Error('response must contain a results array');
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
results: value.results.flatMap((result) => {
|
|
103
|
+
if (!isRecord(result) || typeof result.url !== 'string')
|
|
104
|
+
return [];
|
|
105
|
+
return [{
|
|
106
|
+
url: result.url,
|
|
107
|
+
...(typeof result.title === 'string' ? { title: result.title } : {}),
|
|
108
|
+
...(typeof result.text === 'string' ? { text: result.text } : {}),
|
|
109
|
+
...(typeof result.published === 'string' ? { published: result.published } : {}),
|
|
110
|
+
}];
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function errorMessage(response, signal) {
|
|
115
|
+
const fallback = `Synthetic API error (HTTP ${response.status})`;
|
|
116
|
+
try {
|
|
117
|
+
const body = await response.json();
|
|
118
|
+
if (!isRecord(body))
|
|
119
|
+
return fallback;
|
|
120
|
+
for (const key of ['error', 'message', 'detail']) {
|
|
121
|
+
if (nonBlankString(body[key]))
|
|
122
|
+
return body[key];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (signal?.aborted === true || isAbortError(error))
|
|
127
|
+
throw aborted(signal, error);
|
|
128
|
+
}
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
function abortable(operation, signal) {
|
|
132
|
+
if (signal === undefined)
|
|
133
|
+
return operation;
|
|
134
|
+
if (signal.aborted)
|
|
135
|
+
return Promise.reject(aborted(signal));
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const onAbort = () => reject(aborted(signal));
|
|
138
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
139
|
+
operation.then((value) => {
|
|
140
|
+
signal.removeEventListener('abort', onAbort);
|
|
141
|
+
resolve(value);
|
|
142
|
+
}, (error) => {
|
|
143
|
+
signal.removeEventListener('abort', onAbort);
|
|
144
|
+
reject(error);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function throwIfAborted(signal) {
|
|
149
|
+
if (signal?.aborted === true)
|
|
150
|
+
throw aborted(signal);
|
|
151
|
+
}
|
|
152
|
+
function aborted(signal, fallback) {
|
|
153
|
+
return new WebError('Synthetic search aborted', 'WEB_ABORTED', {
|
|
154
|
+
cause: signal?.aborted === true ? signal.reason : fallback,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function isRecord(value) {
|
|
158
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
159
|
+
}
|
|
160
|
+
function isAbortError(error) {
|
|
161
|
+
return error instanceof DOMException && error.name === 'AbortError';
|
|
162
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@auggieteo/dsh-synthetic-web-search",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Synthetic-backed search provider and Settings card for the DeepSeek Harness web capability seam (ctx.web)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/auggie246/dsh-synthetic-web-search.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/auggie246/dsh-synthetic-web-search/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/auggie246/dsh-synthetic-web-search#readme",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.12.0"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "lib/index.js",
|
|
22
|
+
"types": "lib/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./lib/index.d.ts",
|
|
26
|
+
"default": "./lib/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./client": {
|
|
29
|
+
"types": "./lib/client.d.ts",
|
|
30
|
+
"default": "./lib/client.js"
|
|
31
|
+
},
|
|
32
|
+
"./invariant": {
|
|
33
|
+
"types": "./lib/invariant.d.ts",
|
|
34
|
+
"default": "./lib/invariant.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"dsh": {
|
|
39
|
+
"client": {
|
|
40
|
+
"platform": "web",
|
|
41
|
+
"inject": [
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
44
|
+
"@deepseek-ai/dsh-client-connection"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"lib",
|
|
50
|
+
"examples",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"clean": "node ./scripts/build.mjs --clean-only",
|
|
56
|
+
"build": "node ./scripts/build.mjs",
|
|
57
|
+
"check": "tsc --noEmit",
|
|
58
|
+
"test": "tsx --test test/**/*.test.ts",
|
|
59
|
+
"prepare": "npm run build",
|
|
60
|
+
"prepack": "npm run build",
|
|
61
|
+
"verify": "npm run clean && npm run check && npm test && npm run build && npm pack --dry-run"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
68
|
+
"@deepseek-ai/dsh-credentials": "^0.1.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-web": "^0.1.1-rc.2"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
76
|
+
"@deepseek-ai/dsh-credentials": "^0.1.1-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.1-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-web": "^0.1.1-rc.2",
|
|
81
|
+
"@types/node": "^22.0.0",
|
|
82
|
+
"tsx": "^4.19.0",
|
|
83
|
+
"typescript": "^5.7.0"
|
|
84
|
+
}
|
|
85
|
+
}
|