@taurgis/bonsai 1.0.6 → 3.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 (120) hide show
  1. package/README.md +7 -13
  2. package/agents/agents/official-docs-researcher.agent.md +15 -35
  3. package/agents/agents/salesforce-docs-researcher.agent.md +17 -44
  4. package/agents/hooks/README.md +2 -9
  5. package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
  6. package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
  7. package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
  8. package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
  9. package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
  10. package/agents/instructions/salesforce-research.instructions.md +7 -8
  11. package/agents/instructions/web-research.instructions.md +3 -4
  12. package/agents/skills/web-research/SKILL.md +24 -37
  13. package/bin/cli.mjs +15 -2
  14. package/dist/base-command.d.ts +15 -2
  15. package/dist/base-command.js +32 -4
  16. package/dist/commands/fetch.d.ts +2 -0
  17. package/dist/commands/fetch.js +43 -29
  18. package/dist/commands/import.js +11 -3
  19. package/dist/commands/inspect.d.ts +4 -0
  20. package/dist/commands/inspect.js +48 -24
  21. package/dist/commands/list.d.ts +3 -1
  22. package/dist/commands/list.js +42 -25
  23. package/dist/commands/prune.d.ts +1 -0
  24. package/dist/commands/prune.js +35 -9
  25. package/dist/commands/status.d.ts +2 -0
  26. package/dist/commands/status.js +57 -25
  27. package/dist/commands.d.ts +0 -2
  28. package/dist/commands.js +0 -2
  29. package/dist/hooks/command-not-found/suggest.d.ts +7 -1
  30. package/dist/hooks/command-not-found/suggest.js +82 -57
  31. package/dist/lib/argv.js +51 -7
  32. package/dist/lib/color.d.ts +20 -0
  33. package/dist/lib/color.js +36 -0
  34. package/dist/lib/envelope.js +8 -2
  35. package/dist/lib/help-preflight.d.ts +15 -0
  36. package/dist/lib/help-preflight.js +81 -0
  37. package/dist/lib/limit-flag.d.ts +1 -0
  38. package/dist/lib/limit-flag.js +24 -0
  39. package/dist/lib/research/{search-index.d.ts → artifact-index.d.ts} +5 -5
  40. package/dist/lib/research/{search-index.js → artifact-index.js} +10 -10
  41. package/dist/lib/research/browser.d.ts +0 -8
  42. package/dist/lib/research/browser.js +20 -43
  43. package/dist/lib/research/regression.d.ts +18 -0
  44. package/dist/lib/research/regression.js +47 -0
  45. package/dist/lib/research/storage.d.ts +0 -1
  46. package/dist/lib/research/storage.js +15 -13
  47. package/dist/lib/research/url.d.ts +22 -0
  48. package/dist/lib/research/url.js +63 -1
  49. package/dist/lib/text.d.ts +3 -1
  50. package/dist/lib/text.js +9 -1
  51. package/dist/sites/coveo.d.ts +7 -0
  52. package/dist/sites/coveo.js +33 -0
  53. package/dist/sites/index.js +1 -18
  54. package/dist/sites/salesforce/fetch-page.js +1 -1
  55. package/dist/sites/salesforce/index.js +3 -8
  56. package/dist/sites/salesforce-developer/fetch-page.js +3 -1
  57. package/dist/sites/salesforce-developer/index.js +1 -2
  58. package/dist/sites/salesforce-doc-fetch.d.ts +11 -0
  59. package/dist/sites/salesforce-doc-fetch.js +117 -76
  60. package/dist/sites/salesforce-dom-probe.d.ts +16 -0
  61. package/dist/sites/salesforce-dom-probe.js +114 -0
  62. package/dist/sites/types.d.ts +0 -6
  63. package/oclif.manifest.json +31 -106
  64. package/package.json +32 -25
  65. package/dist/commands/index.d.ts +0 -35
  66. package/dist/commands/index.js +0 -306
  67. package/dist/commands/research/config/base.d.ts +0 -23
  68. package/dist/commands/research/config/base.js +0 -46
  69. package/dist/commands/research/config/get.d.ts +0 -20
  70. package/dist/commands/research/config/get.js +0 -45
  71. package/dist/commands/research/config/list.d.ts +0 -17
  72. package/dist/commands/research/config/list.js +0 -42
  73. package/dist/commands/research/config/set.d.ts +0 -31
  74. package/dist/commands/research/config/set.js +0 -94
  75. package/dist/commands/research/config/unset.d.ts +0 -21
  76. package/dist/commands/research/config/unset.js +0 -59
  77. package/dist/commands/research/import.d.ts +0 -38
  78. package/dist/commands/research/import.js +0 -313
  79. package/dist/commands/research/inspect.d.ts +0 -13
  80. package/dist/commands/research/inspect.js +0 -86
  81. package/dist/commands/research/list.d.ts +0 -25
  82. package/dist/commands/research/list.js +0 -143
  83. package/dist/commands/research/prune.d.ts +0 -24
  84. package/dist/commands/research/prune.js +0 -141
  85. package/dist/commands/research/search.d.ts +0 -32
  86. package/dist/commands/research/search.js +0 -334
  87. package/dist/commands/research/status.d.ts +0 -17
  88. package/dist/commands/research/status.js +0 -108
  89. package/dist/commands/research.d.ts +0 -36
  90. package/dist/commands/research.js +0 -304
  91. package/dist/commands/search.d.ts +0 -35
  92. package/dist/commands/search.js +0 -391
  93. package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +0 -1
  94. package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +0 -2
  95. package/dist/lib/research/docs/discovery-index.d.ts +0 -14
  96. package/dist/lib/research/docs/discovery-index.js +0 -112
  97. package/dist/lib/research/docs/remote-search-runner.d.ts +0 -21
  98. package/dist/lib/research/docs/remote-search-runner.js +0 -114
  99. package/dist/lib/research/docs/remote-search.d.ts +0 -14
  100. package/dist/lib/research/docs/remote-search.js +0 -101
  101. package/dist/lib/research/docs/search-index.d.ts +0 -13
  102. package/dist/lib/research/docs/search-index.js +0 -176
  103. package/dist/sites/algolia.d.ts +0 -9
  104. package/dist/sites/algolia.js +0 -26
  105. package/dist/sites/auto.d.ts +0 -8
  106. package/dist/sites/auto.js +0 -30
  107. package/dist/sites/nextjs/index.d.ts +0 -2
  108. package/dist/sites/nextjs/index.js +0 -52
  109. package/dist/sites/react/index.d.ts +0 -2
  110. package/dist/sites/react/index.js +0 -17
  111. package/dist/sites/salesforce/browser-search.d.ts +0 -7
  112. package/dist/sites/salesforce/browser-search.js +0 -82
  113. package/dist/sites/salesforce/coveo.d.ts +0 -23
  114. package/dist/sites/salesforce/coveo.js +0 -161
  115. package/dist/sites/salesforce/token.d.ts +0 -26
  116. package/dist/sites/salesforce/token.js +0 -154
  117. package/dist/sites/tailwind/index.d.ts +0 -2
  118. package/dist/sites/tailwind/index.js +0 -17
  119. package/dist/sites/vue/index.d.ts +0 -2
  120. package/dist/sites/vue/index.js +0 -17
@@ -1,101 +0,0 @@
1
- // Algolia identifiers extracted from untrusted page HTML are interpolated into the request URL, so
2
- // they MUST be format-validated to prevent SSRF (an unvalidated appId like "evil.com/x" would point
3
- // the request at an attacker host). App IDs are uppercase alphanumeric; public search keys are hex;
4
- // the index name must contain no slash (it is path-encoded but we still reject separators).
5
- const ALGOLIA_APP_ID_RE = /^[A-Z0-9]{6,24}$/;
6
- const ALGOLIA_API_KEY_RE = /^[a-f0-9]{16,64}$/;
7
- const ALGOLIA_INDEX_RE = /^[A-Za-z0-9._~-]{1,128}$/;
8
- // Pulls a DocSearch config out of embedded site data. Returns null unless all three fields are
9
- // present AND well-formed — a partial or malformed signal is not enough to query.
10
- export function extractAlgoliaConfig(html) {
11
- const text = html.replace(/\\"/g, '"');
12
- const appId = text.match(/"appId"\s*:\s*"([^"]+)"/)?.[1];
13
- const apiKey = text.match(/"apiKey"\s*:\s*"([^"]+)"/)?.[1];
14
- const indexName = text.match(/"indexName"\s*:\s*"([^"]+)"/)?.[1];
15
- if (!appId || !apiKey || !indexName)
16
- return null;
17
- if (!ALGOLIA_APP_ID_RE.test(appId) ||
18
- !ALGOLIA_API_KEY_RE.test(apiKey) ||
19
- !ALGOLIA_INDEX_RE.test(indexName)) {
20
- return null;
21
- }
22
- return { appId, apiKey, indexName };
23
- }
24
- export function algoliaQueryUrl(config) {
25
- return `https://${config.appId}-dsn.algolia.net/1/indexes/${encodeURIComponent(config.indexName)}/query`;
26
- }
27
- // DocSearch records nest the page hierarchy under `hierarchy` and the page URL under `url`.
28
- function algoliaTitle(hit) {
29
- const h = hit?.hierarchy ?? {};
30
- // Prefer the deepest (most specific) hierarchy level present.
31
- for (const level of ['lvl6', 'lvl5', 'lvl4', 'lvl3', 'lvl2', 'lvl1', 'lvl0']) {
32
- if (h[level])
33
- return String(h[level]);
34
- }
35
- return hit?.url ?? 'Untitled';
36
- }
37
- export function parseAlgoliaResponse(body, indexUrl) {
38
- const data = JSON.parse(body);
39
- if (!Array.isArray(data.hits))
40
- throw new Error('Algolia response missing "hits"');
41
- return data.hits
42
- .filter((hit) => typeof hit?.url === 'string')
43
- .map((hit, i) => ({
44
- title: algoliaTitle(hit),
45
- url: hit.url,
46
- snippet: hit?.content ?? undefined,
47
- provider: 'algolia-docsearch',
48
- score: data.hits.length - i, // preserve Algolia's relevance order
49
- indexUrl,
50
- }));
51
- }
52
- function vitepressUrl(base, id) {
53
- try {
54
- return new URL(id, base).toString();
55
- }
56
- catch {
57
- return null;
58
- }
59
- }
60
- // Ranks one VitePress doc by query term hits: a title match weighs 10×, body occurrences 1× each
61
- // (capped at 5). Kept separate so the search loop stays a flat filter → score → format.
62
- function scoreVitePressDoc(doc, terms) {
63
- const haystack = `${doc.title ?? ''} ${(doc.titles ?? []).join(' ')} ${doc.text ?? ''}`.toLowerCase();
64
- let score = 0;
65
- for (const term of terms) {
66
- if ((doc.title ?? '').toLowerCase().includes(term))
67
- score += 10;
68
- score += Math.min(haystack.split(term).length - 1, 5);
69
- }
70
- return score;
71
- }
72
- /**
73
- * Parses a VitePress local search index (MiniSearch documents) and ranks docs by query term hits.
74
- * `base` is the docs site origin/base used to resolve the relative `id` routes.
75
- */
76
- export function searchVitePressLocalIndex(body, indexUrl, base, query) {
77
- const docs = JSON.parse(body);
78
- if (!Array.isArray(docs))
79
- throw new Error('VitePress local index is not an array');
80
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
81
- const results = [];
82
- for (const doc of docs) {
83
- if (!doc.id)
84
- continue;
85
- const score = scoreVitePressDoc(doc, terms);
86
- if (score <= 0)
87
- continue;
88
- const url = vitepressUrl(base, doc.id);
89
- if (!url)
90
- continue;
91
- results.push({
92
- title: doc.title ?? doc.id,
93
- url,
94
- snippet: (doc.text ?? '').slice(0, 150),
95
- provider: 'vitepress-local',
96
- score,
97
- indexUrl,
98
- });
99
- }
100
- return results.sort((a, b) => b.score - a.score);
101
- }
@@ -1,13 +0,0 @@
1
- import type { SearchProvider } from './capabilities.js';
2
- export interface DocsSearchResult {
3
- title: string;
4
- url: string;
5
- anchor?: string;
6
- snippet?: string;
7
- provider: SearchProvider;
8
- score: number;
9
- indexUrl: string;
10
- }
11
- export declare function searchMkDocsIndex(body: string, indexUrl: string, query: string): DocsSearchResult[];
12
- export declare function searchJekyllIndex(body: string, indexUrl: string, query: string): DocsSearchResult[];
13
- export declare function searchSphinxIndex(js: string, indexUrl: string, query: string): DocsSearchResult[];
@@ -1,176 +0,0 @@
1
- function queryTerms(query) {
2
- return query.toLowerCase().split(/\s+/).filter(Boolean);
3
- }
4
- // Score = title term hits (heavy) + body term hits (light). Zero means no match (dropped).
5
- function scoreText(title, body, terms) {
6
- const t = title.toLowerCase();
7
- const b = body.toLowerCase();
8
- let score = 0;
9
- for (const term of terms) {
10
- if (t.includes(term))
11
- score += 10;
12
- const bodyHits = b.split(term).length - 1;
13
- score += Math.min(bodyHits, 5);
14
- }
15
- return score;
16
- }
17
- function snippetFrom(text, terms) {
18
- const lower = text.toLowerCase();
19
- let idx = 0;
20
- for (const term of terms) {
21
- const found = lower.indexOf(term);
22
- if (found !== -1) {
23
- idx = found;
24
- break;
25
- }
26
- }
27
- const start = Math.max(0, idx - 40);
28
- return text
29
- .slice(start, start + 150)
30
- .replace(/\s+/g, ' ')
31
- .trim();
32
- }
33
- function splitAnchor(location) {
34
- const hashIdx = location.indexOf('#');
35
- if (hashIdx === -1)
36
- return { path: location };
37
- return { path: location.slice(0, hashIdx), anchor: location.slice(hashIdx + 1) || undefined };
38
- }
39
- function resolve(base, relative) {
40
- try {
41
- return new URL(relative, base).toString();
42
- }
43
- catch {
44
- return null;
45
- }
46
- }
47
- // MkDocs / Material for MkDocs: { docs: [ { location: "page/#anchor", title, text } ] }.
48
- // Locations are relative to the site root (the index sits at <root>/search/search_index.json).
49
- export function searchMkDocsIndex(body, indexUrl, query) {
50
- const data = JSON.parse(body);
51
- if (!Array.isArray(data.docs))
52
- throw new Error('MkDocs index missing "docs" array');
53
- const root = resolve(indexUrl, '../') ?? indexUrl;
54
- const terms = queryTerms(query);
55
- const results = [];
56
- for (const doc of data.docs) {
57
- if (!doc.location && doc.location !== '')
58
- continue;
59
- const { path, anchor } = splitAnchor(doc.location ?? '');
60
- const url = resolve(root, path);
61
- if (!url)
62
- continue;
63
- const score = scoreText(doc.title ?? '', doc.text ?? '', terms);
64
- if (score <= 0)
65
- continue;
66
- results.push({
67
- title: doc.title ?? path,
68
- url,
69
- anchor,
70
- snippet: snippetFrom(doc.text ?? '', terms),
71
- provider: 'mkdocs-local',
72
- score,
73
- indexUrl,
74
- });
75
- }
76
- return results.sort((a, b) => b.score - a.score);
77
- }
78
- // Just the Docs: { "0": { title, content, url|relUrl }, "1": {...} } keyed by stringified ints.
79
- export function searchJekyllIndex(body, indexUrl, query) {
80
- const data = JSON.parse(body);
81
- if (typeof data !== 'object' || data === null)
82
- throw new Error('Jekyll index is not an object');
83
- const terms = queryTerms(query);
84
- const results = [];
85
- for (const entry of Object.values(data)) {
86
- const rel = entry.url ?? entry.relUrl;
87
- if (!rel)
88
- continue;
89
- const { path, anchor } = splitAnchor(rel);
90
- const url = resolve(indexUrl, path);
91
- if (!url)
92
- continue;
93
- const score = scoreText(entry.title ?? '', entry.content ?? '', terms);
94
- if (score <= 0)
95
- continue;
96
- results.push({
97
- title: entry.title ?? path,
98
- url,
99
- anchor,
100
- snippet: snippetFrom(entry.content ?? '', terms),
101
- provider: 'jekyll-json',
102
- score,
103
- indexUrl,
104
- });
105
- }
106
- return results.sort((a, b) => b.score - a.score);
107
- }
108
- // Extracts the object literal from `Search.setIndex({...})` as inert data and JSON.parses it —
109
- // never eval. Anchors on the `setIndex(` call so leading comments/whitespace can't shift the slice;
110
- // falls back to first-`{`/last-`}` for index variants that assign the object differently.
111
- function parseSphinxObject(js) {
112
- const call = js.indexOf('setIndex(');
113
- const start = call !== -1 ? js.indexOf('{', call) : js.indexOf('{');
114
- const end = js.lastIndexOf('}');
115
- if (start === -1 || end <= start)
116
- throw new Error('Sphinx index has no object literal');
117
- return JSON.parse(js.slice(start, end + 1));
118
- }
119
- function collectDocIndices(map, terms) {
120
- const hits = new Map();
121
- if (!map)
122
- return hits;
123
- for (const [word, posting] of Object.entries(map)) {
124
- const lower = word.toLowerCase();
125
- if (!terms.some((t) => lower.includes(t)))
126
- continue;
127
- const docs = Array.isArray(posting) ? posting : [posting];
128
- for (const d of docs)
129
- hits.set(d, (hits.get(d) ?? 0) + 1);
130
- }
131
- return hits;
132
- }
133
- // Scores each Sphinx doc index by term hits: title-term hits and exact title matches weigh 10×,
134
- // body-term hits 1×. Kept separate so the search function stays a flat parse → score → format.
135
- function scoreSphinxDocs(index, terms, titles) {
136
- const scores = new Map();
137
- for (const [doc, count] of collectDocIndices(index.titleterms, terms)) {
138
- scores.set(doc, (scores.get(doc) ?? 0) + count * 10);
139
- }
140
- for (const [doc, count] of collectDocIndices(index.terms, terms)) {
141
- scores.set(doc, (scores.get(doc) ?? 0) + count);
142
- }
143
- for (let i = 0; i < titles.length; i++) {
144
- if (terms.some((t) => (titles[i] ?? '').toLowerCase().includes(t))) {
145
- scores.set(i, (scores.get(i) ?? 0) + 10);
146
- }
147
- }
148
- return scores;
149
- }
150
- // Sphinx searchindex.js. Page URLs are docname + ".html" relative to the index's directory.
151
- export function searchSphinxIndex(js, indexUrl, query) {
152
- const index = parseSphinxObject(js);
153
- const docnames = index.docnames ?? [];
154
- const titles = index.titles ?? [];
155
- if (docnames.length === 0)
156
- throw new Error('Sphinx index has no docnames');
157
- const terms = queryTerms(query);
158
- const scores = scoreSphinxDocs(index, terms, titles);
159
- const results = [];
160
- for (const [doc, score] of scores) {
161
- const name = docnames[doc];
162
- if (!name || score <= 0)
163
- continue;
164
- const url = resolve(indexUrl, `${name}.html`);
165
- if (!url)
166
- continue;
167
- results.push({
168
- title: titles[doc] ?? name,
169
- url,
170
- provider: 'sphinx-searchindex',
171
- score,
172
- indexUrl,
173
- });
174
- }
175
- return results.sort((a, b) => b.score - a.score);
176
- }
@@ -1,9 +0,0 @@
1
- import type { SiteModule } from './types.js';
2
- export declare function createAlgoliaSite(id: string, name: string, domains: string[], appId: string, apiKey: string, indexName: string): SiteModule;
3
- export declare const react: SiteModule;
4
- export declare const vue: SiteModule;
5
- export declare const tailwind: SiteModule;
6
- export declare const jest: SiteModule;
7
- export declare const cypress: SiteModule;
8
- export declare const vitest: SiteModule;
9
- export declare const vitepress: SiteModule;
@@ -1,26 +0,0 @@
1
- import { algoliaQueryUrl, parseAlgoliaResponse } from '../lib/research/docs/remote-search.js';
2
- import { postJson } from '../lib/research/fetcher.js';
3
- export function createAlgoliaSite(id, name, domains, appId, apiKey, indexName) {
4
- return {
5
- id,
6
- name,
7
- domains,
8
- search: async (query) => {
9
- const url = algoliaQueryUrl({ appId, apiKey, indexName });
10
- const bodyParams = { params: `query=${encodeURIComponent(query)}&hitsPerPage=20` };
11
- const res = await postJson(url, bodyParams, {
12
- 'Content-Type': 'application/json',
13
- 'X-Algolia-API-Key': apiKey,
14
- 'X-Algolia-Application-Id': appId,
15
- });
16
- return parseAlgoliaResponse(res, url);
17
- },
18
- };
19
- }
20
- export const react = createAlgoliaSite('react', 'React', ['react.dev', 'legacy.reactjs.org'], '1FCF9AYYAT', '1b7ad4e1c89e645e351e59d40544eda1', 'beta-react');
21
- export const vue = createAlgoliaSite('vue', 'Vue.js', ['vuejs.org'], 'ML0LEBN7FQ', '10e7a8b13e6aec4007343338ab134e05', 'vuejs');
22
- export const tailwind = createAlgoliaSite('tailwind', 'Tailwind CSS', ['tailwindcss.com'], 'KNPXZI5B0M', '5fc87cef58bb80203d2207578309fab6', 'tailwindcss');
23
- export const jest = createAlgoliaSite('jest', 'Jest', ['jestjs.io'], 'HP439UUSOL', 'e5e670fd16f8f17caada79d6b0931682', 'jest-v2');
24
- export const cypress = createAlgoliaSite('cypress', 'Cypress', ['docs.cypress.io', 'cypress.io'], 'R9KDA5FMJB', 'b4af59e23bc2fa05281af7dcf13fcae5', 'cypress_docs');
25
- export const vitest = createAlgoliaSite('vitest', 'Vitest', ['vitest.dev'], 'ZTF29HGJ69', '9c3ced6fed60d2670bb36ab7e8bed8bc', 'vitest');
26
- export const vitepress = createAlgoliaSite('vitepress', 'VitePress', ['vitepress.dev'], '8J64VVRP8K', '52f578a92b88ad6abde815aae2b0ad7c', 'vitepress');
@@ -1,8 +0,0 @@
1
- import type { SiteModule } from './types.js';
2
- export declare function createAutoSite(id: string, name: string, domains: string[], docsUrl: string): SiteModule;
3
- export declare const angular: SiteModule;
4
- export declare const redux: SiteModule;
5
- export declare const vite: SiteModule;
6
- export declare const fastify: SiteModule;
7
- export declare const rollup: SiteModule;
8
- export declare const vueuse: SiteModule;
@@ -1,30 +0,0 @@
1
- import { runRemoteDocsSearch } from '../lib/research/docs/remote-search-runner.js';
2
- import { fetchStaticHtml, fetchText, postJson } from '../lib/research/fetcher.js';
3
- export function createAutoSite(id, name, domains, docsUrl) {
4
- return {
5
- id,
6
- name,
7
- domains,
8
- search: async (query) => {
9
- const deps = {
10
- fetchStatic: async (url) => {
11
- const res = await fetchStaticHtml(url);
12
- return { content: res.content, finalUrl: res.finalUrl };
13
- },
14
- fetchText: async (url) => {
15
- const res = await fetchText(url);
16
- return { content: res.content, status: res.status };
17
- },
18
- postJson: (url, body, headers) => postJson(url, body, headers),
19
- };
20
- const { results } = await runRemoteDocsSearch(docsUrl, query, deps);
21
- return results;
22
- },
23
- };
24
- }
25
- export const angular = createAutoSite('angular', 'Angular', ['angular.dev'], 'https://angular.dev');
26
- export const redux = createAutoSite('redux', 'Redux', ['redux.js.org'], 'https://redux.js.org');
27
- export const vite = createAutoSite('vite', 'Vite', ['vitejs.dev', 'vite.dev'], 'https://vite.dev');
28
- export const fastify = createAutoSite('fastify', 'Fastify', ['fastify.dev', 'fastify.io'], 'https://fastify.dev');
29
- export const rollup = createAutoSite('rollup', 'Rollup', ['rollupjs.org'], 'https://rollupjs.org');
30
- export const vueuse = createAutoSite('vueuse', 'VueUse', ['vueuse.org'], 'https://vueuse.org');
@@ -1,2 +0,0 @@
1
- import type { SiteModule } from '../types.js';
2
- export declare const nextjs: SiteModule;
@@ -1,52 +0,0 @@
1
- import { postJson } from '../../lib/research/fetcher.js';
2
- export const nextjs = {
3
- id: 'nextjs',
4
- name: 'Next.js',
5
- domains: ['nextjs.org'],
6
- search: async (query) => {
7
- const url = 'https://nextjs.org/api/search';
8
- const bodyStr = await postJson(url, { query, version: 'stable', routerType: 'app' }, {});
9
- let data;
10
- try {
11
- // The Next.js API returns a JSON array sometimes or a JSON object with 'hits'
12
- const parsed = JSON.parse(bodyStr);
13
- if (Array.isArray(parsed)) {
14
- data = parsed;
15
- }
16
- else if (parsed && Array.isArray(parsed.hits)) {
17
- data = parsed.hits;
18
- }
19
- else {
20
- throw new Error('Unexpected response format from Next.js search');
21
- }
22
- }
23
- catch (err) {
24
- throw new Error(`Failed to parse Next.js search response: ${err.message}`);
25
- }
26
- const queryLower = query.toLowerCase();
27
- return data.map((item) => {
28
- const content = item.content || '';
29
- let snippet = content;
30
- const idx = content.toLowerCase().indexOf(queryLower);
31
- if (idx !== -1) {
32
- const start = Math.max(0, idx - 40);
33
- const end = Math.min(content.length, idx + 110);
34
- snippet = content.slice(start, end);
35
- if (start > 0)
36
- snippet = '...' + snippet;
37
- if (end < content.length)
38
- snippet = snippet + '...';
39
- }
40
- else {
41
- snippet = content.slice(0, 150);
42
- }
43
- // Clean up whitespace and newlines so it looks like a snippet and not a raw script
44
- snippet = snippet.replace(/\s+/g, ' ').trim();
45
- return {
46
- url: `https://nextjs.org${item.path}${item.anchor ? `#${item.anchor}` : ''}`,
47
- title: item.title ? `${item.pageTitle} - ${item.title}` : item.pageTitle || 'Untitled',
48
- snippet,
49
- };
50
- });
51
- },
52
- };
@@ -1,2 +0,0 @@
1
- import type { SiteModule } from '../types.js';
2
- export declare const react: SiteModule;
@@ -1,17 +0,0 @@
1
- import { algoliaQueryUrl, parseAlgoliaResponse } from '../../lib/research/docs/remote-search.js';
2
- import { postJson } from '../../lib/research/fetcher.js';
3
- export const react = {
4
- id: 'react',
5
- name: 'React',
6
- domains: ['react.dev', 'legacy.reactjs.org'],
7
- search: async (query) => {
8
- const config = {
9
- appId: '1FCF9AYYAT',
10
- apiKey: '1b7ad4e1c89e645e351e59d40544eda1',
11
- indexName: 'beta-react'
12
- };
13
- const url = algoliaQueryUrl(config);
14
- const body = await postJson(url, { params: `query=${encodeURIComponent(query)}` }, { 'X-Algolia-API-Key': config.apiKey, 'X-Algolia-Application-Id': config.appId });
15
- return parseAlgoliaResponse(body, url);
16
- }
17
- };
@@ -1,7 +0,0 @@
1
- import type { SiteSearchResult } from '../types.js';
2
- /**
3
- * Searches Salesforce Help via Coveo. Reuses a cached token for a direct API call when
4
- * possible; otherwise drives a headless browser, reads the page's own Coveo search response,
5
- * and caches the Coveo token (intercepted from the page's Aura call) for the next run.
6
- */
7
- export declare function searchSalesforce(query: string, limit?: number): Promise<SiteSearchResult[]>;
@@ -1,82 +0,0 @@
1
- import { openCdpPage, waitForLoad, ResponseCapture } from '../../lib/research/browser.js';
2
- import { checkDnsSafety } from '../../lib/research/fetcher.js';
3
- import { buildSearchPageUrl, extractCoveoResults, searchCoveoDirect, COVEO_SEARCH_PATH, AURA_PATH, } from './coveo.js';
4
- import { loadToken, storeToken, isTokenValid, parseAuraToken } from './token.js';
5
- const DEFAULT_LIMIT = 10;
6
- const PAGE_LOAD_TIMEOUT_MS = 15_000;
7
- const COVEO_WAIT_MS = 30_000;
8
- const TOKEN_WAIT_MS = 5_000;
9
- const DEBUG = Boolean(process.env.RESEARCH_DEBUG);
10
- function dbg(message) {
11
- if (DEBUG)
12
- console.error(`[salesforce-search] ${message}`);
13
- }
14
- // Salesforce gates content behind a OneTrust cookie banner; dismissing it lets the page run.
15
- const ACCEPT_CONSENT_EXPRESSION = "(() => { const b = document.querySelector('#onetrust-accept-btn-handler'); if (b) b.click(); })()";
16
- /**
17
- * Searches Salesforce Help via Coveo. Reuses a cached token for a direct API call when
18
- * possible; otherwise drives a headless browser, reads the page's own Coveo search response,
19
- * and caches the Coveo token (intercepted from the page's Aura call) for the next run.
20
- */
21
- export async function searchSalesforce(query, limit = DEFAULT_LIMIT) {
22
- const cached = loadToken();
23
- if (cached && isTokenValid(cached.expiresAtMs)) {
24
- dbg('using cached token (direct Coveo call)');
25
- const results = extractCoveoResults(await searchCoveoDirect(cached, query, limit), limit);
26
- if (results.length)
27
- return results;
28
- dbg('cached token returned no results; falling back to browser');
29
- }
30
- return searchViaBrowser(query, limit);
31
- }
32
- async function searchViaBrowser(query, limit) {
33
- await checkDnsSafety('help.salesforce.com');
34
- const page = await openCdpPage();
35
- try {
36
- // No asset blocking: the search SPA must fully boot to run its query and mint its token.
37
- const capture = new ResponseCapture(page, [
38
- { key: 'coveo', test: ({ url }) => url.includes(COVEO_SEARCH_PATH) },
39
- // help.salesforce.com runs on LWR (no global $A to mint a token ourselves), so we
40
- // intercept the page's own Aura token response — the one whose body parses to a token.
41
- {
42
- key: 'token',
43
- test: ({ url }) => url.includes(AURA_PATH),
44
- accept: (body) => parseAuraToken(body) !== null,
45
- },
46
- ]);
47
- await page.client.send('Page.navigate', { url: buildSearchPageUrl(query) }, page.sessionId);
48
- await waitForLoad(page.client, page.sessionId, PAGE_LOAD_TIMEOUT_MS).catch(() => { });
49
- await page.client
50
- .send('Runtime.evaluate', { expression: ACCEPT_CONSENT_EXPRESSION }, page.sessionId)
51
- .catch(() => { });
52
- const coveoBody = await capture.waitFor('coveo', COVEO_WAIT_MS);
53
- await cacheToken(capture);
54
- if (!coveoBody) {
55
- dbg('no Coveo search response captured');
56
- return [];
57
- }
58
- return extractCoveoResults(parseJson(coveoBody), limit);
59
- }
60
- finally {
61
- await page.close();
62
- }
63
- }
64
- async function cacheToken(capture) {
65
- const tokenBody = await capture.waitFor('token', TOKEN_WAIT_MS);
66
- const token = tokenBody ? parseAuraToken(tokenBody) : null;
67
- if (token) {
68
- storeToken(token);
69
- dbg('cached Coveo token for next run');
70
- }
71
- else {
72
- dbg('no Aura token response intercepted; next run will use the browser again');
73
- }
74
- }
75
- function parseJson(text) {
76
- try {
77
- return JSON.parse(text);
78
- }
79
- catch {
80
- return null;
81
- }
82
- }
@@ -1,23 +0,0 @@
1
- import type { SiteSearchResult } from '../types.js';
2
- import { type TokenInfo } from './token.js';
3
- export declare const ALLOWED_DOC_HOSTS: Set<string>;
4
- export declare const SEARCH_PAGE_URL = "https://help.salesforce.com/s/search-result";
5
- export declare const COVEO_SEARCH_PATH = "/services/apexrest/coveo/analytics/rest/search/v2";
6
- export declare const DEFAULT_LANGUAGE = "en_US";
7
- export declare const AURA_PATH = "/s/sfsites/aura";
8
- export declare function isAllowedDocHost(hostname: string): boolean;
9
- /** The search page reads its query from the URL hash, so this URL is browser-only (never normalized). */
10
- export declare function buildSearchPageUrl(query: string, language?: string): string;
11
- /**
12
- * Coveo indexes Help articles under an internal `/help_doccontent` URL; rewrite it to the
13
- * canonical `/s/articleView` page a human (or our fetcher) can actually open.
14
- */
15
- export declare function normalizeHelpDocContentUrl(rawUrl: string): string;
16
- /** Maps a raw Coveo response into deduped, host-validated search results. */
17
- export declare function extractCoveoResults(data: unknown, limit: number): SiteSearchResult[];
18
- /**
19
- * Direct Coveo Search API call using a cached token — the no-browser fast path.
20
- * Tries a bearer header first, then falls back to a query-string token on auth rejection.
21
- * Returns the parsed response body (with a `results` array) or null on failure.
22
- */
23
- export declare function searchCoveoDirect(tokenInfo: TokenInfo, query: string, limit: number, language?: string, timeoutMs?: number): Promise<unknown | null>;