@taurgis/bonsai 3.2.0 → 4.0.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 (173) hide show
  1. package/README.md +9 -6
  2. package/agents/agents/salesforce-docs-researcher.agent.md +17 -4
  3. package/agents/instructions/salesforce-research.instructions.md +2 -1
  4. package/bin/cli.mjs +13 -18
  5. package/dist/base-command.d.ts +59 -12
  6. package/dist/base-command.js +179 -51
  7. package/dist/commands/config/base.js +12 -2
  8. package/dist/commands/config/get.js +8 -12
  9. package/dist/commands/config/index.js +2 -2
  10. package/dist/commands/config/list.js +11 -17
  11. package/dist/commands/config/set.d.ts +2 -1
  12. package/dist/commands/config/set.js +35 -28
  13. package/dist/commands/config/unset.d.ts +2 -1
  14. package/dist/commands/config/unset.js +26 -25
  15. package/dist/commands/fetch.d.ts +7 -15
  16. package/dist/commands/fetch.js +61 -401
  17. package/dist/commands/import.d.ts +4 -9
  18. package/dist/commands/import.js +56 -284
  19. package/dist/commands/inspect.d.ts +7 -1
  20. package/dist/commands/inspect.js +112 -52
  21. package/dist/commands/list.d.ts +18 -3
  22. package/dist/commands/list.js +121 -46
  23. package/dist/commands/prune.d.ts +6 -1
  24. package/dist/commands/prune.js +89 -78
  25. package/dist/commands/status.d.ts +3 -2
  26. package/dist/commands/status.js +62 -79
  27. package/dist/hooks/command-not-found/suggest.d.ts +2 -1
  28. package/dist/hooks/command-not-found/suggest.js +78 -41
  29. package/dist/lib/argv.d.ts +29 -3
  30. package/dist/lib/argv.js +147 -54
  31. package/dist/lib/batch.d.ts +41 -0
  32. package/dist/lib/batch.js +39 -0
  33. package/dist/lib/cache-view.d.ts +21 -0
  34. package/dist/lib/cache-view.js +30 -0
  35. package/dist/lib/cli-emit.d.ts +28 -0
  36. package/dist/lib/cli-emit.js +44 -0
  37. package/dist/lib/cli-error-policy.d.ts +57 -0
  38. package/dist/lib/cli-error-policy.js +74 -0
  39. package/dist/lib/cli-flag-manifest.d.ts +12 -0
  40. package/dist/lib/cli-flag-manifest.js +15 -0
  41. package/dist/lib/cli-presentation.d.ts +41 -0
  42. package/dist/lib/cli-presentation.js +53 -0
  43. package/dist/lib/cli-result-types.d.ts +112 -0
  44. package/dist/lib/cli-result-types.js +2 -0
  45. package/dist/lib/config/index.d.ts +23 -5
  46. package/dist/lib/config/index.js +60 -5
  47. package/dist/lib/config/io.d.ts +9 -0
  48. package/dist/lib/config/io.js +71 -21
  49. package/dist/lib/config/schema.d.ts +2 -0
  50. package/dist/lib/config/schema.js +7 -0
  51. package/dist/lib/config-persist.d.ts +28 -0
  52. package/dist/lib/config-persist.js +29 -0
  53. package/dist/lib/envelope.d.ts +41 -3
  54. package/dist/lib/envelope.js +154 -4
  55. package/dist/lib/help-preflight.d.ts +5 -8
  56. package/dist/lib/help-preflight.js +13 -32
  57. package/dist/lib/json-meta.d.ts +0 -2
  58. package/dist/lib/json-meta.js +21 -25
  59. package/dist/lib/limit-flag.d.ts +11 -1
  60. package/dist/lib/limit-flag.js +19 -6
  61. package/dist/lib/parse-error-ux.d.ts +5 -0
  62. package/dist/lib/parse-error-ux.js +73 -0
  63. package/dist/lib/prune-flags.d.ts +30 -0
  64. package/dist/lib/prune-flags.js +86 -0
  65. package/dist/lib/research/artifact-index.d.ts +11 -2
  66. package/dist/lib/research/artifact-index.js +14 -5
  67. package/dist/lib/research/artifact.js +20 -2
  68. package/dist/lib/research/browser.d.ts +53 -2
  69. package/dist/lib/research/browser.js +202 -58
  70. package/dist/lib/research/capture.d.ts +24 -2
  71. package/dist/lib/research/capture.js +24 -4
  72. package/dist/lib/research/cli-io.d.ts +40 -0
  73. package/dist/lib/research/cli-io.js +27 -0
  74. package/dist/lib/research/compress.d.ts +7 -0
  75. package/dist/lib/research/compress.js +7 -3
  76. package/dist/lib/research/docs/capabilities.d.ts +15 -0
  77. package/dist/lib/research/docs/capabilities.js +5 -0
  78. package/dist/lib/research/docs/clean-dom.d.ts +2 -0
  79. package/dist/lib/research/docs/clean-dom.js +2 -0
  80. package/dist/lib/research/docs/code-blocks.d.ts +2 -0
  81. package/dist/lib/research/docs/code-blocks.js +2 -0
  82. package/dist/lib/research/docs/detect.d.ts +4 -0
  83. package/dist/lib/research/docs/detect.js +4 -0
  84. package/dist/lib/research/docs/machine-readable.d.ts +18 -0
  85. package/dist/lib/research/docs/machine-readable.js +10 -0
  86. package/dist/lib/research/docs/markdown-source.d.ts +20 -0
  87. package/dist/lib/research/docs/markdown-source.js +19 -2
  88. package/dist/lib/research/docs/quality-gates.d.ts +5 -0
  89. package/dist/lib/research/docs/quality-gates.js +4 -0
  90. package/dist/lib/research/docs/rsc.d.ts +9 -2
  91. package/dist/lib/research/docs/rsc.js +5 -2
  92. package/dist/lib/research/docs/section-artifacts.d.ts +12 -0
  93. package/dist/lib/research/docs/section-artifacts.js +12 -0
  94. package/dist/lib/research/docs/sections.d.ts +18 -0
  95. package/dist/lib/research/docs/sections.js +17 -1
  96. package/dist/lib/research/docs/source-map.d.ts +28 -0
  97. package/dist/lib/research/docs/source-map.js +27 -0
  98. package/dist/lib/research/docs/validate.d.ts +35 -2
  99. package/dist/lib/research/docs/validate.js +47 -2
  100. package/dist/lib/research/extract.d.ts +16 -0
  101. package/dist/lib/research/extract.js +12 -1
  102. package/dist/lib/research/fetch-command-service.d.ts +36 -0
  103. package/dist/lib/research/fetch-command-service.js +367 -0
  104. package/dist/lib/research/fetch-result.d.ts +114 -0
  105. package/dist/lib/research/fetch-result.js +212 -0
  106. package/dist/lib/research/fetcher.d.ts +46 -8
  107. package/dist/lib/research/fetcher.js +61 -31
  108. package/dist/lib/research/freshness.d.ts +54 -11
  109. package/dist/lib/research/freshness.js +91 -31
  110. package/dist/lib/research/import-command-service.d.ts +56 -0
  111. package/dist/lib/research/import-command-service.js +402 -0
  112. package/dist/lib/research/keywords.js +5 -1
  113. package/dist/lib/research/markdown.d.ts +12 -1
  114. package/dist/lib/research/markdown.js +13 -5
  115. package/dist/lib/research/metadata-filters.d.ts +18 -0
  116. package/dist/lib/research/metadata-filters.js +37 -0
  117. package/dist/lib/research/metadata-flags.d.ts +34 -0
  118. package/dist/lib/research/metadata-flags.js +45 -0
  119. package/dist/lib/research/persist-artifact.d.ts +23 -0
  120. package/dist/lib/research/persist-artifact.js +27 -0
  121. package/dist/lib/research/prompt-injection.d.ts +9 -0
  122. package/dist/lib/research/prompt-injection.js +59 -14
  123. package/dist/lib/research/proxy.d.ts +14 -8
  124. package/dist/lib/research/proxy.js +14 -10
  125. package/dist/lib/research/resolve-target.d.ts +2 -0
  126. package/dist/lib/research/resolve-target.js +1 -1
  127. package/dist/lib/research/revalidate.d.ts +49 -20
  128. package/dist/lib/research/revalidate.js +80 -15
  129. package/dist/lib/research/schema.d.ts +7 -0
  130. package/dist/lib/research/schema.js +2 -0
  131. package/dist/lib/research/secret-scan.d.ts +6 -0
  132. package/dist/lib/research/secret-scan.js +8 -0
  133. package/dist/lib/research/secure-write.d.ts +4 -0
  134. package/dist/lib/research/secure-write.js +31 -2
  135. package/dist/lib/research/storage.d.ts +9 -8
  136. package/dist/lib/research/storage.js +34 -28
  137. package/dist/lib/research/summarize.d.ts +15 -0
  138. package/dist/lib/research/summarize.js +11 -0
  139. package/dist/lib/research/url.d.ts +9 -6
  140. package/dist/lib/research/url.js +33 -21
  141. package/dist/lib/text.d.ts +19 -8
  142. package/dist/lib/text.js +32 -9
  143. package/dist/lib/value-flags.d.ts +14 -0
  144. package/dist/lib/value-flags.js +28 -0
  145. package/dist/sites/coveo.d.ts +10 -0
  146. package/dist/sites/coveo.js +10 -0
  147. package/dist/sites/index.d.ts +16 -0
  148. package/dist/sites/index.js +16 -4
  149. package/dist/sites/markdown-twin.d.ts +31 -0
  150. package/dist/sites/markdown-twin.js +56 -0
  151. package/dist/sites/salesforce/fetch-page.d.ts +8 -2
  152. package/dist/sites/salesforce/fetch-page.js +14 -3
  153. package/dist/sites/salesforce/help-tooling-markdown.d.ts +30 -0
  154. package/dist/sites/salesforce/help-tooling-markdown.js +74 -0
  155. package/dist/sites/salesforce/index.d.ts +1 -0
  156. package/dist/sites/salesforce/index.js +1 -0
  157. package/dist/sites/salesforce-developer/fetch-page.d.ts +6 -1
  158. package/dist/sites/salesforce-developer/fetch-page.js +6 -1
  159. package/dist/sites/salesforce-developer/index.d.ts +1 -0
  160. package/dist/sites/salesforce-developer/index.js +1 -0
  161. package/dist/sites/salesforce-developer/route-markdown.d.ts +18 -6
  162. package/dist/sites/salesforce-developer/route-markdown.js +29 -67
  163. package/dist/sites/salesforce-doc-fetch.d.ts +39 -5
  164. package/dist/sites/salesforce-doc-fetch.js +52 -7
  165. package/dist/sites/salesforce-dom-probe.d.ts +20 -2
  166. package/dist/sites/salesforce-dom-probe.js +20 -2
  167. package/dist/sites/types.d.ts +14 -8
  168. package/dist/sites/types.js +3 -0
  169. package/oclif.manifest.json +216 -102
  170. package/package.json +20 -7
  171. package/agents/agents/official-docs-researcher.agent.md +0 -87
  172. package/agents/instructions/web-research.instructions.md +0 -44
  173. package/agents/skills/web-research/SKILL.md +0 -118
@@ -1,6 +1,5 @@
1
1
  import { fetchText } from '../../lib/research/fetcher.js';
2
- import { validateTextArtifact } from '../../lib/research/docs/validate.js';
3
- import { extractFromSource } from '../../lib/research/docs/markdown-source.js';
2
+ import { probeMarkdownTwin } from '../markdown-twin.js';
4
3
  // developer.salesforce.com now publishes a Markdown twin for supported doc articles (the
5
4
  // "View as Markdown" link): /docs/<cloud>/<book>/guide/<article>[.html] also serves
6
5
  // <article>.md with Content-Type text/markdown. The URL mapping is deterministic, so we derive
@@ -9,10 +8,9 @@ import { extractFromSource } from '../../lib/research/docs/markdown-source.js';
9
8
  //
10
9
  // Coverage is rolling out per doc set, so the probe result is only a candidate (T-19/T-24):
11
10
  // unsupported articles 404, and atlas.* books answer the .md route with an HTTP 200 HTML SPA
12
- // shell — which makes the strict content-type + body validation below load-bearing, not
13
- // defensive garnish.
11
+ // shell — which makes the strict content-type + body validation in probeMarkdownTwin
12
+ // load-bearing, not defensive garnish.
14
13
  const DEVELOPER_HOST = 'developer.salesforce.com';
15
- const MARKDOWN_CONTENT_TYPE = 'text/markdown';
16
14
  // Salesforce's WAF answers 403 unless the User-Agent LEADS with a recognized HTTP-tool token
17
15
  // (curl/wget/python-requests pass; bare product names and browser UAs on a non-browser TLS stack
18
16
  // are blocked). Lead with the compatibility token — the same convention as browsers' Mozilla/5.0
@@ -24,6 +22,9 @@ const PROBE_HEADERS = {
24
22
  * Derives the candidate `.md` route for a developer.salesforce.com doc URL, or null when the URL
25
23
  * can't serve one (wrong host, not under /docs/, or an atlas.* book — verified to answer `.md`
26
24
  * with an HTML shell, never Markdown). Query/hash are view state and are dropped.
25
+ *
26
+ * @param url - A developer.salesforce.com doc URL (or any URL; non-matching returns null).
27
+ * @returns The derived `.md` URL, or null when no `.md` route exists for this URL.
27
28
  */
28
29
  export function deriveMarkdownUrl(url) {
29
30
  let parsed;
@@ -46,7 +47,11 @@ export function deriveMarkdownUrl(url) {
46
47
  // `::include{src="../../shared/….md"}` directives reference build-time snippet files that are
47
48
  // not published on the .md route (they 404), so they'd survive as inert noise in the artifact.
48
49
  const INCLUDE_DIRECTIVE = /^\s*::include\{[^}]*\}\s*$/;
49
- /** Removes unresolvable `::include{…}` directive lines, reporting how many were dropped. */
50
+ /** Removes unresolvable `::include{…}` directive lines, reporting how many were dropped.
51
+ *
52
+ * @param md - Markdown body from the `.md` route response.
53
+ * @returns Cleaned body string and a count of removed directive lines.
54
+ */
50
55
  export function stripIncludeDirectives(md) {
51
56
  let dropped = 0;
52
57
  const body = md
@@ -68,75 +73,32 @@ export function stripIncludeDirectives(md) {
68
73
  .join('');
69
74
  return { body, dropped };
70
75
  }
71
- // A probe response counts as the Markdown twin only when the server labels it text/markdown AND
72
- // the redirect chain ended on https at the developer host AND the body validates as non-HTML,
73
- // non-error text. Atlas shells fail the content-type check; non-2xx statuses already threw in
74
- // the fetcher (the probe sends no conditional headers, so a 304 can't occur either).
75
- function isMarkdownResponse(res) {
76
- if (!res.content)
77
- return false;
78
- const mediaType = (res.contentType?.split(';')[0] ?? '').trim().toLowerCase();
79
- if (mediaType !== MARKDOWN_CONTENT_TYPE)
80
- return false;
81
- let finalUrl;
82
- try {
83
- finalUrl = new URL(res.finalUrl);
84
- }
85
- catch {
86
- return false;
87
- }
88
- // Scheme matters, not just host: a redirect hop downgrading to plain http would let an on-path
89
- // response be cached as the trusted twin.
90
- if (finalUrl.protocol !== 'https:' || finalUrl.hostname.toLowerCase() !== DEVELOPER_HOST) {
91
- return false;
92
- }
93
- return validateTextArtifact(res.content).ok;
76
+ function stripIncludesForTwin(raw) {
77
+ const { body, dropped } = stripIncludeDirectives(raw);
78
+ if (dropped === 0)
79
+ return { body };
80
+ return {
81
+ body,
82
+ qualityNote: `${dropped} ::include directive(s) removed (shared snippets are not published on the .md route)`,
83
+ };
94
84
  }
95
- // A real twin is a small static file that answers fast; anything slower should mean "no twin,
96
- // fall back now" instead of stacking the fetcher's default 10s on top of the browser render.
97
- const PROBE_TIMEOUT_MS = 4_000;
98
- // The browser path refuses to cache captures under this floor (MIN_CONTAINER_CHARS); a twin that
99
- // thin is a rollout stub, and falling back gives the rendered page a chance to do better.
100
- const MIN_TWIN_CHARS = 100;
101
85
  /**
102
86
  * Probes the derived `.md` route for a developer doc URL. Returns a complete SiteFetchResult
103
87
  * (with route_markdown provenance) when the twin validates, or null so the caller falls back to
104
88
  * the browser capture. Never throws: any network/validation failure is just "no twin".
89
+ *
90
+ * @param url - The developer.salesforce.com doc page URL to probe.
91
+ * @param fetcher - HTTP fetcher (injectable for tests; defaults to `fetchText`).
92
+ * @returns SiteFetchResult with route_markdown provenance, or null when no valid twin is found.
105
93
  */
106
94
  export async function fetchDeveloperRouteMarkdown(url, fetcher = fetchText) {
107
95
  const candidate = deriveMarkdownUrl(url);
108
96
  if (!candidate)
109
97
  return null;
110
- let res;
111
- try {
112
- res = await fetcher(candidate, { headers: PROBE_HEADERS, timeoutMs: PROBE_TIMEOUT_MS });
113
- }
114
- catch {
115
- return null;
116
- }
117
- if (!isMarkdownResponse(res))
118
- return null;
119
- const { body, dropped } = stripIncludeDirectives(res.content);
120
- const extraction = extractFromSource(body, res.finalUrl);
121
- if (extraction.detailedMarkdown.length < MIN_TWIN_CHARS)
122
- return null;
123
- if (dropped > 0) {
124
- extraction.qualityNotes = [
125
- ...(extraction.qualityNotes ?? []),
126
- `${dropped} ::include directive(s) removed (shared snippets are not published on the .md route)`,
127
- ];
128
- }
129
- return {
130
- fetchResult: {
131
- contentType: res.contentType,
132
- etag: res.etag,
133
- lastModified: res.lastModified,
134
- finalUrl: res.finalUrl,
135
- responseSize: res.responseSize,
136
- content: res.content,
137
- },
138
- extraction,
139
- captureMethod: 'route_markdown',
140
- sourceDocUrl: res.finalUrl,
141
- };
98
+ return probeMarkdownTwin([candidate], {
99
+ allowedHost: DEVELOPER_HOST,
100
+ fetcher,
101
+ headers: PROBE_HEADERS,
102
+ transformBody: stripIncludesForTwin,
103
+ });
142
104
  }
@@ -1,25 +1,59 @@
1
1
  import type { SiteFetchResult } from './types.js';
2
- /** True when the extracted text is an error/not-found/loading shell rather than real content. */
2
+ /** True when the extracted text is an error/not-found/loading shell rather than real content.
3
+ *
4
+ * @param text - Extracted Markdown text to check.
5
+ * @returns True when the text matches a Salesforce error/not-found pattern.
6
+ */
3
7
  export declare function looksLikeSalesforceError(text: string): boolean;
8
+ /**
9
+ * Removes known boilerplate lines from converted Salesforce Markdown: the feedback widget text,
10
+ * "View as Markdown" toolbar labels, and empty icon-anchor lines.
11
+ *
12
+ * @param markdown - Markdown output from the DOM or content API conversion.
13
+ * @returns Markdown with boilerplate lines stripped and excess blank lines collapsed.
14
+ */
4
15
  export declare function stripBoilerplate(markdown: string): string;
5
- /** Parses the Salesforce private content API JSON payload. */
16
+ /** Parses the Salesforce private content API JSON payload.
17
+ *
18
+ * @param raw - Raw JSON string from the `/docs/get_document_content/` network response.
19
+ * @returns Parsed `{ content, title }` object, or null when the body is empty or unparseable.
20
+ */
6
21
  export declare function parseDocsApiPayload(raw: string): {
7
22
  content: string;
8
23
  title: string;
9
24
  } | null;
10
- /** Keeps whichever Markdown source carries more structural content (DOM vs content API HTML). */
25
+ /** Keeps whichever Markdown source carries more structural content (DOM vs content API HTML).
26
+ *
27
+ * @param domMarkdown - Markdown serialized from the shadow-DOM container.
28
+ * @param apiHtml - Raw HTML body from the Salesforce content API.
29
+ * @param domTitle - Page title from the DOM capture.
30
+ * @param apiTitle - Title from the content API response.
31
+ * @returns The richer Markdown, the winning title, and a flag indicating whether the API source won.
32
+ */
11
33
  export declare function preferRicherMarkdown(domMarkdown: string, apiHtml: string, domTitle: string, apiTitle: string): {
12
34
  markdown: string;
13
35
  title: string;
14
36
  usedApi: boolean;
15
37
  };
38
+ /**
39
+ * Options for the shared Salesforce Lightning Web Runtime (LWR) shadow-DOM fetcher.
40
+ * `contentSelectors` drive the priority content-container pick; `removeSelectors` strips
41
+ * per-site chrome that survives the shared BASE_REMOVE_SELECTORS list.
42
+ */
16
43
  export interface SalesforceDocOptions {
17
44
  allowedHost: string;
18
45
  contentSelectors: string[];
19
46
  removeSelectors?: string[];
20
47
  }
21
48
  /**
22
- * Renders a Salesforce LWR doc page, serializes its shadow-DOM content container, and converts
23
- * it to Markdown. Validates the host before any network access.
49
+ * Renders a Salesforce Lightning Web Runtime (LWR) doc page, serializes its shadow-DOM content
50
+ * container, and converts it to Markdown. Validates the host before any network access.
51
+ *
52
+ * @param url - The Salesforce doc page URL to fetch.
53
+ * @param options - Allowed host, priority content selectors, and optional per-site remove selectors.
54
+ * @returns Extracted Markdown, fetch metadata, and browser-capture provenance.
55
+ * @throws {Error} When the URL is invalid, the host is not the allowed host, DNS is unsafe,
56
+ * Chrome/CDP navigation or evaluation fails, the response body exceeds the size limit,
57
+ * or the captured content is too short / looks like an error page.
24
58
  */
25
59
  export declare function fetchSalesforceDoc(url: string, { allowedHost, contentSelectors, removeSelectors }: SalesforceDocOptions): Promise<SiteFetchResult>;
@@ -1,6 +1,7 @@
1
1
  import { openCdpPage, waitForLoad, ResponseCapture, describeNavigationFailure, } from '../lib/research/browser.js';
2
2
  import { checkDnsSafety } from '../lib/research/fetcher.js';
3
3
  import { htmlToMarkdown } from '../lib/research/markdown.js';
4
+ import { sanitizePromptInjection } from '../lib/research/prompt-injection.js';
4
5
  import { contentMetrics, contentRicherThan } from '../lib/research/regression.js';
5
6
  import { SALESFORCE_SHADOW_DOM_HELPERS, buildPriorityContainerPickBlock, pollSalesforceContentReady, } from './salesforce-dom-probe.js';
6
7
  // Both help.salesforce.com and developer.salesforce.com are LWR sites that render doc content
@@ -24,7 +25,11 @@ const ERROR_PATTERNS = [
24
25
  /we looked high and low/i,
25
26
  /sorry to interrupt/i,
26
27
  ];
27
- /** True when the extracted text is an error/not-found/loading shell rather than real content. */
28
+ /** True when the extracted text is an error/not-found/loading shell rather than real content.
29
+ *
30
+ * @param text - Extracted Markdown text to check.
31
+ * @returns True when the text matches a Salesforce error/not-found pattern.
32
+ */
28
33
  export function looksLikeSalesforceError(text) {
29
34
  return ERROR_PATTERNS.some((re) => re.test(text));
30
35
  }
@@ -71,6 +76,13 @@ const MARKDOWN_TOOLBAR_LINE = /^(copy as markdown|view as markdown|copy url to m
71
76
  // A link with empty anchor text — e.g. an icon-only "home" logo link. It carries no readable
72
77
  // content and renders as a bare `[](url)`. (Images `![](url)` are intentionally not matched.)
73
78
  const EMPTY_LINK_LINE = /^\[\]\([^)]*\)$/;
79
+ /**
80
+ * Removes known boilerplate lines from converted Salesforce Markdown: the feedback widget text,
81
+ * "View as Markdown" toolbar labels, and empty icon-anchor lines.
82
+ *
83
+ * @param markdown - Markdown output from the DOM or content API conversion.
84
+ * @returns Markdown with boilerplate lines stripped and excess blank lines collapsed.
85
+ */
74
86
  export function stripBoilerplate(markdown) {
75
87
  return markdown
76
88
  .split('\n')
@@ -82,7 +94,11 @@ export function stripBoilerplate(markdown) {
82
94
  .trim();
83
95
  }
84
96
  const DOCS_CONTENT_API = '/docs/get_document_content/';
85
- /** Parses the Salesforce private content API JSON payload. */
97
+ /** Parses the Salesforce private content API JSON payload.
98
+ *
99
+ * @param raw - Raw JSON string from the `/docs/get_document_content/` network response.
100
+ * @returns Parsed `{ content, title }` object, or null when the body is empty or unparseable.
101
+ */
86
102
  export function parseDocsApiPayload(raw) {
87
103
  if (!raw?.trim())
88
104
  return null;
@@ -96,7 +112,14 @@ export function parseDocsApiPayload(raw) {
96
112
  return null;
97
113
  }
98
114
  }
99
- /** Keeps whichever Markdown source carries more structural content (DOM vs content API HTML). */
115
+ /** Keeps whichever Markdown source carries more structural content (DOM vs content API HTML).
116
+ *
117
+ * @param domMarkdown - Markdown serialized from the shadow-DOM container.
118
+ * @param apiHtml - Raw HTML body from the Salesforce content API.
119
+ * @param domTitle - Page title from the DOM capture.
120
+ * @param apiTitle - Title from the content API response.
121
+ * @returns The richer Markdown, the winning title, and a flag indicating whether the API source won.
122
+ */
100
123
  export function preferRicherMarkdown(domMarkdown, apiHtml, domTitle, apiTitle) {
101
124
  const apiMarkdown = stripBoilerplate(htmlToMarkdown(apiHtml));
102
125
  if (contentRicherThan(contentMetrics(apiMarkdown), contentMetrics(domMarkdown))) {
@@ -295,10 +318,22 @@ async function captureSalesforceAttempt(page, url, captureScript, contentSelecto
295
318
  // as if it were real article content — it's plausible-length HTML that isn't a 404/loading shell — so
296
319
  // the fetch would silently "succeed" with Chrome's error page cached as the documentation.
297
320
  const nav = await page.client.send('Page.navigate', { url }, page.sessionId);
321
+ // A fast redirect can fail synchronously within Page.navigate's own response (surfacing as
322
+ // nav.errorText below); check the Fetch-domain safety guard first so a blocked navigation (e.g.
323
+ // an SSRF attempt via a private-address redirect) reports its specific reason instead of the
324
+ // opaque net::ERR_BLOCKED_BY_CLIENT that Chrome's own errorText would otherwise give it.
325
+ const blockedNavigation = page.takeBlockedNavigationError();
326
+ if (blockedNavigation)
327
+ throw blockedNavigation;
298
328
  if (nav.errorText) {
299
329
  throw describeNavigationFailure(nav.errorText, url);
300
330
  }
301
331
  await waitForLoad(page.client, page.sessionId, TIMEOUT_MS, 0).catch(() => { });
332
+ // A mid-navigation redirect may have been blocked after Page.navigate returned; surface that
333
+ // instead of letting Chrome's resulting error page sail through as captured content.
334
+ const blockedNavigationAfterLoad = page.takeBlockedNavigationError();
335
+ if (blockedNavigationAfterLoad)
336
+ throw blockedNavigationAfterLoad;
302
337
  await page.client
303
338
  .send('Runtime.evaluate', { expression: ACCEPT_CONSENT_EXPRESSION }, page.sessionId)
304
339
  .catch(() => { });
@@ -327,6 +362,9 @@ async function captureSalesforceAttempt(page, url, captureScript, contentSelecto
327
362
  return { html, title, detailedMarkdown, usedDocsApi };
328
363
  }
329
364
  function buildSalesforceFetchResult(url, html, title, detailedMarkdown, qualityNotes) {
365
+ // Same untrusted-HTML → Markdown path as extractHtmlContent: sanitize before the artifact
366
+ // is built so site-module browser captures cannot skip the shared prompt-injection guard.
367
+ const sanitizedMarkdown = sanitizePromptInjection(detailedMarkdown);
330
368
  return {
331
369
  fetchResult: {
332
370
  contentType: 'text/html',
@@ -338,8 +376,8 @@ function buildSalesforceFetchResult(url, html, title, detailedMarkdown, qualityN
338
376
  },
339
377
  extraction: {
340
378
  title: title || url,
341
- detailedMarkdown,
342
- confidence: confidenceFor(detailedMarkdown.length),
379
+ detailedMarkdown: sanitizedMarkdown,
380
+ confidence: confidenceFor(sanitizedMarkdown.length),
343
381
  qualityNotes,
344
382
  },
345
383
  // Rendered capture is this fetcher's only strategy; reporting it lets a refresh overwrite
@@ -348,8 +386,15 @@ function buildSalesforceFetchResult(url, html, title, detailedMarkdown, qualityN
348
386
  };
349
387
  }
350
388
  /**
351
- * Renders a Salesforce LWR doc page, serializes its shadow-DOM content container, and converts
352
- * it to Markdown. Validates the host before any network access.
389
+ * Renders a Salesforce Lightning Web Runtime (LWR) doc page, serializes its shadow-DOM content
390
+ * container, and converts it to Markdown. Validates the host before any network access.
391
+ *
392
+ * @param url - The Salesforce doc page URL to fetch.
393
+ * @param options - Allowed host, priority content selectors, and optional per-site remove selectors.
394
+ * @returns Extracted Markdown, fetch metadata, and browser-capture provenance.
395
+ * @throws {Error} When the URL is invalid, the host is not the allowed host, DNS is unsafe,
396
+ * Chrome/CDP navigation or evaluation fails, the response body exceeds the size limit,
397
+ * or the captured content is too short / looks like an error page.
353
398
  */
354
399
  export async function fetchSalesforceDoc(url, { allowedHost, contentSelectors, removeSelectors = [] }) {
355
400
  const parsed = parseSalesforceDocUrl(url, allowedHost);
@@ -1,16 +1,34 @@
1
1
  import type { CdpPage } from '../lib/research/browser.js';
2
- /** Shared in-page helpers for traversing Salesforce LWR open shadow roots. */
2
+ /** Shared in-page helpers for traversing Salesforce Lightning Web Runtime (LWR) open shadow roots. */
3
3
  export declare const SALESFORCE_SHADOW_DOM_HELPERS = "\nconst deepElements = (root) => {\n const out = [];\n const visit = (node) => {\n if (node.shadowRoot) visit(node.shadowRoot);\n for (const child of Array.from(node.children || [])) {\n out.push(child);\n visit(child);\n }\n };\n visit(root);\n return out;\n};\n\nconst deepText = (root) => {\n let txt = '';\n const visit = (node) => {\n if (node.nodeType === 3) {\n txt += node.textContent || '';\n } else if (node.nodeType === 1 || node.nodeType === 9 || node.nodeType === 11) {\n const tag = node.tagName ? node.tagName.toLowerCase() : '';\n if (tag !== 'style' && tag !== 'script') {\n if (node.shadowRoot) visit(node.shadowRoot);\n for (const child of Array.from(node.childNodes || [])) visit(child);\n }\n }\n };\n visit(root);\n return txt;\n};\n\nconst textLenFor = (el) =>\n Math.max((el.innerText || '').trim().length, deepText(el).trim().length);\n";
4
4
  /**
5
5
  * In-page block: pick the highest-priority content container that exists on the page.
6
6
  * Does not fall through to lower-priority selectors when a higher-priority host is present
7
7
  * (e.g. doc-content-layout banner vs doc-amf-reference table).
8
+ *
9
+ * @param selectors - CSS selectors in priority order; the first that matches wins.
10
+ * @param minChars - Minimum text length a matched container must have to be accepted.
11
+ * @param containerVar - Name of the JS variable to assign the result to (default `'container'`).
12
+ * @returns A JavaScript code block string (without the surrounding `(() => {…})()` wrapper).
8
13
  */
9
14
  export declare function buildPriorityContainerPickBlock(selectors: string[], minChars: number, containerVar?: string): string;
10
- /** CDP expression polled until the priority content host reaches minChars and the body settles. */
15
+ /**
16
+ * Chrome DevTools Protocol (CDP) expression polled until the priority content host reaches
17
+ * `minChars` and the body settles.
18
+ *
19
+ * @param selectors - CSS selectors in priority order matching the site's content containers.
20
+ * @param minChars - Minimum text length before the container is considered ready.
21
+ * @returns A self-invoking JS expression string that evaluates to `{ has: boolean, len: number }`.
22
+ */
11
23
  export declare function buildContentReadyProbeExpression(selectors: string[], minChars: number): string;
12
24
  /**
13
25
  * Polls until the priority Salesforce content host is ready and body text has settled.
14
26
  * Returns without throwing on timeout; the caller captures whatever rendered and judges quality.
27
+ *
28
+ * @param page - Open Chrome DevTools Protocol (CDP) page to evaluate expressions on.
29
+ * @param selectors - CSS selectors in priority order matching the site's content containers.
30
+ * @param minChars - Minimum container text length before polling stops.
31
+ * @param timeoutMs - Maximum wall-clock time (ms) to wait before giving up.
32
+ * @returns Resolves when content is ready, or when `timeoutMs` elapses (no throw on timeout).
15
33
  */
16
34
  export declare function pollSalesforceContentReady(page: CdpPage, selectors: string[], minChars: number, timeoutMs: number): Promise<void>;
@@ -1,4 +1,4 @@
1
- /** Shared in-page helpers for traversing Salesforce LWR open shadow roots. */
1
+ /** Shared in-page helpers for traversing Salesforce Lightning Web Runtime (LWR) open shadow roots. */
2
2
  export const SALESFORCE_SHADOW_DOM_HELPERS = `
3
3
  const deepElements = (root) => {
4
4
  const out = [];
@@ -37,6 +37,11 @@ const textLenFor = (el) =>
37
37
  * In-page block: pick the highest-priority content container that exists on the page.
38
38
  * Does not fall through to lower-priority selectors when a higher-priority host is present
39
39
  * (e.g. doc-content-layout banner vs doc-amf-reference table).
40
+ *
41
+ * @param selectors - CSS selectors in priority order; the first that matches wins.
42
+ * @param minChars - Minimum text length a matched container must have to be accepted.
43
+ * @param containerVar - Name of the JS variable to assign the result to (default `'container'`).
44
+ * @returns A JavaScript code block string (without the surrounding `(() => {…})()` wrapper).
40
45
  */
41
46
  export function buildPriorityContainerPickBlock(selectors, minChars, containerVar = 'container') {
42
47
  return `
@@ -61,7 +66,14 @@ export function buildPriorityContainerPickBlock(selectors, minChars, containerVa
61
66
  ${containerVar} = ${containerVar} || document.body || document.documentElement;
62
67
  `;
63
68
  }
64
- /** CDP expression polled until the priority content host reaches minChars and the body settles. */
69
+ /**
70
+ * Chrome DevTools Protocol (CDP) expression polled until the priority content host reaches
71
+ * `minChars` and the body settles.
72
+ *
73
+ * @param selectors - CSS selectors in priority order matching the site's content containers.
74
+ * @param minChars - Minimum text length before the container is considered ready.
75
+ * @returns A self-invoking JS expression string that evaluates to `{ has: boolean, len: number }`.
76
+ */
65
77
  export function buildContentReadyProbeExpression(selectors, minChars) {
66
78
  return `(() => {
67
79
  ${SALESFORCE_SHADOW_DOM_HELPERS}
@@ -90,6 +102,12 @@ export function buildContentReadyProbeExpression(selectors, minChars) {
90
102
  /**
91
103
  * Polls until the priority Salesforce content host is ready and body text has settled.
92
104
  * Returns without throwing on timeout; the caller captures whatever rendered and judges quality.
105
+ *
106
+ * @param page - Open Chrome DevTools Protocol (CDP) page to evaluate expressions on.
107
+ * @param selectors - CSS selectors in priority order matching the site's content containers.
108
+ * @param minChars - Minimum container text length before polling stops.
109
+ * @param timeoutMs - Maximum wall-clock time (ms) to wait before giving up.
110
+ * @returns Resolves when content is ready, or when `timeoutMs` elapses (no throw on timeout).
93
111
  */
94
112
  export async function pollSalesforceContentReady(page, selectors, minChars, timeoutMs) {
95
113
  const expression = buildContentReadyProbeExpression(selectors, minChars);
@@ -1,14 +1,13 @@
1
1
  import type { ExtractionResult } from '../lib/research/extract.js';
2
+ import type { FetchedContent } from '../lib/research/fetcher.js';
2
3
  import type { CaptureMethod } from '../lib/research/schema.js';
4
+ /**
5
+ * Result returned by a site module's custom `fetchPage` implementation.
6
+ * Carries the raw fetch metadata, the extracted Markdown, and optional provenance fields
7
+ * (`captureMethod`, `sourceDocUrl`) so the cache pipeline can record how the content was obtained.
8
+ */
3
9
  export interface SiteFetchResult {
4
- fetchResult: {
5
- contentType: string | null;
6
- etag: string | null;
7
- lastModified: string | null;
8
- finalUrl: string;
9
- responseSize: number;
10
- content: string;
11
- };
10
+ fetchResult: FetchedContent;
12
11
  extraction: ExtractionResult;
13
12
  captureMethod?: CaptureMethod;
14
13
  sourceDocUrl?: string;
@@ -17,12 +16,19 @@ export interface SiteFetchResult {
17
16
  * Stamps a site module's reported provenance onto artifact metadata. Shared by the cache-miss
18
17
  * fetch and revalidation so the two write paths can't drift. A result without captureMethod (a
19
18
  * module predating provenance reporting) leaves the metadata untouched.
19
+ *
20
+ * @param metadata - Artifact metadata fields updated in place.
21
+ * @param siteFetch - Site-module fetch result carrying optional provenance.
20
22
  */
21
23
  export declare function applySiteFetchProvenance(metadata: {
22
24
  capture_method: CaptureMethod | null;
23
25
  source_doc_url: string | null;
24
26
  source_urls: string[];
25
27
  }, siteFetch: SiteFetchResult): void;
28
+ /** Registered site-specific fetch/extract strategy for a domain family.
29
+ * `defaults.rendered` ORs with the user's `--rendered` flag; a missing or false value is a safe default.
30
+ * When `fetchPage` is absent, callers use the generic fetch/extract pipeline.
31
+ */
26
32
  export interface SiteModule {
27
33
  id: string;
28
34
  name: string;
@@ -2,6 +2,9 @@
2
2
  * Stamps a site module's reported provenance onto artifact metadata. Shared by the cache-miss
3
3
  * fetch and revalidation so the two write paths can't drift. A result without captureMethod (a
4
4
  * module predating provenance reporting) leaves the metadata untouched.
5
+ *
6
+ * @param metadata - Artifact metadata fields updated in place.
7
+ * @param siteFetch - Site-module fetch result carrying optional provenance.
5
8
  */
6
9
  export function applySiteFetchProvenance(metadata, siteFetch) {
7
10
  if (!siteFetch.captureMethod)