@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.
@@ -0,0 +1,186 @@
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 { WEB_SEARCH_TOOL_NAME } from "./contract.js";
11
+ export const SEARCH_COUNT_DEFAULT = 10;
12
+ export const SEARCH_COUNT_MAX = 50;
13
+ /**
14
+ * A call this contract refuses. Exported because a backend throws it too:
15
+ * an option it declared `unsupported` and a value its vendor rejects are
16
+ * the same kind of answer to the agent.
17
+ */
18
+ export class SearchOptionError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'SearchOptionError';
22
+ }
23
+ }
24
+ /**
25
+ * Fixed-length designators only. `W` and `D` before the `T`, `H`, `M` and
26
+ * `S` after it — and note that `M` is absent from the date portion on
27
+ * purpose, which is what makes the rejection of `P1M` positional rather
28
+ * than a ban on the letter.
29
+ *
30
+ * The lookahead after `T` is what rejects a bare `P1DT`: ISO 8601 requires
31
+ * at least one component after the designator, and without it the optional
32
+ * group matches nothing and the trailing `T` is silently ignored.
33
+ */
34
+ const FIXED_DURATION = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?=\d)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
35
+ /** A year or a month in the date portion — the two ambiguous ones. */
36
+ const CALENDAR_DURATION = /^P\d+[YM]/;
37
+ const UNITS = ['W', 'D', 'H', 'M', 'S'];
38
+ const MS = { W: 604_800_000, D: 86_400_000, H: 3_600_000, M: 60_000, S: 1000 };
39
+ /**
40
+ * An ISO 8601 duration of fixed length, in milliseconds.
41
+ *
42
+ * **Fixed-length only, because `P1M` and `P1Y` are not one length.**
43
+ * Calendar arithmetic is where two conforming adapters part company:
44
+ * subtracting a month from 31 March, or a year from 29 February, has more
45
+ * than one defensible answer, and an adapter that converts to 30 or 365
46
+ * days instead disagrees with one that does not — at boundaries,
47
+ * intermittently, which is the worst way for a difference to show up.
48
+ * Rejecting the two ambiguous designators costs nothing a caller wants:
49
+ * `P30D` and `P365D` say what someone asking for "a month" or "a year" of
50
+ * search results actually means, and say it identically everywhere.
51
+ *
52
+ * `M` means two things and only one of them is rejected. ISO 8601 uses the
53
+ * same letter for months in the date portion and minutes after the `T`, so
54
+ * "reject `M`" would throw out `PT30M`, which is fixed-length and perfectly
55
+ * well defined.
56
+ */
57
+ export const parseFreshnessDuration = (raw) => {
58
+ const value = raw.trim().toUpperCase();
59
+ const match = FIXED_DURATION.exec(value);
60
+ const total = UNITS.reduce((sum, unit, index) => {
61
+ const digits = match?.[index + 1];
62
+ return digits === undefined ? sum : sum + Number(digits) * MS[unit];
63
+ }, 0);
64
+ if (!match || total === 0) {
65
+ if (CALENDAR_DURATION.test(value)) {
66
+ throw new SearchOptionError(`freshness ${JSON.stringify(raw)} is a calendar length, which is not one length: a month from 31 March and a year `
67
+ + 'from 29 February each have more than one defensible answer. Write P30D or P365D instead. '
68
+ + '(PT30M, meaning thirty minutes, is fine — the rejection is about the date portion.)');
69
+ }
70
+ throw new SearchOptionError(`freshness must be a positive ISO 8601 duration of weeks, days, hours, minutes, or seconds — P7D, P4W, PT12H, PT30M. `
71
+ + `Got ${JSON.stringify(raw)}.`);
72
+ }
73
+ return total;
74
+ };
75
+ /**
76
+ * One hostname, normalized so two adapters compare the same string.
77
+ *
78
+ * **Hostname with label-boundary matching, not "registrable domain".** A
79
+ * registrable domain is the public-suffix-plus-one form, so
80
+ * `docs.example.com` is not one — an adapter enforcing that term would
81
+ * reject it or widen it up to `example.com`, silently broadening a search
82
+ * another adapter would keep narrow. This needs no public-suffix list,
83
+ * which is a dependency worth not acquiring. The two-label minimum is what
84
+ * stops `site: com`.
85
+ */
86
+ export const normalizeSearchSite = (raw) => {
87
+ const trimmed = raw.trim().replace(/\.$/, '');
88
+ if (trimmed.length === 0) {
89
+ throw new SearchOptionError('site must be a hostname like example.com; it was empty.');
90
+ }
91
+ if (/[\s/\\?#@:]/.test(trimmed)) {
92
+ throw new SearchOptionError(`site must be a bare hostname — no scheme, no port, no path. Got ${JSON.stringify(raw)}; write example.com, not https://example.com/docs.`);
93
+ }
94
+ let hostname;
95
+ try {
96
+ // The URL parser is what lower-cases and converts an IDN to its A-label
97
+ // form, so `Bücher.de` and `bücher.de` and `xn--bcher-kva.de` are one
98
+ // string by the time anything compares them.
99
+ hostname = new URL(`https://${trimmed}`).hostname;
100
+ }
101
+ catch {
102
+ throw new SearchOptionError(`site is not a hostname: ${JSON.stringify(raw)}.`);
103
+ }
104
+ const labels = hostname.split('.');
105
+ if (labels.length < 2 || labels.some((label) => label.length === 0)) {
106
+ throw new SearchOptionError(`site must name a host with at least two labels — example.com or docs.example.com, not ${JSON.stringify(raw)}.`);
107
+ }
108
+ return hostname;
109
+ };
110
+ /**
111
+ * Whether one result's host is inside a `site` filter.
112
+ *
113
+ * On label boundaries, which is the whole rule: `notexample.com` ends with
114
+ * `example.com` as a string and is a different company.
115
+ */
116
+ export const hostMatchesSite = (host, site) => {
117
+ const normalized = host.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
118
+ return normalized === site || normalized.endsWith(`.${site}`);
119
+ };
120
+ /**
121
+ * Every field a call may carry. A misspelled option is refused rather than
122
+ * ignored, for the reason the rest of this file is strict: `freshnes: "P7D"`
123
+ * read as "no freshness filter" runs an unrestricted search that *succeeds*,
124
+ * and the agent then says the answer is recent. A silently dropped option is
125
+ * the same failure as a silently ignored one, arriving one step earlier.
126
+ */
127
+ const KNOWN_FIELDS = ['query', 'count', 'site', 'freshness'];
128
+ const requireString = (value, field) => {
129
+ if (typeof value !== 'string') {
130
+ throw new SearchOptionError(`${field} must be a string; got ${value === null ? 'null' : typeof value}.`);
131
+ }
132
+ return value;
133
+ };
134
+ /**
135
+ * One tool call's input, turned into the options a backend receives.
136
+ *
137
+ * The query is passed through as **literal text**. "Verbatim" was the wrong
138
+ * word for it and the two halves could not both be true: most search APIs
139
+ * parse their own operators out of the query field, so handing the string
140
+ * over untouched is precisely what lets the upstream reinterpret `site:`,
141
+ * `OR`, a minus sign, or a quote — while a backend that escapes them is no
142
+ * longer passing anything verbatim. What is required is the *meaning*: the
143
+ * query is literal, and each backend does whatever its provider needs to
144
+ * make that true. An unbalanced quote is a search for a quote character,
145
+ * not a syntax error.
146
+ */
147
+ export const parseSearchCall = (input, now) => {
148
+ const unknown = Object.keys(input).filter((key) => !KNOWN_FIELDS.includes(key));
149
+ if (unknown.length > 0) {
150
+ throw new SearchOptionError(`${unknown.join(', ')} ${unknown.length === 1 ? 'is not a field' : 'are not fields'} of ${WEB_SEARCH_TOOL_NAME}. `
151
+ + `It takes ${KNOWN_FIELDS.join(', ')}.`);
152
+ }
153
+ const query = requireString(input.query, 'query').trim();
154
+ if (query.length === 0) {
155
+ throw new SearchOptionError('query is required and must not be empty.');
156
+ }
157
+ let count = SEARCH_COUNT_DEFAULT;
158
+ if (input.count !== undefined) {
159
+ if (typeof input.count !== 'number' || !Number.isInteger(input.count)) {
160
+ throw new SearchOptionError(`count must be a whole number between 1 and ${SEARCH_COUNT_MAX}; got ${JSON.stringify(input.count)}.`);
161
+ }
162
+ if (input.count < 1 || input.count > SEARCH_COUNT_MAX) {
163
+ throw new SearchOptionError(`count must be between 1 and ${SEARCH_COUNT_MAX}; got ${input.count}.`);
164
+ }
165
+ count = input.count;
166
+ }
167
+ const site = input.site === undefined ? undefined : normalizeSearchSite(requireString(input.site, 'site'));
168
+ let freshness;
169
+ if (input.freshness !== undefined) {
170
+ const written = requireString(input.freshness, 'freshness');
171
+ const ms = parseFreshnessDuration(written);
172
+ // Measured back from the instant the request is made, in UTC, and
173
+ // resolved once so the backend's native filter and the filter applied
174
+ // afterwards cannot disagree about when "now" was.
175
+ freshness = { duration: written.trim().toUpperCase(), ms, since: new Date(now.getTime() - ms).toISOString() };
176
+ }
177
+ return {
178
+ query,
179
+ options: {
180
+ count,
181
+ ...(site !== undefined ? { site } : {}),
182
+ ...(freshness !== undefined ? { freshness } : {}),
183
+ },
184
+ };
185
+ };
186
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,oBAAoB,EAA4C,MAAM,eAAe,CAAC;AAE/F,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAEnC;;;;GAIG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,cAAc,GAAG,wEAAwE,CAAC;AAEhG,sEAAsE;AACtE,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAEtC,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AACjD,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAW,CAAC;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,GAAW,EAAU,EAAE;IAC5D,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACvC,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;QAC9C,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAClC,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,EAAE,CAAC,CAAC,CAAC;IAEN,IAAI,CAAC,KAAK,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAC1B,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,iBAAiB,CACzB,aAAa,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,mFAAmF;kBACjH,2FAA2F;kBAC3F,qFAAqF,CACxF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,iBAAiB,CACzB,sHAAsH;cACpH,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAChC,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAW,EAAU,EAAE;IACzD,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC9C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,iBAAiB,CAAC,yDAAyD,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,iBAAiB,CACzB,mEAAmE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,oDAAoD,CAC3I,CAAC;IACJ,CAAC;IACD,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,wEAAwE;QACxE,sEAAsE;QACtE,6CAA6C;QAC7C,QAAQ,GAAG,IAAI,GAAG,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,iBAAiB,CAAC,2BAA2B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,iBAAiB,CACzB,yFAAyF,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAChH,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAE,IAAY,EAAW,EAAE;IACrE,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACjF,OAAO,UAAU,KAAK,IAAI,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAU,CAAC;AAEtE,MAAM,aAAa,GAAG,CAAC,KAAc,EAAE,KAAa,EAAU,EAAE;IAC9D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,iBAAiB,CAAC,GAAG,KAAK,0BAA0B,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,GAAG,CAAC,CAAC;IAC3G,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAOF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAiB,EAAE,GAAS,EAAoB,EAAE;IAChF,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAE,YAAkC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACvG,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,iBAAiB,CACzB,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,OAAO,oBAAoB,IAAI;cAChH,YAAY,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACzC,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACzD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,iBAAiB,CAAC,0CAA0C,CAAC,CAAC;IAC1E,CAAC;IAED,IAAI,KAAK,GAAG,oBAAoB,CAAC;IACjC,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACtE,MAAM,IAAI,iBAAiB,CAAC,8CAA8C,gBAAgB,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrI,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,gBAAgB,EAAE,CAAC;YACtD,MAAM,IAAI,iBAAiB,CAAC,+BAA+B,gBAAgB,SAAS,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;QACtG,CAAC;QACD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAE3G,IAAI,SAAsC,CAAC;IAC3C,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QAC5D,MAAM,EAAE,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;QAC3C,kEAAkE;QAClE,sEAAsE;QACtE,mDAAmD;QACnD,SAAS,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;IAChH,CAAC;IAED,OAAO;QACL,KAAK;QACL,OAAO,EAAE;YACP,KAAK;YACL,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvC,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClD;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * What an agent actually receives, whichever backend answered.
3
+ *
4
+ * Normalized results are part of the contract, not an implementation
5
+ * detail, so the enforcement is here rather than trusted to each adapter.
6
+ * That is deliberately belt and braces: a backend passes `count`, `site`,
7
+ * and `freshness` upstream because a provider filtering natively ranks
8
+ * better than a filter applied afterwards — and this package applies them
9
+ * again because "two backends answer the same call the same way" cannot
10
+ * rest on every ecosystem author having got it right.
11
+ */
12
+ import { type EgressPolicy } from '@stratusagent/egress';
13
+ import type { SearchOptions, SearchResult } from './contract.ts';
14
+ export declare const plainSnippet: (value: string) => string;
15
+ /**
16
+ * An instant, or nothing.
17
+ *
18
+ * `publishedAt` is an ISO 8601 instant in UTC when the backend supplies one
19
+ * and absent when it does not — never a guess, never a locale-formatted
20
+ * date. Under `freshness` that means the result is dropped, which is why
21
+ * this has to be strict rather than merely tolerant: **`new Date(value)` is
22
+ * not a validator.** It reads `03/04/2026` as 4 March without ever saying
23
+ * whether the backend meant March or April, and it silently *corrects*
24
+ * `2026-02-30` into 2 March — so a lenient parse does not fail to date a
25
+ * result, it invents a date, and then a freshness filter keeps or drops the
26
+ * result on the strength of it. The shape is checked first, and the
27
+ * calendar date is round-tripped so a day that does not exist is refused
28
+ * instead of rolled forward.
29
+ *
30
+ * A date with no time is accepted and read as UTC midnight, which is what
31
+ * ISO 8601 and JavaScript both say it is. Plenty of backends only have a
32
+ * day, and refusing them would silently empty every `freshness` search
33
+ * against such a backend; reading it as midnight errs towards calling a
34
+ * page slightly older than it is, which is the safe direction here.
35
+ */
36
+ export declare const normalizePublishedAt: (value: string | undefined) => string | undefined;
37
+ /**
38
+ * Hold a backend's answer to the contract: absolute permitted URLs, plain
39
+ * snippets, real instants, the `site` and `freshness` filters, and `count`
40
+ * as a maximum.
41
+ *
42
+ * An undated result does not survive a `freshness` filter. Backends omit
43
+ * dates for some hits routinely, so leaving this to each of them lets two
44
+ * conforming adapters return different sets for identical calls — the
45
+ * interchangeability criterion failing on the most ordinary input there is.
46
+ * Exclusion is the right side to land on: the agent asked for recent
47
+ * results and will say the answer is recent, and "we could not determine a
48
+ * date" is not evidence that it is. The cost is real — a good undated page
49
+ * is dropped — which is why the exclusion applies *only* when `freshness`
50
+ * is set, and a call that does not ask about time sees every result.
51
+ */
52
+ export declare const normalizeSearchResults: (raw: readonly SearchResult[], options: SearchOptions, policy?: EgressPolicy) => SearchResult[];
53
+ //# sourceMappingURL=results.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../src/results.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AA+EjE,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,MAWlC,CAAC;AAaZ;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SA8BzE,CAAC;AAmBF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,GACjC,KAAK,SAAS,YAAY,EAAE,EAC5B,SAAS,aAAa,EACtB,SAAQ,YAAiB,KACxB,YAAY,EAkCd,CAAC"}
@@ -0,0 +1,224 @@
1
+ /**
2
+ * What an agent actually receives, whichever backend answered.
3
+ *
4
+ * Normalized results are part of the contract, not an implementation
5
+ * detail, so the enforcement is here rather than trusted to each adapter.
6
+ * That is deliberately belt and braces: a backend passes `count`, `site`,
7
+ * and `freshness` upstream because a provider filtering natively ranks
8
+ * better than a filter applied afterwards — and this package applies them
9
+ * again because "two backends answer the same call the same way" cannot
10
+ * rest on every ecosystem author having got it right.
11
+ */
12
+ import { assertRequestAllowed } from '@stratusagent/egress';
13
+ import { hostMatchesSite } from "./options.js";
14
+ /**
15
+ * A snippet as plain text.
16
+ *
17
+ * Vendors decorate their snippets with markup — Brave wraps matched terms
18
+ * in `<strong>` unless told not to — and a model reading `<strong>` learns
19
+ * nothing except that there was markup. Tags out, whitespace collapsed to
20
+ * one line.
21
+ *
22
+ * The pattern requires a letter or a slash after the `<`, which is what
23
+ * keeps `5 < 10 and 20 > 15` intact: a blanket `<[^>]*>` reads everything
24
+ * between a less-than and the next greater-than as a tag and deletes the
25
+ * sentence between them. Snippets are prose about arbitrary subjects, so
26
+ * that case is not hypothetical.
27
+ *
28
+ * **A formatting tag is removed, not replaced with a space.** Vendors
29
+ * highlight the matched *substring*, so `un<strong>expected</strong>`
30
+ * arrives routinely — and a space there produces `un expected`, a word the
31
+ * page does not contain. The same space detaches punctuation, turning
32
+ * `<strong>kettle</strong>.` into `kettle .`. Anything that is not inline
33
+ * formatting becomes a space, because removing a cell or list boundary
34
+ * glues two values into one instead — see `INLINE_ELEMENTS`.
35
+ *
36
+ * Entity *decoding* is deliberately not done here: a snippet arrives as a
37
+ * string a backend parsed out of its vendor's JSON, so whatever encoding
38
+ * that payload used is the backend's to undo, and this package guessing at
39
+ * it would corrupt a snippet that legitimately contains an ampersand
40
+ * followed by a word.
41
+ */
42
+ /**
43
+ * Formatting that wraps text which was already adjacent. Everything not
44
+ * named here becomes a space instead, which is the safe direction: naming
45
+ * the *separators* looks equivalent and is not, because that list is never
46
+ * complete — a custom element is missing from every list anybody will
47
+ * write, and deleting one glues `AlphaBeta` out of two cells. Inline
48
+ * formatting is a closed set, so it is the half that can be finished.
49
+ *
50
+ * The name is captured **whole**, punctuation included, before it is looked
51
+ * up. Stopping at the first character outside `[a-zA-Z0-9-]` reads
52
+ * `<a:widget>` as the inline `a` and deletes it — and a namespaced or
53
+ * framework-generated element is precisely the unknown tag the separator
54
+ * default exists for, so misreading one defeats the rule at the only point
55
+ * where it matters.
56
+ *
57
+ * `@stratusagent/tool-web` keeps the same set for the same reason. They are
58
+ * not shared because no dependency direction exists between a contract
59
+ * package and a plugin: `search` must not depend on `tool-web`, and the
60
+ * reverse would be backwards.
61
+ */
62
+ const INLINE_ELEMENTS = new Set([
63
+ 'a', 'abbr', 'b', 'bdi', 'bdo', 'big', 'cite', 'code', 'data', 'del', 'dfn',
64
+ 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'ruby',
65
+ 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time',
66
+ 'tt', 'u', 'var', 'wbr',
67
+ ]);
68
+ // `rt` and `rp` are deliberately absent, though they are inline elements.
69
+ // They hold a ruby annotation — a pronunciation printed *above* the base
70
+ // text, not beside it — so joining them produces a token the page never
71
+ // shows: `<ruby>東京<rt>とうきょう</rt></ruby>` reads back as 東京とうきょう,
72
+ // fusing a word with its own furigana. Separating loses nothing and fuses
73
+ // nothing, which is the safer of the two ways to be wrong here; dropping
74
+ // the annotation outright would read better still and would be a guess
75
+ // about whether the pronunciation is content.
76
+ //
77
+ // `sub` and `sup` stay, and are the deliberate contrast: `H<sub>2</sub>O`
78
+ // is one token and joining it is the whole point.
79
+ /**
80
+ * `q` is the one element that renders a character of its own: a browser
81
+ * draws quotation marks around it from the stylesheet, so neither answer
82
+ * above is right. Removing it fuses `<q>yes</q><q>no</q>` into `yesno`,
83
+ * and spacing it detaches the comma in `<q>yes</q>, then left` — the very
84
+ * defect this file exists to fix. Emitting the mark the reader sees costs
85
+ * nothing and is the more faithful extraction of the two.
86
+ */
87
+ const QUOTE_ELEMENT = 'q';
88
+ export const plainSnippet = (value) => value
89
+ .replace(/<\/?([a-zA-Z][^\s/>]*)[^>]*>/g, (_tag, rawName) => {
90
+ const name = rawName.toLowerCase();
91
+ if (name === QUOTE_ELEMENT)
92
+ return '"';
93
+ return INLINE_ELEMENTS.has(name) ? '' : ' ';
94
+ })
95
+ .replace(/\s+/g, ' ')
96
+ .trim();
97
+ /**
98
+ * A date-time this contract will accept: a calendar date, optionally with a
99
+ * time that carries an explicit zone.
100
+ *
101
+ * The zone is mandatory once there is a time, because a zone-less
102
+ * `2026-09-01T10:00:00` is *local* time to whatever machine parses it — the
103
+ * same string becomes two different instants on two daemons, which is the
104
+ * disagreement this package exists to remove.
105
+ */
106
+ const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:?\d{2}))?$/;
107
+ /**
108
+ * An instant, or nothing.
109
+ *
110
+ * `publishedAt` is an ISO 8601 instant in UTC when the backend supplies one
111
+ * and absent when it does not — never a guess, never a locale-formatted
112
+ * date. Under `freshness` that means the result is dropped, which is why
113
+ * this has to be strict rather than merely tolerant: **`new Date(value)` is
114
+ * not a validator.** It reads `03/04/2026` as 4 March without ever saying
115
+ * whether the backend meant March or April, and it silently *corrects*
116
+ * `2026-02-30` into 2 March — so a lenient parse does not fail to date a
117
+ * result, it invents a date, and then a freshness filter keeps or drops the
118
+ * result on the strength of it. The shape is checked first, and the
119
+ * calendar date is round-tripped so a day that does not exist is refused
120
+ * instead of rolled forward.
121
+ *
122
+ * A date with no time is accepted and read as UTC midnight, which is what
123
+ * ISO 8601 and JavaScript both say it is. Plenty of backends only have a
124
+ * day, and refusing them would silently empty every `freshness` search
125
+ * against such a backend; reading it as midnight errs towards calling a
126
+ * page slightly older than it is, which is the safe direction here.
127
+ */
128
+ export const normalizePublishedAt = (value) => {
129
+ if (value === undefined) {
130
+ return undefined;
131
+ }
132
+ const trimmed = value.trim();
133
+ const match = ISO_DATE_TIME.exec(trimmed);
134
+ if (!match) {
135
+ return undefined;
136
+ }
137
+ const [, year = '', month = '', day = '', hour = '0', minute = '0', second = '0', zone = 'Z'] = match;
138
+ // Round-tripped rather than trusted: `Date.UTC(2026, 1, 30)` answers
139
+ // 2 March quite happily, and a date the backend did not give is worse
140
+ // than no date at all.
141
+ const probe = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
142
+ if (probe.getUTCFullYear() !== Number(year)
143
+ || probe.getUTCMonth() !== Number(month) - 1
144
+ || probe.getUTCDate() !== Number(day)
145
+ || Number(hour) > 23
146
+ || Number(minute) > 59
147
+ || Number(second) > 60) {
148
+ return undefined;
149
+ }
150
+ // `+0200` and `+02:00` are the same offset in ISO 8601, and only the
151
+ // second is in JavaScript's own grammar. Spelling it the way the parser
152
+ // requires is not a guess about what the backend meant.
153
+ const parsed = new Date(trimmed.replace(/([+-]\d{2})(\d{2})$/, '$1:$2'));
154
+ const time = parsed.getTime();
155
+ return Number.isFinite(time) ? parsed.toISOString() : undefined;
156
+ };
157
+ /**
158
+ * A result's URL, or nothing when an agent must not be handed it.
159
+ *
160
+ * The same address policy `web.fetch` uses, applied before the URL reaches
161
+ * a model that is about to fetch it: a ranker that returned an intranet
162
+ * host, a `javascript:` URL, or the metadata endpoint has returned one bad
163
+ * row, not a reason to fail a search — so the row is dropped and the rest
164
+ * of the page stands.
165
+ */
166
+ const permittedUrl = (raw, policy) => {
167
+ try {
168
+ return assertRequestAllowed(raw, policy).href;
169
+ }
170
+ catch {
171
+ return undefined;
172
+ }
173
+ };
174
+ /**
175
+ * Hold a backend's answer to the contract: absolute permitted URLs, plain
176
+ * snippets, real instants, the `site` and `freshness` filters, and `count`
177
+ * as a maximum.
178
+ *
179
+ * An undated result does not survive a `freshness` filter. Backends omit
180
+ * dates for some hits routinely, so leaving this to each of them lets two
181
+ * conforming adapters return different sets for identical calls — the
182
+ * interchangeability criterion failing on the most ordinary input there is.
183
+ * Exclusion is the right side to land on: the agent asked for recent
184
+ * results and will say the answer is recent, and "we could not determine a
185
+ * date" is not evidence that it is. The cost is real — a good undated page
186
+ * is dropped — which is why the exclusion applies *only* when `freshness`
187
+ * is set, and a call that does not ask about time sees every result.
188
+ */
189
+ export const normalizeSearchResults = (raw, options, policy = {}) => {
190
+ const kept = [];
191
+ // Compared as instants rather than as strings: ISO text sorts
192
+ // chronologically only for four-digit years, and a filter that is right
193
+ // "almost always" is the kind of difference between two backends this
194
+ // package exists to remove.
195
+ const since = options.freshness === undefined ? undefined : Date.parse(options.freshness.since);
196
+ for (const entry of raw) {
197
+ if (kept.length >= options.count) {
198
+ break;
199
+ }
200
+ if (typeof entry?.title !== 'string' || typeof entry.url !== 'string') {
201
+ continue;
202
+ }
203
+ const url = permittedUrl(entry.url, policy);
204
+ if (url === undefined) {
205
+ continue;
206
+ }
207
+ if (options.site !== undefined && !hostMatchesSite(new URL(url).hostname, options.site)) {
208
+ continue;
209
+ }
210
+ const publishedAt = normalizePublishedAt(typeof entry.publishedAt === 'string' ? entry.publishedAt : undefined);
211
+ if (since !== undefined && (publishedAt === undefined || Date.parse(publishedAt) < since)) {
212
+ continue;
213
+ }
214
+ const snippet = typeof entry.snippet === 'string' ? plainSnippet(entry.snippet) : '';
215
+ kept.push({
216
+ title: entry.title.trim(),
217
+ url,
218
+ ...(snippet.length > 0 ? { snippet } : {}),
219
+ ...(publishedAt !== undefined ? { publishedAt } : {}),
220
+ });
221
+ }
222
+ return kept;
223
+ };
224
+ //# sourceMappingURL=results.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"results.js","sourceRoot":"","sources":["../src/results.ts"],"names":[],"mappings":"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"}
package/dist/tool.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The `web.search` tool: one definition, whichever backend is installed.
3
+ *
4
+ * A backend supplies `search(query, options, context)` and nothing else.
5
+ * The name, the input schema, the option meanings, the result envelope, the
6
+ * credential seam, and the address policy are all here — which is what
7
+ * makes swapping vendors change no soul, no skill, and no result shape.
8
+ */
9
+ import { type CredentialResolver, type JsonObject, type Tool } from '@stratusagent/core';
10
+ import { type SearchProvider } from './contract.ts';
11
+ /**
12
+ * What an agent may spend in a day before someone has to say so.
13
+ *
14
+ * A query acts on nothing, which is why `web.search` is `safe` — but a
15
+ * metered API costs money, and an agent in a loop can spend it unattended.
16
+ * That is a budget control rather than a permission, so it is a cap here
17
+ * and not an approval prompt on every search.
18
+ */
19
+ export declare const DEFAULT_MAX_SEARCHES_PER_DAY = 200;
20
+ /**
21
+ * Said once, in the envelope, rather than left for each backend to word.
22
+ *
23
+ * Snippets are attacker-controlled text: written by whoever owns the page,
24
+ * selected by a third-party ranker, and handed to a model that is about to
25
+ * decide what to do next. This does not solve prompt injection and does not
26
+ * claim to — it marks the boundary, because doing so is nearly free now and
27
+ * a retrofit across every tool that returns third-party text is not.
28
+ */
29
+ export declare const UNTRUSTED_RESULT_NOTE: string;
30
+ /** What the daemon records about a search. Deliberately not the query. */
31
+ export interface SearchLogRecord {
32
+ agentId: string;
33
+ /** Which backend answered, so an operator can tell where the bill came from. */
34
+ provider: string;
35
+ results: number;
36
+ }
37
+ export interface WebSearchToolOptions {
38
+ provider: SearchProvider;
39
+ /**
40
+ * The plugin's own config block, resolved per agent on every call. A
41
+ * plugin whose settings are an access boundary must not close over one
42
+ * agent's answer at setup, and the address policy is exactly that.
43
+ */
44
+ config?: JsonObject;
45
+ /**
46
+ * How the calling agent's `search.apiKey` is found. A host that omits it
47
+ * gives up per-agent credentials entirely: every call reports the key as
48
+ * missing, naming what to add, rather than quietly searching with
49
+ * somebody else's.
50
+ */
51
+ credentials?: CredentialResolver;
52
+ /**
53
+ * The daemon's structured log. It records that a search ran and against
54
+ * which provider — **never the query**, which is user content, in a log
55
+ * that is a trace rather than a second transcript.
56
+ */
57
+ onSearch?: (record: SearchLogRecord) => void;
58
+ /** The clock, so a test can hold one still. */
59
+ now?: () => Date;
60
+ }
61
+ export declare const createWebSearchTool: (options: WebSearchToolOptions) => Tool;
62
+ //# sourceMappingURL=tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,UAAU,EAGf,KAAK,IAAI,EACV,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAI1E;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,QAE2D,CAAC;AAE9F,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AA0DD,eAAO,MAAM,mBAAmB,GAAI,SAAS,oBAAoB,KAAG,IAiFnE,CAAC"}