deepseek-web-retrieval 0.3.0 → 0.5.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +96 -230
  2. package/README.md +53 -25
  3. package/dist/cli.js +1358 -617
  4. package/docs/agents/codex.md +32 -11
  5. package/docs/agents/opencode-v2.md +5 -5
  6. package/docs/agents/pi.md +92 -90
  7. package/docs/design/installer-design.md +72 -9
  8. package/docs/hosts.jsonc +2 -2
  9. package/docs/integration.md +14 -9
  10. package/docs/research/control-matrix.md +48 -13
  11. package/docs/research/mcp-compatibility.md +9 -4
  12. package/docs/research/search-api-defaults.md +309 -0
  13. package/examples/pi.settings.jsonc +17 -0
  14. package/index.ts +2 -2
  15. package/package.json +35 -4
  16. package/src/core/auth.ts +4 -6
  17. package/src/core/client.ts +65 -53
  18. package/src/core/constants.ts +30 -24
  19. package/src/core/descriptions.ts +30 -6
  20. package/src/core/domains.ts +56 -67
  21. package/src/core/env.ts +112 -0
  22. package/src/core/format.ts +53 -50
  23. package/src/core/index.ts +9 -9
  24. package/src/core/jsonc.ts +6 -6
  25. package/src/core/object.ts +4 -0
  26. package/src/core/options.ts +21 -26
  27. package/src/core/search.ts +45 -44
  28. package/src/core/transport.ts +109 -113
  29. package/src/core/types.ts +47 -47
  30. package/src/core/url.ts +20 -20
  31. package/src/installer/apply.ts +256 -81
  32. package/src/installer/cli.ts +135 -104
  33. package/src/installer/detect.ts +46 -31
  34. package/src/installer/formats/index.ts +26 -28
  35. package/src/installer/formats/json.ts +81 -77
  36. package/src/installer/formats/rules.ts +55 -28
  37. package/src/installer/formats/toml.ts +88 -81
  38. package/src/installer/formats/value.ts +34 -30
  39. package/src/installer/hosts/codex.ts +100 -54
  40. package/src/installer/hosts/index.ts +126 -41
  41. package/src/installer/hosts/opencode.ts +70 -51
  42. package/src/installer/hosts/pi.ts +269 -75
  43. package/src/installer/hosts/shared.ts +19 -14
  44. package/src/installer/operation-lock.ts +54 -0
  45. package/src/installer/ownership.ts +129 -0
  46. package/src/installer/paths.ts +7 -5
  47. package/src/installer/persistence.ts +66 -0
  48. package/src/installer/render.ts +29 -29
  49. package/src/installer/revert.ts +33 -29
  50. package/src/installer/rulesContent.ts +24 -13
  51. package/src/installer/state.ts +116 -31
  52. package/src/installer/types.ts +74 -56
  53. package/src/installer/uninstall.ts +40 -17
  54. package/src/mcp/auth.ts +7 -8
  55. package/src/mcp/cli.ts +38 -20
  56. package/src/mcp/client.ts +35 -0
  57. package/src/mcp/server.ts +68 -97
  58. package/src/opencode/auth.ts +24 -26
  59. package/src/opencode/plugin.ts +99 -87
  60. package/src/pi/extension.ts +164 -0
  61. package/src/pi/settings.ts +49 -0
  62. package/src/mcp/config.ts +0 -124
@@ -1,18 +1,11 @@
1
- import {
2
- FETCH_TRUNCATION_MARKER,
3
- MAX_ERROR_BODY_CHARS,
4
- } from "./constants"
5
- import type { FormattedFetchContent, SearchResult } from "./types"
1
+ import { FETCH_TRUNCATION_MARKER, MAX_ERROR_BODY_CHARS } from "./constants";
2
+ import type { FormattedFetchContent, SearchResult } from "./types";
6
3
 
7
4
  /** Truncates by Unicode code points so emoji and surrogate pairs are not split. */
8
5
  export function truncate(value: string, maxChars: number): string {
9
- if (maxChars <= 0) return ""
10
- if (value.length <= maxChars) return value
11
- return Array.from(value).slice(0, maxChars).join("")
12
- }
13
-
14
- export function formatFetchContent(title: string, content: string, maxChars: number): string {
15
- return truncate(withTitle(title, content), maxChars)
6
+ if (maxChars <= 0) return "";
7
+ if (value.length <= maxChars) return value;
8
+ return Array.from(value).slice(0, maxChars).join("");
16
9
  }
17
10
 
18
11
  /**
@@ -20,40 +13,36 @@ export function formatFetchContent(title: string, content: string, maxChars: num
20
13
  * truncation marker counts toward `maxChars`, so the returned content never
21
14
  * exceeds the configured budget.
22
15
  */
23
- export function formatFetchedContent(
24
- title: string,
25
- content: string,
26
- maxChars: number,
27
- ): FormattedFetchContent {
28
- const document = withTitle(title, content)
29
- const length = Array.from(document).length
30
- if (maxChars <= 0) return { content: "", truncated: length > 0 }
31
- if (length <= maxChars) return { content: document, truncated: false }
16
+ export function formatFetchedContent(title: string, content: string, maxChars: number): FormattedFetchContent {
17
+ const document = withTitle(title, content);
18
+ const length = Array.from(document).length;
19
+ if (maxChars <= 0) return { content: "", truncated: length > 0 };
20
+ if (length <= maxChars) return { content: document, truncated: false };
32
21
 
33
- const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length
22
+ const markerLength = Array.from(FETCH_TRUNCATION_MARKER).length;
34
23
  if (maxChars <= markerLength) {
35
- return { content: truncate(document, maxChars), truncated: true }
24
+ return { content: truncate(document, maxChars), truncated: true };
36
25
  }
37
26
  return {
38
27
  content: `${truncate(document, maxChars - markerLength)}${FETCH_TRUNCATION_MARKER}`,
39
28
  truncated: true,
40
- }
29
+ };
41
30
  }
42
31
 
43
32
  function withTitle(title: string, content: string): string {
44
- const heading = title.trim()
45
- if (!heading) return content
33
+ const heading = title.trim();
34
+ if (!heading) return content;
46
35
 
47
- const body = content.trimStart()
48
- const lines = body.split(/\r?\n/)
49
- const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1]
36
+ const body = content.trimStart();
37
+ const lines = body.split(/\r?\n/);
38
+ const firstHeading = lines[0]?.match(/^\\?#\s+(.+?)\s*$/)?.[1];
50
39
  if (firstHeading && normalizeHeading(firstHeading) === normalizeHeading(heading)) {
51
40
  // DeepSeek sometimes escapes the leading "#" as "\#"; normalize it so the
52
41
  // result is a real Markdown heading instead of a duplicated literal line.
53
- lines[0] = `# ${heading}`
54
- return lines.join("\n")
42
+ lines[0] = `# ${heading}`;
43
+ return lines.join("\n");
55
44
  }
56
- return `# ${heading}\n\n${content}`
45
+ return `# ${heading}\n\n${content}`;
57
46
  }
58
47
 
59
48
  function normalizeHeading(value: string): string {
@@ -61,23 +50,23 @@ function normalizeHeading(value: string): string {
61
50
  .trim()
62
51
  .replace(/\s+/g, " ")
63
52
  .replace(/\s+#+\s*$/, "")
64
- .toLowerCase()
53
+ .toLowerCase();
65
54
  }
66
55
 
67
56
  export function redactSecrets(value: string): string {
68
57
  return value
69
58
  .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-***")
70
- .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***")
59
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer ***");
71
60
  }
72
61
 
73
62
  export function formatApiError(operation: string, status: number, body: string): string {
74
- const detail = redactSecrets(body.trim())
75
- const detailChars = Array.from(detail)
63
+ const detail = redactSecrets(body.trim());
64
+ const detailChars = Array.from(detail);
76
65
  const text = detail
77
66
  ? detailChars.length > MAX_ERROR_BODY_CHARS
78
67
  ? `${detailChars.slice(0, MAX_ERROR_BODY_CHARS).join("")}…`
79
68
  : detail
80
- : "empty response body"
69
+ : "empty response body";
81
70
  const hint =
82
71
  status === 401
83
72
  ? "invalid API key"
@@ -87,13 +76,13 @@ export function formatApiError(operation: string, status: number, body: string):
87
76
  ? "permission denied"
88
77
  : status === 429
89
78
  ? "rate limited"
90
- : undefined
91
- const label = hint ? `${operation} failed (HTTP ${status}, ${hint})` : `${operation} failed (HTTP ${status})`
92
- return `${label}: ${text}`
79
+ : undefined;
80
+ const label = hint ? `${operation} failed (HTTP ${status}, ${hint})` : `${operation} failed (HTTP ${status})`;
81
+ return `${label}: ${text}`;
93
82
  }
94
83
 
95
84
  export function formatTimeoutError(operation: string, timeoutMs: number): string {
96
- return `${operation} timed out after ${timeoutMs} ms`
85
+ return `${operation} timed out after ${timeoutMs} ms`;
97
86
  }
98
87
 
99
88
  /**
@@ -101,27 +90,41 @@ export function formatTimeoutError(operation: string, timeoutMs: number): string
101
90
  * Hosts that ignore structured output read this text.
102
91
  */
103
92
  export function formatSearchResults(results: readonly SearchResult[]): string {
104
- if (results.length === 0) return "No results found."
93
+ if (results.length === 0) return "No results found.";
105
94
 
106
95
  return results
107
96
  .map((result, index) => {
108
- const title = result.title?.trim() || result.url
109
- const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`]
97
+ const title = result.title?.trim() || result.url;
98
+ const lines = [`${index + 1}. [${escapeBrackets(title)}](${result.url})`];
110
99
  if (result.content) {
111
- lines.push(` ${result.content.replaceAll("\n", " ")}`)
100
+ lines.push(` ${result.content.replaceAll("\n", " ")}`);
112
101
  }
113
102
  if (result.published !== undefined) {
114
- lines.push(` Published: ${formatPublished(result.published)}`)
103
+ lines.push(` Published: ${formatPublished(result.published)}`);
115
104
  }
116
- return lines.join("\n")
105
+ return lines.join("\n");
117
106
  })
118
- .join("\n\n")
107
+ .join("\n\n");
108
+ }
109
+
110
+ /**
111
+ * Renders one search result as the structured mirror of its text form; hosts
112
+ * that read structuredContent (every JSON-replacing client excepted) get this
113
+ * shape from both adapters.
114
+ */
115
+ export function toStructuredResult(result: SearchResult) {
116
+ return {
117
+ url: result.url,
118
+ ...(result.title ? { title: result.title } : {}),
119
+ ...(result.content ? { content: result.content } : {}),
120
+ ...(result.published !== undefined ? { published: result.published } : {}),
121
+ };
119
122
  }
120
123
 
121
124
  function escapeBrackets(value: string): string {
122
- return value.replaceAll("[", "\\[").replaceAll("]", "\\]")
125
+ return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
123
126
  }
124
127
 
125
128
  function formatPublished(epochMs: number): string {
126
- return new Date(epochMs).toISOString().slice(0, 10)
129
+ return new Date(epochMs).toISOString().slice(0, 10);
127
130
  }
package/src/core/index.ts CHANGED
@@ -1,9 +1,9 @@
1
- export * from "./auth"
2
- export * from "./client"
3
- export * from "./constants"
4
- export * from "./domains"
5
- export * from "./format"
6
- export * from "./options"
7
- export * from "./search"
8
- export * from "./types"
9
- export * from "./url"
1
+ export * from "./auth";
2
+ export * from "./client";
3
+ export * from "./constants";
4
+ export * from "./domains";
5
+ export * from "./format";
6
+ export * from "./options";
7
+ export * from "./search";
8
+ export * from "./types";
9
+ export * from "./url";
package/src/core/jsonc.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { parse } from "jsonc-parser/lib/esm/main.js"
1
+ import { parse } from "jsonc-parser/lib/esm/main.js";
2
2
 
3
3
  /**
4
4
  * Parses JSONC (JSON with comments and trailing commas) via jsonc-parser,
@@ -6,11 +6,11 @@ import { parse } from "jsonc-parser/lib/esm/main.js"
6
6
  * text has errors beyond tolerated comments and trailing commas.
7
7
  */
8
8
  export function parseJsonc(text: string): unknown {
9
- const errors: { error: number; offset: number; length: number }[] = []
10
- const value = parse(text, errors, { allowTrailingComma: true })
9
+ const errors: { error: number; offset: number; length: number }[] = [];
10
+ const value = parse(text, errors, { allowTrailingComma: true });
11
11
  if (errors.length > 0) {
12
- const first = errors[0]
13
- throw new SyntaxError(`JSONC parse error at offset ${first.offset} (code ${first.error})`)
12
+ const first = errors[0];
13
+ throw new SyntaxError(`JSONC parse error at offset ${first.offset} (code ${first.error})`);
14
14
  }
15
- return value
15
+ return value;
16
16
  }
@@ -0,0 +1,4 @@
1
+ /** Shared plain-object guard: core modules and the installer's value helpers agree on one. */
2
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
@@ -2,8 +2,8 @@ import {
2
2
  DEFAULT_API_KEY_SOURCE,
3
3
  DEFAULT_FETCH_MAX_CHARS,
4
4
  DEFAULT_MAX_RESULTS,
5
- DEFAULT_MAX_SUMMARY_CHARS,
6
5
  DEFAULT_MAX_RETRIES,
6
+ DEFAULT_MAX_SUMMARY_CHARS,
7
7
  DEFAULT_MIN_AUTHORITY,
8
8
  DEFAULT_MIN_RERANK,
9
9
  DEFAULT_RETRY_BASE_DELAY_MS,
@@ -13,8 +13,8 @@ import {
13
13
  MAX_RETRY_DELAY_MS,
14
14
  MAX_TIMEOUT_MS,
15
15
  MIN_TIMEOUT_MS,
16
- } from "./constants"
17
- import { normalizeDomains } from "./domains"
16
+ } from "./constants";
17
+ import { normalizeDomains } from "./domains";
18
18
  import type {
19
19
  ApiKeySource,
20
20
  AuthOptions,
@@ -23,7 +23,7 @@ import type {
23
23
  ResolvedOptions,
24
24
  RetryOptions,
25
25
  SearchOptions,
26
- } from "./types"
26
+ } from "./types";
27
27
 
28
28
  export function resolvePluginOptions(input?: PluginOptions): ResolvedOptions {
29
29
  return {
@@ -31,7 +31,7 @@ export function resolvePluginOptions(input?: PluginOptions): ResolvedOptions {
31
31
  fetch: resolveFetchOptions(input),
32
32
  auth: resolveAuthOptions(input),
33
33
  retry: resolveRetryOptions(input),
34
- }
34
+ };
35
35
  }
36
36
 
37
37
  export function resolveSearchOptions(input?: PluginOptions): SearchOptions {
@@ -40,14 +40,14 @@ export function resolveSearchOptions(input?: PluginOptions): SearchOptions {
40
40
  maxResults: clampInt(input?.maxResults, DEFAULT_MAX_RESULTS, 0, 50),
41
41
  minRerank: finiteNumber(input?.minRerank, DEFAULT_MIN_RERANK),
42
42
  minAuthority: finiteNumber(input?.minAuthority, DEFAULT_MIN_AUTHORITY),
43
- allowUnknownAuthority: input?.allowUnknownAuthority === true,
43
+ allowUnknownAuthority: input?.allowUnknownAuthority !== false,
44
44
  blockedDomains: normalizeDomains(input?.blockedDomains),
45
45
  includeDomains: normalizeDomains(input?.includeDomains),
46
46
  language: optionalText(input?.language),
47
47
  maxSummaryChars: clampInt(input?.maxSummaryChars, DEFAULT_MAX_SUMMARY_CHARS, 0, 1_000_000),
48
48
  timeoutMs: clampInt(input?.searchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
49
49
  enabled: input?.searchEnabled !== false,
50
- }
50
+ };
51
51
  }
52
52
 
53
53
  export function resolveFetchOptions(input?: PluginOptions): FetchOptions {
@@ -58,26 +58,21 @@ export function resolveFetchOptions(input?: PluginOptions): FetchOptions {
58
58
  timeoutMs: clampInt(input?.fetchTimeoutMs, DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS),
59
59
  blockPrivateHosts: input?.fetchBlockPrivateHosts !== false,
60
60
  enabled: input?.fetchEnabled !== false,
61
- }
61
+ };
62
62
  }
63
63
 
64
64
  export function resolveAuthOptions(input?: PluginOptions): AuthOptions {
65
- const value = input?.apiKeySource
65
+ const value = input?.apiKeySource;
66
66
  return {
67
67
  source: value === "opencode" || value === "env" ? value : (DEFAULT_API_KEY_SOURCE as ApiKeySource),
68
- }
68
+ };
69
69
  }
70
70
 
71
71
  export function resolveRetryOptions(input?: PluginOptions): RetryOptions {
72
72
  return {
73
73
  maxRetries: clampInt(input?.maxRetries, DEFAULT_MAX_RETRIES, 0, MAX_RETRIES),
74
- baseDelayMs: clampInt(
75
- input?.retryBaseDelayMs,
76
- DEFAULT_RETRY_BASE_DELAY_MS,
77
- 0,
78
- MAX_RETRY_DELAY_MS,
79
- ),
80
- }
74
+ baseDelayMs: clampInt(input?.retryBaseDelayMs, DEFAULT_RETRY_BASE_DELAY_MS, 0, MAX_RETRY_DELAY_MS),
75
+ };
81
76
  }
82
77
 
83
78
  /**
@@ -86,23 +81,23 @@ export function resolveRetryOptions(input?: PluginOptions): RetryOptions {
86
81
  */
87
82
  export function resolveFetchTimeoutMs(configuredMs: number, inputSeconds?: unknown): number {
88
83
  if (typeof inputSeconds !== "number" || !Number.isFinite(inputSeconds) || inputSeconds <= 0) {
89
- return configuredMs
84
+ return configuredMs;
90
85
  }
91
- const inputMs = Math.min(MAX_TIMEOUT_MS, Math.max(1, Math.round(inputSeconds * 1000)))
92
- return Math.min(configuredMs, inputMs)
86
+ const inputMs = Math.min(MAX_TIMEOUT_MS, Math.max(1, Math.round(inputSeconds * 1000)));
87
+ return Math.min(configuredMs, inputMs);
93
88
  }
94
89
 
95
90
  function optionalText(value: unknown): string | undefined {
96
- if (typeof value !== "string") return undefined
97
- const trimmed = value.trim()
98
- return trimmed ? trimmed : undefined
91
+ if (typeof value !== "string") return undefined;
92
+ const trimmed = value.trim();
93
+ return trimmed ? trimmed : undefined;
99
94
  }
100
95
 
101
96
  function finiteNumber(value: unknown, fallback: number): number {
102
- return typeof value === "number" && Number.isFinite(value) ? value : fallback
97
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
103
98
  }
104
99
 
105
100
  function clampInt(value: unknown, fallback: number, min: number, max: number): number {
106
- const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback
107
- return Math.min(max, Math.max(min, number))
101
+ const number = typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : fallback;
102
+ return Math.min(max, Math.max(min, number));
108
103
  }
@@ -1,7 +1,7 @@
1
- import { normalizeDomains } from "./domains"
2
- import { truncate } from "./format"
3
- import type { DeepSeekSearchResult, SearchOptions, SearchResult } from "./types"
4
- import { normalizeResultUrl } from "./url"
1
+ import { normalizeDomains } from "./domains";
2
+ import { truncate } from "./format";
3
+ import type { DeepSeekSearchResult, SearchOptions, SearchResult } from "./types";
4
+ import { normalizeResultUrl } from "./url";
5
5
 
6
6
  /**
7
7
  * Appends DeepSeek search operators:
@@ -13,72 +13,73 @@ export function buildQuery(
13
13
  query: string,
14
14
  options: Pick<SearchOptions, "blockedDomains" | "includeDomains" | "language">,
15
15
  ): string {
16
- const parts = [query.trim()]
17
- for (const domain of normalizeDomains(options.blockedDomains)) parts.push(`-site:${domain}`)
18
- const include = normalizeDomains(options.includeDomains)
19
- if (include.length) parts.push(include.map((domain) => `site:${domain}`).join(" OR "))
20
- const language = options.language?.trim().replace(/^lang:/i, "")
21
- if (language) parts.push(`lang:${language}`)
22
- return parts.filter((part) => part.length > 0).join(" ")
16
+ const parts = [query.trim()];
17
+ for (const domain of normalizeDomains(options.blockedDomains)) parts.push(`-site:${domain}`);
18
+ const include = normalizeDomains(options.includeDomains);
19
+ if (include.length) parts.push(include.map((domain) => `site:${domain}`).join(" OR "));
20
+ const language = options.language?.trim().replace(/^lang:/i, "");
21
+ if (language) parts.push(`lang:${language}`);
22
+ return parts.filter((part) => part.length > 0).join(" ");
23
23
  }
24
24
 
25
25
  export function parsePublished(value: unknown): number | undefined {
26
- if (typeof value !== "string" || !value.trim()) return undefined
27
- const timestamp = Date.parse(value)
28
- return Number.isFinite(timestamp) ? timestamp : undefined
26
+ if (typeof value !== "string" || !value.trim()) return undefined;
27
+ const timestamp = Date.parse(value);
28
+ return Number.isFinite(timestamp) ? timestamp : undefined;
29
29
  }
30
30
 
31
31
  /**
32
32
  * Filters and maps DeepSeek results into the neutral SearchResult shape.
33
- * Missing/non-finite rerank_score never passes. Authority -1/missing is
33
+ * A present rerank_score always compares against minRerank; a missing or
34
+ * non-finite score passes only while no positive threshold is set, so the
35
+ * default returns exactly what DeepSeek ranked. Authority -1/missing is
34
36
  * "unknown" and only passes when allowUnknownAuthority is enabled.
35
37
  */
36
- export function processResults(
37
- raw: readonly DeepSeekSearchResult[],
38
- options: SearchOptions,
39
- ): SearchResult[] {
40
- const results: SearchResult[] = []
41
- const seen = new Set<string>()
38
+ export function processResults(raw: readonly DeepSeekSearchResult[], options: SearchOptions): SearchResult[] {
39
+ const results: SearchResult[] = [];
40
+ const seen = new Set<string>();
42
41
 
43
42
  for (const item of raw) {
44
- if (results.length >= options.maxResults) break
45
- if (!item || typeof item !== "object") continue
43
+ if (results.length >= options.maxResults) break;
44
+ if (!item || typeof item !== "object") continue;
46
45
 
47
- const url = normalizeResultUrl(item.url)
48
- if (!url || seen.has(url)) continue
46
+ const url = normalizeResultUrl(item.url);
47
+ if (!url || seen.has(url)) continue;
49
48
 
50
- const rerank = item.rerank_score
51
- if (typeof rerank !== "number" || !Number.isFinite(rerank) || rerank < options.minRerank) continue
49
+ const rerank = item.rerank_score;
50
+ if (typeof rerank === "number" && Number.isFinite(rerank)) {
51
+ if (rerank < options.minRerank) continue;
52
+ } else if (options.minRerank > 0) {
53
+ // A threshold was explicitly raised, so a result that cannot be scored
54
+ // against it must not slip through.
55
+ continue;
56
+ }
52
57
 
53
- const authority = item.authority
54
- const knownAuthority =
55
- typeof authority === "number" && Number.isFinite(authority) && authority >= 0
58
+ const authority = item.authority;
59
+ const knownAuthority = typeof authority === "number" && Number.isFinite(authority) && authority >= 0;
56
60
  if (knownAuthority) {
57
- if (authority < options.minAuthority) continue
61
+ if (authority < options.minAuthority) continue;
58
62
  } else if (!options.allowUnknownAuthority) {
59
- continue
63
+ continue;
60
64
  }
61
65
 
62
- seen.add(url)
63
- const content = truncate(
64
- optionalText(item.summary) ?? optionalText(item.snippet) ?? "",
65
- options.maxSummaryChars,
66
- )
67
- const published = parsePublished(item.date)
68
- const title = optionalText(item.title)
66
+ seen.add(url);
67
+ const content = truncate(optionalText(item.summary) ?? optionalText(item.snippet) ?? "", options.maxSummaryChars);
68
+ const published = parsePublished(item.date);
69
+ const title = optionalText(item.title);
69
70
  results.push({
70
71
  url,
71
72
  ...(title ? { title } : {}),
72
73
  ...(content ? { content } : {}),
73
74
  ...(published !== undefined ? { published } : {}),
74
- })
75
+ });
75
76
  }
76
77
 
77
- return results
78
+ return results;
78
79
  }
79
80
 
80
81
  function optionalText(value: unknown): string | undefined {
81
- if (typeof value !== "string") return undefined
82
- const trimmed = value.trim()
83
- return trimmed ? trimmed : undefined
82
+ if (typeof value !== "string") return undefined;
83
+ const trimmed = value.trim();
84
+ return trimmed ? trimmed : undefined;
84
85
  }