@stratusagent/search 0.11.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 +271 -0
- package/dist/contract.d.ts +171 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +40 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +78 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +186 -0
- package/dist/options.js.map +1 -0
- package/dist/results.d.ts +53 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +224 -0
- package/dist/results.js.map +1 -0
- package/dist/tool.d.ts +62 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +148 -0
- package/dist/tool.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stratus Labs
|
|
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,271 @@
|
|
|
1
|
+
# @stratusagent/search
|
|
2
|
+
|
|
3
|
+
**`web.search`: the contract, not a backend.** One tool name, one option
|
|
4
|
+
meaning, one result shape — so two independently written search plugins are
|
|
5
|
+
interchangeable behind one soul allowlist, and swapping vendors changes no
|
|
6
|
+
soul, no skill, and no result shape.
|
|
7
|
+
|
|
8
|
+
This package ships **no backend and reaches no vendor.** Every backend needs
|
|
9
|
+
an API key and a commercial relationship, so core ships `web.fetch` and the
|
|
10
|
+
ecosystem ships `web.search` ([`plugins.md`](../../docs/architecture/plugins.md)).
|
|
11
|
+
What has to be first-party is the half that makes the other half worth
|
|
12
|
+
anything.
|
|
13
|
+
|
|
14
|
+
## For an operator
|
|
15
|
+
|
|
16
|
+
You need a backend plugin. Install it, enable it in a trusted config, and
|
|
17
|
+
store the key:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g stratus-plugin-somesearch
|
|
21
|
+
printf %s "$SEARCH_KEY" | stratus credential set search.apiKey
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```jsonc
|
|
25
|
+
// ~/.stratus/config.json — a trusted config only
|
|
26
|
+
{
|
|
27
|
+
"plugins": {
|
|
28
|
+
"stratus-plugin-somesearch": { "enabled": true, "maxSearchesPerDay": 200 }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then each agent's soul opts in, on both gates — the tool and the credential:
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
---
|
|
37
|
+
id: ava
|
|
38
|
+
tools: [web.fetch, web.search]
|
|
39
|
+
credentials: [search.apiKey]
|
|
40
|
+
---
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`tools: [web.*]` picks up search too, with no soul edit, which is the reason
|
|
44
|
+
the glob exists. The `credentials:` line is separate and is **not** optional:
|
|
45
|
+
without it every call answers "Agent ava is not allowed to access credential:
|
|
46
|
+
search.apiKey".
|
|
47
|
+
|
|
48
|
+
**Two backends cannot be enabled at once.** Both contribute `web.search`, and
|
|
49
|
+
a tool name is unique per install, so that is a load-time error naming both
|
|
50
|
+
packages. A deployment that genuinely needs two backends wants two daemons.
|
|
51
|
+
|
|
52
|
+
### Settings every backend accepts
|
|
53
|
+
|
|
54
|
+
A backend's manifest must declare these in its `config` schema, because they
|
|
55
|
+
are read by the shared tool rather than by the backend's own code. All of
|
|
56
|
+
them take a per-agent override in the `agents` sub-block.
|
|
57
|
+
|
|
58
|
+
| Key | Default | What |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `maxSearchesPerDay` | `200` | Calls one agent may make in a UTC day. A query acts on nothing, but a metered API costs money and an agent in a loop can spend it. `0` refuses every call, which is how you turn one agent's search off without editing its soul. There is deliberately no "uncapped" — set a large number if that is what you mean. |
|
|
61
|
+
| `allowedHosts` | none | Hosts exempt from the address check. What a self-hosted backend on `localhost` needs. |
|
|
62
|
+
| `allowPrivateAddresses` | `false` | Reach any non-global address. Turns the SSRF protection off rather than adjusting it. |
|
|
63
|
+
|
|
64
|
+
The cap is **kept in the daemon's process**, so a restart forgives the count.
|
|
65
|
+
That is stated rather than hidden: its job is to stop a runaway loop, and a
|
|
66
|
+
loop lives inside one daemon's lifetime.
|
|
67
|
+
|
|
68
|
+
### The credential is one name, whatever the vendor
|
|
69
|
+
|
|
70
|
+
`search.apiKey`. Always. A Brave adapter asking for `BRAVE_API_KEY` and a
|
|
71
|
+
Tavily adapter asking for `TAVILY_API_KEY` would mean swapping backends edits
|
|
72
|
+
**every soul in the fleet**, even though the `web.search` allowlist entry
|
|
73
|
+
never moved.
|
|
74
|
+
|
|
75
|
+
Two corollaries. Something that is *not* secret — a Google CSE engine id, a
|
|
76
|
+
SearXNG base URL — goes in the plugin's config block, which is not
|
|
77
|
+
soul-scoped. And a backend needing **no** credential is legitimate: a
|
|
78
|
+
self-hosted instance never calls `get`, and is conforming rather than broken.
|
|
79
|
+
|
|
80
|
+
A key belongs in the credential store, never as a literal in the config
|
|
81
|
+
block — a key in `stratus.config.json` is a key in a file people commit.
|
|
82
|
+
|
|
83
|
+
## For a backend author
|
|
84
|
+
|
|
85
|
+
Implement one method. Everything else — the tool name, the schema, the
|
|
86
|
+
option parsing, the filtering, the envelope, the credential seam, the address
|
|
87
|
+
policy — is this package's, which is what makes your backend swappable with
|
|
88
|
+
somebody else's.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import {
|
|
92
|
+
createWebSearchTool,
|
|
93
|
+
defineSearchProvider,
|
|
94
|
+
SEARCH_CREDENTIAL_NAME,
|
|
95
|
+
} from '@stratusagent/search';
|
|
96
|
+
import { requestThroughPolicy } from '@stratusagent/egress';
|
|
97
|
+
import type { JsonObject, Plugin } from '@stratusagent/core';
|
|
98
|
+
|
|
99
|
+
const createBackend = (config: JsonObject) => defineSearchProvider({
|
|
100
|
+
name: 'somesearch',
|
|
101
|
+
async search(query, options, context) {
|
|
102
|
+
const key = await context.credentials.get(SEARCH_CREDENTIAL_NAME);
|
|
103
|
+
const url = new URL('https://api.somesearch.example/v1/search');
|
|
104
|
+
url.searchParams.set('q', query); // literal text — see below
|
|
105
|
+
url.searchParams.set('count', String(options.count));
|
|
106
|
+
if (options.site) url.searchParams.set('host', options.site);
|
|
107
|
+
if (options.freshness) url.searchParams.set('since', options.freshness.since);
|
|
108
|
+
|
|
109
|
+
// Every request through the policy you were handed. Never derive one.
|
|
110
|
+
const response = await requestThroughPolicy(url.href, {
|
|
111
|
+
policy: context.policy,
|
|
112
|
+
headers: { 'x-api-key': key },
|
|
113
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
114
|
+
});
|
|
115
|
+
if (response.status !== 200) {
|
|
116
|
+
throw new Error(`The somesearch API answered ${response.status}.`);
|
|
117
|
+
}
|
|
118
|
+
return (JSON.parse(response.body).results ?? []).map((row) => ({
|
|
119
|
+
title: row.title,
|
|
120
|
+
url: row.url,
|
|
121
|
+
snippet: row.description,
|
|
122
|
+
publishedAt: row.published,
|
|
123
|
+
}));
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const createPlugin = (config: JsonObject): Plugin => ({
|
|
128
|
+
name: 'stratus-plugin-somesearch',
|
|
129
|
+
setup(context) {
|
|
130
|
+
context.tools.register(createWebSearchTool({
|
|
131
|
+
provider: createBackend(config),
|
|
132
|
+
config,
|
|
133
|
+
...(context.credentials ? { credentials: context.credentials } : {}),
|
|
134
|
+
// The daemon's log records that a search ran and against which
|
|
135
|
+
// provider — never the query, which is user content.
|
|
136
|
+
onSearch: (record) => context.log?.(`web.search via ${record.provider} — ${record.results} result(s)`),
|
|
137
|
+
}));
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Your `package.json` manifest declares the tool and the credential:
|
|
143
|
+
|
|
144
|
+
```jsonc
|
|
145
|
+
"stratus": {
|
|
146
|
+
"pluginVersion": 1,
|
|
147
|
+
"contributes": { "tools": [{ "name": "web.search", "risk": "safe" }] },
|
|
148
|
+
"credentials": ["search.apiKey"],
|
|
149
|
+
"config": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"maxSearchesPerDay": { "type": "integer" },
|
|
153
|
+
"allowedHosts": { "type": "array", "items": { "type": "string" } },
|
|
154
|
+
"allowPrivateAddresses": { "type": "boolean" }
|
|
155
|
+
},
|
|
156
|
+
"additionalProperties": false
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Declare `risk: "safe"` if you like — **it will register `gated` anyway.**
|
|
162
|
+
`safe` means "run this unattended, with nobody watching", and that is not a
|
|
163
|
+
claim the code being judged gets to make about itself; the third-party floor
|
|
164
|
+
applies to `web.*` exactly as it does everywhere else.
|
|
165
|
+
|
|
166
|
+
### The options, pinned
|
|
167
|
+
|
|
168
|
+
| Field | Type | Meaning |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| `query` | `string`, required, non-empty | **Literal text.** Do whatever your provider requires — escaping, quoting, a literal-search parameter — so the upstream searches for the characters given rather than parsing operators out of them. An unbalanced quote is a search for a quote character, not a syntax error. |
|
|
171
|
+
| `count` | `integer`, 1–50, default 10 | A **maximum**. Returning fewer is normal; returning more is a contract violation, and this package truncates. |
|
|
172
|
+
| `site` | one normalized hostname | Matches that host and anything under it, on label boundaries: `example.com` matches `docs.example.com` and **not** `notexample.com`. Already lower-cased, trailing dot removed, IDN in A-label form. Not a query operator — never splice `site:` into the query. |
|
|
173
|
+
| `freshness` | `{ duration, ms, since }` | An age, resolved to an instant once per call. `since` is the oldest a result may be. |
|
|
174
|
+
|
|
175
|
+
A call carrying any **other** field is refused naming it. The caller is a
|
|
176
|
+
model writing JSON, so `freshnes: "P7D"` is a realistic mistake — and read as
|
|
177
|
+
"no freshness filter" it runs an unrestricted search that *succeeds*, after
|
|
178
|
+
which the agent says the answer is recent. A dropped option is a silently
|
|
179
|
+
ignored one arriving a step earlier, and this table is strict for that reason
|
|
180
|
+
throughout.
|
|
181
|
+
|
|
182
|
+
### What `publishedAt` may be
|
|
183
|
+
|
|
184
|
+
An **ISO 8601 instant**, and the shape is checked before anything is parsed
|
|
185
|
+
— because `new Date(value)` is not a validator. It reads `03/04/2026` as
|
|
186
|
+
4 March without ever saying whether the backend meant March or April, and it
|
|
187
|
+
silently *corrects* `2026-02-30` into 2 March. A lenient parse therefore does
|
|
188
|
+
not fail to date a result, it **invents** a date, and a `freshness` filter
|
|
189
|
+
then keeps or drops the result on the strength of it.
|
|
190
|
+
|
|
191
|
+
| Accepted | Read as |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `2026-09-01T09:00:00Z` | itself |
|
|
194
|
+
| `2026-09-01T11:00:00+02:00`, `…+0200` | the same instant, in UTC |
|
|
195
|
+
| `2026-09-01` | UTC midnight |
|
|
196
|
+
|
|
197
|
+
Everything else becomes absent, which under `freshness` means the result is
|
|
198
|
+
dropped: a locale-formatted date, a day that does not exist, and a
|
|
199
|
+
**zone-less** `2026-09-01T10:00:00` — that last one is *local* time to
|
|
200
|
+
whatever machine parses it, so the same string is two different instants on
|
|
201
|
+
two daemons, which is the disagreement this package exists to remove. Convert
|
|
202
|
+
your vendor's format; that is the one job a backend cannot hand back.
|
|
203
|
+
|
|
204
|
+
A date with no time is accepted rather than refused because plenty of
|
|
205
|
+
backends only have a day, and refusing them would silently empty every
|
|
206
|
+
`freshness` search against such a backend. Reading it as midnight errs
|
|
207
|
+
towards calling a page slightly older than it is, which is the safe
|
|
208
|
+
direction.
|
|
209
|
+
|
|
210
|
+
Results older than `freshness` are excluded, **and so are results with no
|
|
211
|
+
known date** — which is the case every backend produces without being asked.
|
|
212
|
+
Leaving that unsaid would let two conforming adapters return different sets
|
|
213
|
+
for identical calls. The agent asked for recent results and will say the
|
|
214
|
+
answer is recent, and "we could not determine a date" is not evidence that it
|
|
215
|
+
is. The exclusion applies *only* when `freshness` is set.
|
|
216
|
+
|
|
217
|
+
**Refuse rather than approximate.** A backend that silently ignores an option
|
|
218
|
+
is worse than one that fails. Declare what you cannot honor and the call is
|
|
219
|
+
refused naming it, before a request is spent:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
defineSearchProvider({ name: 'somesearch', unsupported: ['freshness'], search })
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Omitting `unsupported` means you honor everything, which fails towards the
|
|
226
|
+
contract: you opt out deliberately rather than being quietly excused.
|
|
227
|
+
|
|
228
|
+
### What you do not have to get right
|
|
229
|
+
|
|
230
|
+
The shared tool applies `count`, `site`, and `freshness` to whatever you
|
|
231
|
+
return, validates every result URL against the address policy, strips markup
|
|
232
|
+
from snippets, and turns `publishedAt` into a UTC instant (dropping anything
|
|
233
|
+
that will not parse, rather than guessing).
|
|
234
|
+
|
|
235
|
+
Pass your vendor's decorated snippet straight through — highlight markup is
|
|
236
|
+
expected. Inline formatting is **removed**, not replaced with a space,
|
|
237
|
+
because vendors highlight the matched *substring*: `un<strong>expected</strong>`
|
|
238
|
+
reads back as `unexpected`, and `<strong>kettle</strong>.` keeps its full
|
|
239
|
+
stop attached. Anything that is not inline formatting becomes a space
|
|
240
|
+
instead, so two table cells never fuse into `AlphaBeta`. Send the options upstream anyway
|
|
241
|
+
where your vendor supports them — native filtering ranks better than a filter
|
|
242
|
+
applied afterwards — but the guarantee does not rest on your having done so.
|
|
243
|
+
|
|
244
|
+
Freshness is fixed-length only: `P7D`, `P4W`, `PT12H`, `PT30M`. `P1M` and
|
|
245
|
+
`P1Y` are refused, because a month from 31 March and a year from 29 February
|
|
246
|
+
each have more than one defensible answer, and an adapter that converted to
|
|
247
|
+
30 or 365 days would disagree with one that did not — at boundaries,
|
|
248
|
+
intermittently. The rejection is **positional**: `PT30M` is thirty minutes
|
|
249
|
+
and is fine.
|
|
250
|
+
|
|
251
|
+
## The result envelope
|
|
252
|
+
|
|
253
|
+
```jsonc
|
|
254
|
+
{
|
|
255
|
+
"query": "kettles",
|
|
256
|
+
"provider": "somesearch",
|
|
257
|
+
"results": [
|
|
258
|
+
{ "title": "…", "url": "https://…", "snippet": "…", "publishedAt": "2026-09-01T09:00:00.000Z" }
|
|
259
|
+
],
|
|
260
|
+
"untrusted": true,
|
|
261
|
+
"untrustedNote": "Titles and snippets below were written by the pages that own them…"
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Snippets are attacker-controlled text: written by whoever owns the page,
|
|
266
|
+
selected by a third-party ranker, and handed to a model that is about to
|
|
267
|
+
decide what to do next. **This does not solve prompt injection** and does not
|
|
268
|
+
claim to — it marks the boundary, because doing so is nearly free here and a
|
|
269
|
+
retrofit across every tool that returns third-party text is not. Whether the
|
|
270
|
+
marking should become a kernel concept that `web.fetch` and the MCP bridge
|
|
271
|
+
share is [13](../../docs/roadmap/13-search.md)'s open question, still open.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a search backend implements, and what it is handed per call.
|
|
3
|
+
*
|
|
4
|
+
* Published from this repository, implemented outside it. Every backend
|
|
5
|
+
* needs a vendor key and a commercial relationship, so the backends belong
|
|
6
|
+
* to the ecosystem — but the *shape* has to be first-party or the ecosystem
|
|
7
|
+
* is worth nothing: two independently written plugins are interchangeable
|
|
8
|
+
* only if a soul allowlisting `web.search` gets the same tool, the same
|
|
9
|
+
* options, and the same results whichever one is installed.
|
|
10
|
+
*/
|
|
11
|
+
import type { ScopedCredentials } from '@stratusagent/core';
|
|
12
|
+
import type { EgressPolicy } from '@stratusagent/egress';
|
|
13
|
+
/** The tool name, in the `web` namespace on purpose — see the README. */
|
|
14
|
+
export declare const WEB_SEARCH_TOOL_NAME = "web.search";
|
|
15
|
+
/**
|
|
16
|
+
* The one credential name every backend asks for, whatever vendor is
|
|
17
|
+
* behind it.
|
|
18
|
+
*
|
|
19
|
+
* This is not tidiness. A soul's `credentials` list is enforced by the
|
|
20
|
+
* `CredentialResolver`, so a Brave adapter asking for `BRAVE_API_KEY` and a
|
|
21
|
+
* Tavily adapter asking for `TAVILY_API_KEY` would mean swapping backends
|
|
22
|
+
* edits every soul in the fleet — even though the `web.search` allowlist
|
|
23
|
+
* entry never moved. One name is what makes the swap free.
|
|
24
|
+
*
|
|
25
|
+
* A backend needing something that is not secret (a Google CSE engine id, a
|
|
26
|
+
* SearXNG base URL) puts it in the plugin's config block, which is not
|
|
27
|
+
* soul-scoped. A backend needing no credential at all never calls `get`,
|
|
28
|
+
* and is conforming rather than broken.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SEARCH_CREDENTIAL_NAME = "search.apiKey";
|
|
31
|
+
/**
|
|
32
|
+
* The options a backend may refuse by name.
|
|
33
|
+
*
|
|
34
|
+
* `count` is absent on purpose: it is a maximum, and this package truncates
|
|
35
|
+
* to it after the fact, so every backend honors it whether or not its
|
|
36
|
+
* vendor has a parameter for it.
|
|
37
|
+
*/
|
|
38
|
+
export type SearchOptionName = 'site' | 'freshness';
|
|
39
|
+
/**
|
|
40
|
+
* An age, resolved once per call so every part of the request agrees on
|
|
41
|
+
* what "now" was.
|
|
42
|
+
*
|
|
43
|
+
* All three forms are given because backends need different ones: some take
|
|
44
|
+
* a coarse enum, some take a date range, and the filter this package
|
|
45
|
+
* applies afterwards needs the instant.
|
|
46
|
+
*/
|
|
47
|
+
export interface SearchFreshness {
|
|
48
|
+
/** The ISO 8601 duration as the caller wrote it, normalized to upper case (`P7D`). */
|
|
49
|
+
duration: string;
|
|
50
|
+
/** That duration in milliseconds. Always a fixed length — see `parseFreshnessDuration`. */
|
|
51
|
+
ms: number;
|
|
52
|
+
/** The oldest instant a result may carry, as a UTC ISO 8601 instant. */
|
|
53
|
+
since: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One call's options, already parsed, validated, and normalized. A backend
|
|
57
|
+
* never sees the agent's raw input — that is the point of the shape.
|
|
58
|
+
*/
|
|
59
|
+
export interface SearchOptions {
|
|
60
|
+
/**
|
|
61
|
+
* A **maximum**. Returning fewer is normal; returning more is a contract
|
|
62
|
+
* violation, and this package truncates rather than trusting.
|
|
63
|
+
*/
|
|
64
|
+
count: number;
|
|
65
|
+
/**
|
|
66
|
+
* One normalized hostname — lower case, no trailing dot, IDN in its
|
|
67
|
+
* A-label form. Matches that host and anything under it on label
|
|
68
|
+
* boundaries: `example.com` matches `docs.example.com` and never
|
|
69
|
+
* `notexample.com`.
|
|
70
|
+
*
|
|
71
|
+
* Not a query operator. A backend without native site filtering must not
|
|
72
|
+
* splice `site:` into the query — the query is literal text — and can
|
|
73
|
+
* leave the filtering to this package, which applies it either way.
|
|
74
|
+
*/
|
|
75
|
+
site?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Results older than this are excluded, **and so are results with no
|
|
78
|
+
* known date**. A backend that cannot honor it declares `freshness` in
|
|
79
|
+
* `unsupported` rather than ignoring it: an agent that asked for recent
|
|
80
|
+
* results will say the answer is recent.
|
|
81
|
+
*/
|
|
82
|
+
freshness?: SearchFreshness;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* What one search call is handed besides the query.
|
|
86
|
+
*
|
|
87
|
+
* The third parameter is what makes per-agent keys possible at all.
|
|
88
|
+
* `CredentialResolver.resolve` takes an `AgentDefinition`, so a
|
|
89
|
+
* `search(query, options)` seam would leave one shared adapter instance no
|
|
90
|
+
* way to reach the calling agent's key except a factory outside the
|
|
91
|
+
* contract or mutable global state.
|
|
92
|
+
*/
|
|
93
|
+
export interface SearchContext {
|
|
94
|
+
/**
|
|
95
|
+
* Already bound to the calling agent, and `get(name)` is the whole
|
|
96
|
+
* surface. An adapter has no way to *name* another agent, so it has no
|
|
97
|
+
* way to reach another agent's key — where handing it an
|
|
98
|
+
* `AgentDefinition` and a resolver would make agent isolation something
|
|
99
|
+
* every third-party backend author has to not get wrong.
|
|
100
|
+
*/
|
|
101
|
+
credentials: ScopedCredentials;
|
|
102
|
+
/**
|
|
103
|
+
* The address policy every request this call makes must go through,
|
|
104
|
+
* resolved for the calling agent. Backends do not derive it: pass it
|
|
105
|
+
* straight to `requestThroughPolicy` from `@stratusagent/egress`. A
|
|
106
|
+
* self-hosted backend on `http://localhost:8888` is a legitimate
|
|
107
|
+
* configuration and a deliberate allowance, which is exactly the
|
|
108
|
+
* decision this policy already carries.
|
|
109
|
+
*/
|
|
110
|
+
policy: EgressPolicy;
|
|
111
|
+
/**
|
|
112
|
+
* The turn's. A cancelled turn must stop the HTTP request, not merely
|
|
113
|
+
* stop waiting on it.
|
|
114
|
+
*/
|
|
115
|
+
signal?: AbortSignal;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One result, after the backend has reshaped whatever its vendor returned.
|
|
119
|
+
*
|
|
120
|
+
* A backend's raw payload never reaches the agent. This is the whole value
|
|
121
|
+
* of specifying the step: a provider that reshapes its JSON must not
|
|
122
|
+
* reshape what every soul and skill in the fleet was written against.
|
|
123
|
+
*/
|
|
124
|
+
export interface SearchResult {
|
|
125
|
+
title: string;
|
|
126
|
+
/** Absolute, `http`/`https` only. Validated against the address policy before an agent sees it. */
|
|
127
|
+
url: string;
|
|
128
|
+
/** Plain text. Markup is stripped by this package, so a backend may pass its vendor's decorated string through. */
|
|
129
|
+
snippet?: string;
|
|
130
|
+
/**
|
|
131
|
+
* An ISO 8601 instant in UTC when the backend has one, and absent when it
|
|
132
|
+
* does not — never a guess, never a locale-formatted date. Anything that
|
|
133
|
+
* will not parse as an instant is treated as absent, which under
|
|
134
|
+
* `freshness` means the result is dropped.
|
|
135
|
+
*/
|
|
136
|
+
publishedAt?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface SearchProvider {
|
|
139
|
+
/**
|
|
140
|
+
* What this backend is, for the daemon's log and the result envelope —
|
|
141
|
+
* `brave`, `tavily`, `searxng`. Not a URL and not a secret: it is written
|
|
142
|
+
* to a log an operator shares.
|
|
143
|
+
*/
|
|
144
|
+
readonly name: string;
|
|
145
|
+
/**
|
|
146
|
+
* Options this backend cannot honor. A call using one is refused naming
|
|
147
|
+
* it, before a request is made.
|
|
148
|
+
*
|
|
149
|
+
* Omitted means "all of them", which is the right default because it
|
|
150
|
+
* fails towards the contract: a backend opts *out* deliberately rather
|
|
151
|
+
* than being quietly excused for having forgotten. Refusing beats
|
|
152
|
+
* approximating — a backend that silently ignored `freshness` would be
|
|
153
|
+
* worse than one that failed, because the agent's next sentence states
|
|
154
|
+
* the result is recent.
|
|
155
|
+
*/
|
|
156
|
+
readonly unsupported?: readonly SearchOptionName[];
|
|
157
|
+
search(query: string, options: SearchOptions, context: SearchContext): Promise<SearchResult[]>;
|
|
158
|
+
}
|
|
159
|
+
export interface SearchProviderDefinition {
|
|
160
|
+
name: string;
|
|
161
|
+
unsupported?: readonly SearchOptionName[];
|
|
162
|
+
search(query: string, options: SearchOptions, context: SearchContext): Promise<SearchResult[]>;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Canonicalize a backend definition. The conditional spread is not
|
|
166
|
+
* decoration: `exactOptionalPropertyTypes` means an absent `unsupported`
|
|
167
|
+
* and one explicitly set to `undefined` are different types, and only the
|
|
168
|
+
* first one means "honors everything".
|
|
169
|
+
*/
|
|
170
|
+
export declare const defineSearchProvider: (definition: SearchProviderDefinition) => SearchProvider;
|
|
171
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;IACjB,2FAA2F;IAC3F,EAAE,EAAE,MAAM,CAAC;IACX,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,WAAW,EAAE,iBAAiB,CAAC;IAC/B;;;;;;;OAOG;IACH,MAAM,EAAE,YAAY,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,mGAAmG;IACnG,GAAG,EAAE,MAAM,CAAC;IACZ,mHAAmH;IACnH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACnD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAChG;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC1C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAChG;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,YAAY,wBAAwB,KAAG,cAI1E,CAAC"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a search backend implements, and what it is handed per call.
|
|
3
|
+
*
|
|
4
|
+
* Published from this repository, implemented outside it. Every backend
|
|
5
|
+
* needs a vendor key and a commercial relationship, so the backends belong
|
|
6
|
+
* to the ecosystem — but the *shape* has to be first-party or the ecosystem
|
|
7
|
+
* is worth nothing: two independently written plugins are interchangeable
|
|
8
|
+
* only if a soul allowlisting `web.search` gets the same tool, the same
|
|
9
|
+
* options, and the same results whichever one is installed.
|
|
10
|
+
*/
|
|
11
|
+
/** The tool name, in the `web` namespace on purpose — see the README. */
|
|
12
|
+
export const WEB_SEARCH_TOOL_NAME = 'web.search';
|
|
13
|
+
/**
|
|
14
|
+
* The one credential name every backend asks for, whatever vendor is
|
|
15
|
+
* behind it.
|
|
16
|
+
*
|
|
17
|
+
* This is not tidiness. A soul's `credentials` list is enforced by the
|
|
18
|
+
* `CredentialResolver`, so a Brave adapter asking for `BRAVE_API_KEY` and a
|
|
19
|
+
* Tavily adapter asking for `TAVILY_API_KEY` would mean swapping backends
|
|
20
|
+
* edits every soul in the fleet — even though the `web.search` allowlist
|
|
21
|
+
* entry never moved. One name is what makes the swap free.
|
|
22
|
+
*
|
|
23
|
+
* A backend needing something that is not secret (a Google CSE engine id, a
|
|
24
|
+
* SearXNG base URL) puts it in the plugin's config block, which is not
|
|
25
|
+
* soul-scoped. A backend needing no credential at all never calls `get`,
|
|
26
|
+
* and is conforming rather than broken.
|
|
27
|
+
*/
|
|
28
|
+
export const SEARCH_CREDENTIAL_NAME = 'search.apiKey';
|
|
29
|
+
/**
|
|
30
|
+
* Canonicalize a backend definition. The conditional spread is not
|
|
31
|
+
* decoration: `exactOptionalPropertyTypes` means an absent `unsupported`
|
|
32
|
+
* and one explicitly set to `undefined` are different types, and only the
|
|
33
|
+
* first one means "honors everything".
|
|
34
|
+
*/
|
|
35
|
+
export const defineSearchProvider = (definition) => ({
|
|
36
|
+
name: definition.name,
|
|
37
|
+
...(definition.unsupported !== undefined ? { unsupported: definition.unsupported } : {}),
|
|
38
|
+
search: definition.search,
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,yEAAyE;AACzE,MAAM,CAAC,MAAM,oBAAoB,GAAG,YAAY,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,eAAe,CAAC;AA8ItD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,UAAoC,EAAkB,EAAE,CAAC,CAAC;IAC7F,IAAI,EAAE,UAAU,CAAC,IAAI;IACrB,GAAG,CAAC,UAAU,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACxF,MAAM,EAAE,UAAU,CAAC,MAAM;CAC1B,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `web.search`: the contract the ecosystem implements.
|
|
3
|
+
*
|
|
4
|
+
* The backends stay outside this repository — every one of them needs a
|
|
5
|
+
* vendor key and a commercial relationship, so core ships `web.fetch` and
|
|
6
|
+
* the ecosystem ships `web.search`. What has to be first-party is the half
|
|
7
|
+
* that makes the other half worth anything: a fixed tool name and a fixed
|
|
8
|
+
* result shape, so a soul or a skill written against search keeps working
|
|
9
|
+
* when the operator changes vendor.
|
|
10
|
+
*
|
|
11
|
+
* The contract is specified in `docs/roadmap/13-search.md` and summarized
|
|
12
|
+
* in this package's README; the code here is what enforces it.
|
|
13
|
+
*/
|
|
14
|
+
export { defineSearchProvider, SEARCH_CREDENTIAL_NAME, WEB_SEARCH_TOOL_NAME, type SearchContext, type SearchFreshness, type SearchOptionName, type SearchOptions, type SearchProvider, type SearchProviderDefinition, type SearchResult, } from './contract.ts';
|
|
15
|
+
export { hostMatchesSite, normalizeSearchSite, parseFreshnessDuration, parseSearchCall, SearchOptionError, SEARCH_COUNT_DEFAULT, SEARCH_COUNT_MAX, type ParsedSearchCall, } from './options.ts';
|
|
16
|
+
export { normalizePublishedAt, normalizeSearchResults, plainSnippet, } from './results.ts';
|
|
17
|
+
export { createWebSearchTool, DEFAULT_MAX_SEARCHES_PER_DAY, UNTRUSTED_RESULT_NOTE, type SearchLogRecord, type WebSearchToolOptions, } from './tool.ts';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,YAAY,GAClB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,YAAY,GACb,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `web.search`: the contract the ecosystem implements.
|
|
3
|
+
*
|
|
4
|
+
* The backends stay outside this repository — every one of them needs a
|
|
5
|
+
* vendor key and a commercial relationship, so core ships `web.fetch` and
|
|
6
|
+
* the ecosystem ships `web.search`. What has to be first-party is the half
|
|
7
|
+
* that makes the other half worth anything: a fixed tool name and a fixed
|
|
8
|
+
* result shape, so a soul or a skill written against search keeps working
|
|
9
|
+
* when the operator changes vendor.
|
|
10
|
+
*
|
|
11
|
+
* The contract is specified in `docs/roadmap/13-search.md` and summarized
|
|
12
|
+
* in this package's README; the code here is what enforces it.
|
|
13
|
+
*/
|
|
14
|
+
export { defineSearchProvider, SEARCH_CREDENTIAL_NAME, WEB_SEARCH_TOOL_NAME, } from "./contract.js";
|
|
15
|
+
export { hostMatchesSite, normalizeSearchSite, parseFreshnessDuration, parseSearchCall, SearchOptionError, SEARCH_COUNT_DEFAULT, SEARCH_COUNT_MAX, } from "./options.js";
|
|
16
|
+
export { normalizePublishedAt, normalizeSearchResults, plainSnippet, } from "./results.js";
|
|
17
|
+
export { createWebSearchTool, DEFAULT_MAX_SEARCHES_PER_DAY, UNTRUSTED_RESULT_NOTE, } from "./tool.js";
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,GAQrB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GAEjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,YAAY,GACb,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,qBAAqB,GAGtB,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every option pinned to a meaning.
|
|
3
|
+
*
|
|
4
|
+
* Naming the options is not specifying them: two adapters can both satisfy
|
|
5
|
+
* `{ search(query, options) }` while accepting incompatible calls, which
|
|
6
|
+
* defeats the one criterion this package exists for. So the parsing lives
|
|
7
|
+
* here, once, in front of every backend — a call that means one thing on
|
|
8
|
+
* Brave means the same thing on SearXNG because neither of them parsed it.
|
|
9
|
+
*/
|
|
10
|
+
import type { JsonObject } from '@stratusagent/core';
|
|
11
|
+
import { type SearchOptions } from './contract.ts';
|
|
12
|
+
export declare const SEARCH_COUNT_DEFAULT = 10;
|
|
13
|
+
export declare const SEARCH_COUNT_MAX = 50;
|
|
14
|
+
/**
|
|
15
|
+
* A call this contract refuses. Exported because a backend throws it too:
|
|
16
|
+
* an option it declared `unsupported` and a value its vendor rejects are
|
|
17
|
+
* the same kind of answer to the agent.
|
|
18
|
+
*/
|
|
19
|
+
export declare class SearchOptionError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* An ISO 8601 duration of fixed length, in milliseconds.
|
|
24
|
+
*
|
|
25
|
+
* **Fixed-length only, because `P1M` and `P1Y` are not one length.**
|
|
26
|
+
* Calendar arithmetic is where two conforming adapters part company:
|
|
27
|
+
* subtracting a month from 31 March, or a year from 29 February, has more
|
|
28
|
+
* than one defensible answer, and an adapter that converts to 30 or 365
|
|
29
|
+
* days instead disagrees with one that does not — at boundaries,
|
|
30
|
+
* intermittently, which is the worst way for a difference to show up.
|
|
31
|
+
* Rejecting the two ambiguous designators costs nothing a caller wants:
|
|
32
|
+
* `P30D` and `P365D` say what someone asking for "a month" or "a year" of
|
|
33
|
+
* search results actually means, and say it identically everywhere.
|
|
34
|
+
*
|
|
35
|
+
* `M` means two things and only one of them is rejected. ISO 8601 uses the
|
|
36
|
+
* same letter for months in the date portion and minutes after the `T`, so
|
|
37
|
+
* "reject `M`" would throw out `PT30M`, which is fixed-length and perfectly
|
|
38
|
+
* well defined.
|
|
39
|
+
*/
|
|
40
|
+
export declare const parseFreshnessDuration: (raw: string) => number;
|
|
41
|
+
/**
|
|
42
|
+
* One hostname, normalized so two adapters compare the same string.
|
|
43
|
+
*
|
|
44
|
+
* **Hostname with label-boundary matching, not "registrable domain".** A
|
|
45
|
+
* registrable domain is the public-suffix-plus-one form, so
|
|
46
|
+
* `docs.example.com` is not one — an adapter enforcing that term would
|
|
47
|
+
* reject it or widen it up to `example.com`, silently broadening a search
|
|
48
|
+
* another adapter would keep narrow. This needs no public-suffix list,
|
|
49
|
+
* which is a dependency worth not acquiring. The two-label minimum is what
|
|
50
|
+
* stops `site: com`.
|
|
51
|
+
*/
|
|
52
|
+
export declare const normalizeSearchSite: (raw: string) => string;
|
|
53
|
+
/**
|
|
54
|
+
* Whether one result's host is inside a `site` filter.
|
|
55
|
+
*
|
|
56
|
+
* On label boundaries, which is the whole rule: `notexample.com` ends with
|
|
57
|
+
* `example.com` as a string and is a different company.
|
|
58
|
+
*/
|
|
59
|
+
export declare const hostMatchesSite: (host: string, site: string) => boolean;
|
|
60
|
+
export interface ParsedSearchCall {
|
|
61
|
+
query: string;
|
|
62
|
+
options: SearchOptions;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One tool call's input, turned into the options a backend receives.
|
|
66
|
+
*
|
|
67
|
+
* The query is passed through as **literal text**. "Verbatim" was the wrong
|
|
68
|
+
* word for it and the two halves could not both be true: most search APIs
|
|
69
|
+
* parse their own operators out of the query field, so handing the string
|
|
70
|
+
* over untouched is precisely what lets the upstream reinterpret `site:`,
|
|
71
|
+
* `OR`, a minus sign, or a quote — while a backend that escapes them is no
|
|
72
|
+
* longer passing anything verbatim. What is required is the *meaning*: the
|
|
73
|
+
* query is literal, and each backend does whatever its provider needs to
|
|
74
|
+
* make that true. An unbalanced quote is a search for a quote character,
|
|
75
|
+
* not a syntax error.
|
|
76
|
+
*/
|
|
77
|
+
export declare const parseSearchCall: (input: JsonObject, now: Date) => ParsedSearchCall;
|
|
78
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,EAA8C,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAE/F,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAoBD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,sBAAsB,GAAI,KAAK,MAAM,KAAG,MAsBpD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,MA0BjD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,EAAE,MAAM,MAAM,KAAG,OAG5D,CAAC;AAkBF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,UAAU,EAAE,KAAK,IAAI,KAAG,gBA6C9D,CAAC"}
|