@taurgis/bonsai 1.0.6 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -13
- package/agents/agents/official-docs-researcher.agent.md +15 -35
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -44
- package/agents/hooks/README.md +2 -9
- package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
- package/agents/instructions/salesforce-research.instructions.md +7 -8
- package/agents/instructions/web-research.instructions.md +3 -4
- package/agents/skills/web-research/SKILL.md +24 -37
- package/bin/cli.mjs +15 -2
- package/dist/base-command.d.ts +15 -2
- package/dist/base-command.js +32 -4
- package/dist/commands/fetch.d.ts +3 -0
- package/dist/commands/fetch.js +96 -30
- package/dist/commands/import.js +14 -5
- package/dist/commands/inspect.d.ts +4 -0
- package/dist/commands/inspect.js +48 -24
- package/dist/commands/list.d.ts +3 -1
- package/dist/commands/list.js +42 -25
- package/dist/commands/prune.d.ts +1 -0
- package/dist/commands/prune.js +35 -9
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +57 -25
- package/dist/commands.d.ts +0 -2
- package/dist/commands.js +0 -2
- package/dist/hooks/command-not-found/suggest.d.ts +7 -1
- package/dist/hooks/command-not-found/suggest.js +82 -57
- package/dist/lib/argv.js +51 -7
- package/dist/lib/color.d.ts +20 -0
- package/dist/lib/color.js +36 -0
- package/dist/lib/envelope.js +8 -2
- package/dist/lib/help-preflight.d.ts +15 -0
- package/dist/lib/help-preflight.js +81 -0
- package/dist/lib/limit-flag.d.ts +1 -0
- package/dist/lib/limit-flag.js +24 -0
- package/dist/lib/research/{search-index.d.ts → artifact-index.d.ts} +5 -5
- package/dist/lib/research/{search-index.js → artifact-index.js} +10 -10
- package/dist/lib/research/browser.d.ts +1 -8
- package/dist/lib/research/browser.js +32 -46
- package/dist/lib/research/docs/markdown-source.js +2 -1
- package/dist/lib/research/extract.js +2 -1
- package/dist/lib/research/fetcher.js +92 -48
- package/dist/lib/research/prompt-injection.d.ts +1 -0
- package/dist/lib/research/prompt-injection.js +149 -0
- package/dist/lib/research/proxy.d.ts +35 -0
- package/dist/lib/research/proxy.js +107 -0
- package/dist/lib/research/regression.d.ts +18 -0
- package/dist/lib/research/regression.js +47 -0
- package/dist/lib/research/storage.d.ts +0 -1
- package/dist/lib/research/storage.js +15 -13
- package/dist/lib/research/url.d.ts +22 -0
- package/dist/lib/research/url.js +63 -1
- package/dist/lib/text.d.ts +3 -1
- package/dist/lib/text.js +9 -1
- package/dist/sites/coveo.d.ts +7 -0
- package/dist/sites/coveo.js +33 -0
- package/dist/sites/index.js +1 -18
- package/dist/sites/salesforce/fetch-page.js +1 -1
- package/dist/sites/salesforce/index.js +3 -8
- package/dist/sites/salesforce-developer/fetch-page.js +3 -1
- package/dist/sites/salesforce-developer/index.js +1 -2
- package/dist/sites/salesforce-doc-fetch.d.ts +11 -0
- package/dist/sites/salesforce-doc-fetch.js +117 -76
- package/dist/sites/salesforce-dom-probe.d.ts +16 -0
- package/dist/sites/salesforce-dom-probe.js +114 -0
- package/dist/sites/types.d.ts +0 -6
- package/oclif.manifest.json +31 -106
- package/package.json +35 -26
- package/dist/commands/index.d.ts +0 -35
- package/dist/commands/index.js +0 -306
- package/dist/commands/research/config/base.d.ts +0 -23
- package/dist/commands/research/config/base.js +0 -46
- package/dist/commands/research/config/get.d.ts +0 -20
- package/dist/commands/research/config/get.js +0 -45
- package/dist/commands/research/config/list.d.ts +0 -17
- package/dist/commands/research/config/list.js +0 -42
- package/dist/commands/research/config/set.d.ts +0 -31
- package/dist/commands/research/config/set.js +0 -94
- package/dist/commands/research/config/unset.d.ts +0 -21
- package/dist/commands/research/config/unset.js +0 -59
- package/dist/commands/research/import.d.ts +0 -38
- package/dist/commands/research/import.js +0 -313
- package/dist/commands/research/inspect.d.ts +0 -13
- package/dist/commands/research/inspect.js +0 -86
- package/dist/commands/research/list.d.ts +0 -25
- package/dist/commands/research/list.js +0 -143
- package/dist/commands/research/prune.d.ts +0 -24
- package/dist/commands/research/prune.js +0 -141
- package/dist/commands/research/search.d.ts +0 -32
- package/dist/commands/research/search.js +0 -334
- package/dist/commands/research/status.d.ts +0 -17
- package/dist/commands/research/status.js +0 -108
- package/dist/commands/research.d.ts +0 -36
- package/dist/commands/research.js +0 -304
- package/dist/commands/search.d.ts +0 -35
- package/dist/commands/search.js +0 -391
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +0 -1
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +0 -2
- package/dist/lib/research/docs/discovery-index.d.ts +0 -14
- package/dist/lib/research/docs/discovery-index.js +0 -112
- package/dist/lib/research/docs/remote-search-runner.d.ts +0 -21
- package/dist/lib/research/docs/remote-search-runner.js +0 -114
- package/dist/lib/research/docs/remote-search.d.ts +0 -14
- package/dist/lib/research/docs/remote-search.js +0 -101
- package/dist/lib/research/docs/search-index.d.ts +0 -13
- package/dist/lib/research/docs/search-index.js +0 -176
- package/dist/sites/algolia.d.ts +0 -9
- package/dist/sites/algolia.js +0 -26
- package/dist/sites/auto.d.ts +0 -8
- package/dist/sites/auto.js +0 -30
- package/dist/sites/nextjs/index.d.ts +0 -2
- package/dist/sites/nextjs/index.js +0 -52
- package/dist/sites/react/index.d.ts +0 -2
- package/dist/sites/react/index.js +0 -17
- package/dist/sites/salesforce/browser-search.d.ts +0 -7
- package/dist/sites/salesforce/browser-search.js +0 -82
- package/dist/sites/salesforce/coveo.d.ts +0 -23
- package/dist/sites/salesforce/coveo.js +0 -161
- package/dist/sites/salesforce/token.d.ts +0 -26
- package/dist/sites/salesforce/token.js +0 -154
- package/dist/sites/tailwind/index.d.ts +0 -2
- package/dist/sites/tailwind/index.js +0 -17
- package/dist/sites/vue/index.d.ts +0 -2
- package/dist/sites/vue/index.js +0 -17
package/dist/commands/search.js
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import { Args, Flags, ux } from '@oclif/core';
|
|
2
|
-
import { BaseCommand } from '../base-command.js';
|
|
3
|
-
import { loadSearchableArtifacts } from '../lib/research/search-index.js';
|
|
4
|
-
import { levenshtein, NO_TOPIC_LABEL, resultListHeading, truncationNotice, } from '../lib/text.js';
|
|
5
|
-
import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
6
|
-
import { evaluateFreshness } from '../lib/research/freshness.js';
|
|
7
|
-
import { ARTIFACT_TYPES } from '../lib/research/schema.js';
|
|
8
|
-
import { detectSite } from '../sites/index.js';
|
|
9
|
-
import { fetchStaticHtml, fetchText, postJson } from '../lib/research/fetcher.js';
|
|
10
|
-
import { normalizeUrl } from '../lib/research/url.js';
|
|
11
|
-
import { runRemoteDocsSearch, } from '../lib/research/docs/remote-search-runner.js';
|
|
12
|
-
// Results are ranked, so the truncation word is "top"; --limit caps at 50.
|
|
13
|
-
const SEARCH_LABELS = {
|
|
14
|
-
noun: 'matching cached research',
|
|
15
|
-
order: 'top',
|
|
16
|
-
maxLimit: 50,
|
|
17
|
-
};
|
|
18
|
-
const REMOTE_SEARCH_DEPS = {
|
|
19
|
-
fetchStatic: async (url) => {
|
|
20
|
-
const res = await fetchStaticHtml(url);
|
|
21
|
-
return { content: res.content, finalUrl: res.finalUrl };
|
|
22
|
-
},
|
|
23
|
-
fetchText: async (url) => {
|
|
24
|
-
const res = await fetchText(url);
|
|
25
|
-
return { content: res.content, status: res.status };
|
|
26
|
-
},
|
|
27
|
-
postJson: (url, body, headers) => postJson(url, body, headers),
|
|
28
|
-
};
|
|
29
|
-
const FRESHNESS_BONUS = { fresh: 30, stale_grace: 10 };
|
|
30
|
-
export default class ResearchSearch extends BaseCommand {
|
|
31
|
-
static id = 'search';
|
|
32
|
-
static summary = 'Search locally cached research artifacts by keywords.';
|
|
33
|
-
static description = 'Scans the local research cache database and ranks matching entries based on topic, tags, source URLs, summary, and content.';
|
|
34
|
-
static examples = [
|
|
35
|
-
{
|
|
36
|
-
description: 'search for cached notes containing specific keywords',
|
|
37
|
-
command: '<%= config.bin %> <%= command.id %> "authentication flow"',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
description: 'search with remote docs fallback',
|
|
41
|
-
command: '<%= config.bin %> <%= command.id %> "router" --remote https://react.dev',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
description: 'search local cache with tag filtering',
|
|
45
|
-
command: '<%= config.bin %> <%= command.id %> "setup" --tags node --tags tutorial',
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
static args = {
|
|
49
|
-
query: Args.string({
|
|
50
|
-
required: true,
|
|
51
|
-
description: 'the search query containing terms to match',
|
|
52
|
-
}),
|
|
53
|
-
};
|
|
54
|
-
static flags = {
|
|
55
|
-
topic: Flags.string({
|
|
56
|
-
char: 't',
|
|
57
|
-
description: 'filter results by exact topic (case-insensitive)',
|
|
58
|
-
}),
|
|
59
|
-
tags: Flags.string({
|
|
60
|
-
char: 'g',
|
|
61
|
-
description: 'filter results by tags (must match all tags specified)',
|
|
62
|
-
multiple: true,
|
|
63
|
-
}),
|
|
64
|
-
'artifact-type': Flags.option({
|
|
65
|
-
description: 'filter results by artifact type',
|
|
66
|
-
options: ARTIFACT_TYPES,
|
|
67
|
-
})(),
|
|
68
|
-
limit: Flags.integer({
|
|
69
|
-
description: 'maximum number of results to return (default 10, max 50)',
|
|
70
|
-
default: 10,
|
|
71
|
-
}),
|
|
72
|
-
'include-stale': Flags.boolean({
|
|
73
|
-
description: 'include stale expired cache entries in the search results',
|
|
74
|
-
default: false,
|
|
75
|
-
}),
|
|
76
|
-
domain: Flags.string({
|
|
77
|
-
description: 'search a documentation site directly (requires compatible site module)',
|
|
78
|
-
}),
|
|
79
|
-
remote: Flags.string({
|
|
80
|
-
description: 'discover uncached docs via public search or index (falls back to local cache)',
|
|
81
|
-
}),
|
|
82
|
-
};
|
|
83
|
-
static stdoutIsPrimaryData = true;
|
|
84
|
-
validateSearchFlags() {
|
|
85
|
-
const limit = this.flags.limit;
|
|
86
|
-
if (limit !== undefined && (limit < 1 || limit > 50)) {
|
|
87
|
-
this.error('Limit must be between 1 and 50.', { exit: 2, code: 'INVALID_LIMIT' });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
getSearchQueryTerms(query) {
|
|
91
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
92
|
-
if (terms.length === 0) {
|
|
93
|
-
this.error('Query string cannot be empty.', { exit: 2, code: 'EMPTY_QUERY' });
|
|
94
|
-
}
|
|
95
|
-
return terms;
|
|
96
|
-
}
|
|
97
|
-
matchesFilters(meta, freshness, topicFlag, tagsFlag, artTypeFlag, includeStale) {
|
|
98
|
-
if (topicFlag &&
|
|
99
|
-
(!meta.topic || meta.topic.trim().toLowerCase() !== topicFlag.trim().toLowerCase())) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
if (tagsFlag && tagsFlag.length > 0) {
|
|
103
|
-
const metaTagsLower = (meta.tags || []).map((t) => t.toLowerCase());
|
|
104
|
-
const hasAllTags = tagsFlag.every((t) => metaTagsLower.includes(t.toLowerCase()));
|
|
105
|
-
if (!hasAllTags) {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (artTypeFlag && meta.artifact_type !== artTypeFlag) {
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
if (!includeStale && freshness === 'stale_expired') {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
phraseMatchBonus(topic, tags, summ, comp, queryTerms) {
|
|
118
|
-
if (queryTerms.length <= 1)
|
|
119
|
-
return 0;
|
|
120
|
-
const fullQuery = queryTerms.join(' ');
|
|
121
|
-
let bonus = 0;
|
|
122
|
-
if (topic === fullQuery)
|
|
123
|
-
bonus += 200;
|
|
124
|
-
if (topic.includes(fullQuery))
|
|
125
|
-
bonus += 100;
|
|
126
|
-
if (tags.includes(fullQuery))
|
|
127
|
-
bonus += 150;
|
|
128
|
-
if (summ.includes(fullQuery))
|
|
129
|
-
bonus += 50 + Math.min(summ.split(fullQuery).length - 1, 5) * 10;
|
|
130
|
-
if (comp.includes(fullQuery))
|
|
131
|
-
bonus += 20 + Math.min(comp.split(fullQuery).length - 1, 10) * 5;
|
|
132
|
-
return bonus;
|
|
133
|
-
}
|
|
134
|
-
calculateScore(meta, summary, compressed, queryTerms, freshness) {
|
|
135
|
-
const topic = (meta.topic || '').toLowerCase();
|
|
136
|
-
const tags = (meta.tags || []).map((t) => t.toLowerCase());
|
|
137
|
-
const sourceUrl = (meta.source_url || '').toLowerCase();
|
|
138
|
-
const sourceUrls = (meta.source_urls || []).map((u) => u.toLowerCase());
|
|
139
|
-
const summ = summary.toLowerCase();
|
|
140
|
-
const comp = compressed.toLowerCase();
|
|
141
|
-
let score = 0;
|
|
142
|
-
let matchedAny = false;
|
|
143
|
-
for (const term of queryTerms) {
|
|
144
|
-
const termScore = scoreSingleTerm(term, topic, tags, sourceUrl, sourceUrls, summ, comp);
|
|
145
|
-
if (termScore > 0) {
|
|
146
|
-
score += termScore;
|
|
147
|
-
matchedAny = true;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (!matchedAny)
|
|
151
|
-
return 0;
|
|
152
|
-
score += this.phraseMatchBonus(topic, tags, summ, comp, queryTerms);
|
|
153
|
-
score += FRESHNESS_BONUS[freshness] ?? 0;
|
|
154
|
-
return score;
|
|
155
|
-
}
|
|
156
|
-
makeSnippet(text, queryTerms) {
|
|
157
|
-
const lower = text.toLowerCase();
|
|
158
|
-
let bestIdx = 0;
|
|
159
|
-
for (const term of queryTerms) {
|
|
160
|
-
const idx = lower.indexOf(term);
|
|
161
|
-
if (idx !== -1) {
|
|
162
|
-
bestIdx = idx;
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
const start = Math.max(0, bestIdx - 40);
|
|
167
|
-
const end = Math.min(text.length, bestIdx + 110);
|
|
168
|
-
let snippet = text.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
169
|
-
if (start > 0)
|
|
170
|
-
snippet = '...' + snippet;
|
|
171
|
-
if (end < text.length)
|
|
172
|
-
snippet = snippet + '...';
|
|
173
|
-
return snippet;
|
|
174
|
-
}
|
|
175
|
-
scanCacheDirForResults(readRoots, queryTerms, currentTime) {
|
|
176
|
-
const results = [];
|
|
177
|
-
for (const { artifact, filePath } of loadSearchableArtifacts(readRoots)) {
|
|
178
|
-
if (artifact.metadata.status !== 'active')
|
|
179
|
-
continue;
|
|
180
|
-
const freshness = evaluateFreshness(artifact.metadata, currentTime, null);
|
|
181
|
-
if (!this.matchesFilters(artifact.metadata, freshness, this.flags.topic, this.flags.tags, this.flags['artifact-type'], Boolean(this.flags['include-stale'])))
|
|
182
|
-
continue;
|
|
183
|
-
let score = this.calculateScore(artifact.metadata, artifact.summary, artifact.compressed, queryTerms, freshness);
|
|
184
|
-
if (score <= 0)
|
|
185
|
-
continue;
|
|
186
|
-
// Section children are more precise than the whole page, so rank a section hit slightly
|
|
187
|
-
// above its parent for the same query (T-22).
|
|
188
|
-
if (artifact.metadata.artifact_type === 'section')
|
|
189
|
-
score += 15;
|
|
190
|
-
const snippetText = [artifact.summary, artifact.compressed].filter(Boolean).join('\n');
|
|
191
|
-
results.push({
|
|
192
|
-
cacheKey: artifact.metadata.cache_key,
|
|
193
|
-
path: filePath,
|
|
194
|
-
artifactType: artifact.metadata.artifact_type,
|
|
195
|
-
sourceUrls: artifact.metadata.source_urls,
|
|
196
|
-
topic: artifact.metadata.topic,
|
|
197
|
-
tags: artifact.metadata.tags,
|
|
198
|
-
freshness,
|
|
199
|
-
captureMethod: artifact.metadata.capture_method,
|
|
200
|
-
tokenEstimate: artifact.metadata.token_estimate,
|
|
201
|
-
snippet: this.makeSnippet(snippetText, queryTerms),
|
|
202
|
-
siteModuleId: artifact.metadata.site_module_id,
|
|
203
|
-
score,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
return results;
|
|
207
|
-
}
|
|
208
|
-
logSearchResults(finalResults, totalMatched) {
|
|
209
|
-
if (this.jsonEnabled())
|
|
210
|
-
return;
|
|
211
|
-
if (finalResults.length === 0) {
|
|
212
|
-
this.log('No matching cached research entries found.');
|
|
213
|
-
this.log(`\nTip: populate the cache first: ${this.config.bin} <url>`);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
this.log(`${resultListHeading(totalMatched, finalResults.length, SEARCH_LABELS)}\n`);
|
|
217
|
-
finalResults.forEach((res, index) => {
|
|
218
|
-
this.log(`${index + 1}. [${res.topic || NO_TOPIC_LABEL}] Score: ${res.score}`);
|
|
219
|
-
this.log(` Cache Key: ${res.cacheKey}`);
|
|
220
|
-
this.log(` Snippet: ${res.snippet}`);
|
|
221
|
-
this.log(` Source URLs: ${res.sourceUrls.join(', ')}\n`);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
// Prints a numbered result list to stdout for human (non-JSON) output. Shared by site and remote
|
|
225
|
-
// search so the two paths can't drift in formatting.
|
|
226
|
-
printResults(heading, results) {
|
|
227
|
-
if (this.jsonEnabled())
|
|
228
|
-
return;
|
|
229
|
-
this.log(heading);
|
|
230
|
-
results.forEach((r, i) => {
|
|
231
|
-
this.log(`${i + 1}. ${r.title}`);
|
|
232
|
-
this.log(` URL: ${r.url}`);
|
|
233
|
-
if (r.snippet)
|
|
234
|
-
this.log(` ${r.snippet}`);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
async executeSiteSearch(query, domain) {
|
|
238
|
-
const siteModule = detectSite(`https://${domain}`);
|
|
239
|
-
if (!siteModule) {
|
|
240
|
-
this.error(`No site module registered for domain: ${domain}`, {
|
|
241
|
-
exit: 2,
|
|
242
|
-
code: 'UNSUPPORTED_DOMAIN',
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
if (!siteModule.search) {
|
|
246
|
-
this.error(`Site module '${siteModule.id}' does not implement search.`, {
|
|
247
|
-
exit: 2,
|
|
248
|
-
code: 'SEARCH_NOT_SUPPORTED',
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
if (!this.jsonEnabled())
|
|
253
|
-
ux.action.start(`Searching ${domain}`);
|
|
254
|
-
const results = await siteModule.search(query);
|
|
255
|
-
if (!this.jsonEnabled())
|
|
256
|
-
ux.action.stop();
|
|
257
|
-
this.printResults(`Found ${results.length} results from ${siteModule.name}:\n`, results);
|
|
258
|
-
return results.map((r) => ({ ...r, site_module_id: siteModule.id }));
|
|
259
|
-
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
if (!this.jsonEnabled())
|
|
262
|
-
ux.action.stop('failed');
|
|
263
|
-
throw err;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
// Remote docs discovery. On any connector failure, degrade to local cache search with a warning
|
|
267
|
-
// (T-20). Returns discovery results tagged remote: true so callers can tell them from cache hits.
|
|
268
|
-
async executeRemoteSearch(query, docsUrl) {
|
|
269
|
-
try {
|
|
270
|
-
if (!this.jsonEnabled())
|
|
271
|
-
ux.action.start(`Searching ${docsUrl}`);
|
|
272
|
-
const { provider, results } = await runRemoteDocsSearch(docsUrl, query, REMOTE_SEARCH_DEPS);
|
|
273
|
-
if (!this.jsonEnabled())
|
|
274
|
-
ux.action.stop();
|
|
275
|
-
this.printResults(`Found ${results.length} remote results via ${provider}:\n`, results);
|
|
276
|
-
return results.map((r) => ({ ...r, remote: true }));
|
|
277
|
-
}
|
|
278
|
-
catch (err) {
|
|
279
|
-
if (!this.jsonEnabled())
|
|
280
|
-
ux.action.stop('failed');
|
|
281
|
-
this.warn(`Remote docs search unavailable, using local cache: ${err.message}`);
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
async run() {
|
|
286
|
-
const { query } = this.args;
|
|
287
|
-
const { domain, remote } = this.flags;
|
|
288
|
-
// Reject whitespace-only queries on every path (local, --domain, --remote).
|
|
289
|
-
this.getSearchQueryTerms(query);
|
|
290
|
-
if (domain && remote) {
|
|
291
|
-
this.error('--domain and --remote are mutually exclusive. Use one discovery mode at a time.', {
|
|
292
|
-
exit: 2,
|
|
293
|
-
code: 'CONFLICTING_FLAGS',
|
|
294
|
-
suggestions: [
|
|
295
|
-
`Local site API: ${this.config.bin} search "${query}" --domain ${domain}`,
|
|
296
|
-
`Remote index: ${this.config.bin} search "${query}" --remote ${remote}`,
|
|
297
|
-
],
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
if (domain) {
|
|
301
|
-
return this.executeSiteSearch(query, domain);
|
|
302
|
-
}
|
|
303
|
-
if (remote) {
|
|
304
|
-
let normalizedRemote;
|
|
305
|
-
try {
|
|
306
|
-
normalizedRemote = normalizeUrl(remote);
|
|
307
|
-
}
|
|
308
|
-
catch (err) {
|
|
309
|
-
this.error(`Invalid --remote URL: ${err.message}`, {
|
|
310
|
-
exit: 2,
|
|
311
|
-
code: 'INVALID_URL',
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
const remoteResults = await this.executeRemoteSearch(query, normalizedRemote);
|
|
315
|
-
if (remoteResults !== undefined)
|
|
316
|
-
return remoteResults;
|
|
317
|
-
// else: fall through to local cache search (connector/network failure only)
|
|
318
|
-
}
|
|
319
|
-
this.validateSearchFlags();
|
|
320
|
-
const queryTerms = this.getSearchQueryTerms(query);
|
|
321
|
-
const roots = loadStoreRoots({
|
|
322
|
-
configDir: this.config.configDir,
|
|
323
|
-
cwd: process.cwd(),
|
|
324
|
-
dataDir: this.config.dataDir,
|
|
325
|
-
});
|
|
326
|
-
const currentTime = new Date();
|
|
327
|
-
const results = this.scanCacheDirForResults(roots.readRoots, queryTerms, currentTime);
|
|
328
|
-
results.sort((a, b) => b.score - a.score);
|
|
329
|
-
const finalResults = results.slice(0, this.flags.limit);
|
|
330
|
-
// Under --json the human heading is suppressed, so surface truncation on stderr instead (warn
|
|
331
|
-
// always emits, even in --json) without touching the stdout envelope. Human mode already shows
|
|
332
|
-
// it in the heading, so only warn under --json.
|
|
333
|
-
const notice = truncationNotice(results.length, finalResults.length, SEARCH_LABELS);
|
|
334
|
-
if (notice && this.jsonEnabled())
|
|
335
|
-
this.warn(notice);
|
|
336
|
-
this.logSearchResults(finalResults, results.length);
|
|
337
|
-
return finalResults;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
function scoreSingleTerm(term, topic, tags, sourceUrl, sourceUrls, summ, comp) {
|
|
341
|
-
let termScore = 0;
|
|
342
|
-
let termMatched = false;
|
|
343
|
-
// 1. Topic Match
|
|
344
|
-
if (topic === term) {
|
|
345
|
-
termScore += 100;
|
|
346
|
-
termMatched = true;
|
|
347
|
-
}
|
|
348
|
-
else if (topic.includes(term)) {
|
|
349
|
-
termScore += 60;
|
|
350
|
-
termMatched = true;
|
|
351
|
-
}
|
|
352
|
-
else if (isFuzzyMatch(term, topic)) {
|
|
353
|
-
termScore += 40;
|
|
354
|
-
termMatched = true;
|
|
355
|
-
}
|
|
356
|
-
// 2. Tags Match
|
|
357
|
-
if (tags.includes(term)) {
|
|
358
|
-
termScore += 80;
|
|
359
|
-
termMatched = true;
|
|
360
|
-
}
|
|
361
|
-
else if (tags.some((t) => isFuzzyMatch(term, t) || t.includes(term))) {
|
|
362
|
-
termScore += 30;
|
|
363
|
-
termMatched = true;
|
|
364
|
-
}
|
|
365
|
-
// 3. Source URL Match
|
|
366
|
-
if (sourceUrl.includes(term) || sourceUrls.some((u) => u.includes(term))) {
|
|
367
|
-
termScore += 50;
|
|
368
|
-
termMatched = true;
|
|
369
|
-
}
|
|
370
|
-
// 4. Summary Term Frequency Match
|
|
371
|
-
const summMatches = summ.split(term).length - 1;
|
|
372
|
-
if (summMatches > 0) {
|
|
373
|
-
termScore += 20 + Math.min(summMatches, 5) * 5;
|
|
374
|
-
termMatched = true;
|
|
375
|
-
}
|
|
376
|
-
// 5. Compressed Content Term Frequency Match
|
|
377
|
-
const compMatches = comp.split(term).length - 1;
|
|
378
|
-
if (compMatches > 0) {
|
|
379
|
-
termScore += 5 + Math.min(compMatches, 10) * 2;
|
|
380
|
-
termMatched = true;
|
|
381
|
-
}
|
|
382
|
-
if (termMatched) {
|
|
383
|
-
termScore += 10;
|
|
384
|
-
}
|
|
385
|
-
return termScore;
|
|
386
|
-
}
|
|
387
|
-
function isFuzzyMatch(term, target) {
|
|
388
|
-
if (term.length < 4 || target.length < 4)
|
|
389
|
-
return false;
|
|
390
|
-
return levenshtein(term, target) <= 2;
|
|
391
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
Search.setIndex({ "docnames": ["index", "library/functions", "library/string"], "filenames": ["index.rst", "library/functions.rst", "library/string.rst"], "titles": ["Python Documentation", "Built-in Functions", "String Methods"], "terms": { "function": [1], "string": [2], "print": [1], "format": [2] }, "titleterms": { "function": [1], "string": [2], "python": [0], "built": [1], "methods": [2] }, "objects": {}, "envversion": {} });
|
|
2
|
-
export {};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { DocsSearchResult } from './search-index.js';
|
|
2
|
-
interface DiscoveryEntry {
|
|
3
|
-
title: string;
|
|
4
|
-
url: string;
|
|
5
|
-
body?: string;
|
|
6
|
-
}
|
|
7
|
-
export interface SitemapParseResult {
|
|
8
|
-
entries: DiscoveryEntry[];
|
|
9
|
-
sitemapUrls: string[];
|
|
10
|
-
}
|
|
11
|
-
export declare function parseSitemap(body: string): SitemapParseResult;
|
|
12
|
-
export declare function searchSitemap(body: string, indexUrl: string, query: string): DocsSearchResult[];
|
|
13
|
-
export declare function searchLlmsTxt(body: string, indexUrl: string, query: string): DocsSearchResult[];
|
|
14
|
-
export {};
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { DOMParser } from 'linkedom';
|
|
2
|
-
function queryTerms(query) {
|
|
3
|
-
return query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
4
|
-
}
|
|
5
|
-
function scoreEntry(entry, terms) {
|
|
6
|
-
const haystack = `${entry.title} ${entry.url} ${entry.body ?? ''}`.toLowerCase();
|
|
7
|
-
let score = 0;
|
|
8
|
-
for (const term of terms) {
|
|
9
|
-
if (entry.title.toLowerCase().includes(term))
|
|
10
|
-
score += 10;
|
|
11
|
-
if (entry.url.toLowerCase().includes(term))
|
|
12
|
-
score += 5;
|
|
13
|
-
score += Math.min(haystack.split(term).length - 1, 5);
|
|
14
|
-
}
|
|
15
|
-
return score;
|
|
16
|
-
}
|
|
17
|
-
function snippetFrom(entry) {
|
|
18
|
-
return (entry.body || entry.url).replace(/\s+/g, ' ').trim().slice(0, 150);
|
|
19
|
-
}
|
|
20
|
-
function titleFromUrl(url) {
|
|
21
|
-
try {
|
|
22
|
-
const parsed = new URL(url);
|
|
23
|
-
const last = parsed.pathname.split('/').filter(Boolean).pop();
|
|
24
|
-
return last ? last.replace(/\.[a-z0-9]+$/i, '').replace(/[-_]+/g, ' ') : parsed.hostname;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return url;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function toResults(entries, indexUrl, provider, query) {
|
|
31
|
-
const terms = queryTerms(query);
|
|
32
|
-
return entries
|
|
33
|
-
.map((entry) => ({ entry, score: scoreEntry(entry, terms) }))
|
|
34
|
-
.filter(({ score }) => score > 0)
|
|
35
|
-
.map(({ entry, score }) => ({
|
|
36
|
-
title: entry.title,
|
|
37
|
-
url: entry.url,
|
|
38
|
-
snippet: snippetFrom(entry),
|
|
39
|
-
provider,
|
|
40
|
-
score,
|
|
41
|
-
indexUrl,
|
|
42
|
-
}))
|
|
43
|
-
.sort((a, b) => b.score - a.score);
|
|
44
|
-
}
|
|
45
|
-
function textContent(element) {
|
|
46
|
-
const text = element?.textContent?.trim();
|
|
47
|
-
return text || undefined;
|
|
48
|
-
}
|
|
49
|
-
export function parseSitemap(body) {
|
|
50
|
-
const trimmed = body.trim();
|
|
51
|
-
if (!trimmed)
|
|
52
|
-
return { entries: [], sitemapUrls: [] };
|
|
53
|
-
if (!trimmed.startsWith('<')) {
|
|
54
|
-
const entries = trimmed
|
|
55
|
-
.split(/\r?\n/)
|
|
56
|
-
.map((line) => line.trim())
|
|
57
|
-
.filter((line) => /^https?:\/\//i.test(line))
|
|
58
|
-
.map((url) => ({ title: titleFromUrl(url), url }));
|
|
59
|
-
return { entries, sitemapUrls: [] };
|
|
60
|
-
}
|
|
61
|
-
const doc = new DOMParser().parseFromString(trimmed, 'text/xml');
|
|
62
|
-
const sitemapUrls = Array.from(doc.querySelectorAll('sitemap > loc'))
|
|
63
|
-
.map((loc) => textContent(loc))
|
|
64
|
-
.filter((url) => !!url);
|
|
65
|
-
const entries = Array.from(doc.querySelectorAll('url'))
|
|
66
|
-
.map((urlElement) => {
|
|
67
|
-
const url = textContent(urlElement.querySelector('loc'));
|
|
68
|
-
if (!url)
|
|
69
|
-
return null;
|
|
70
|
-
const lastmod = textContent(urlElement.querySelector('lastmod'));
|
|
71
|
-
return { title: titleFromUrl(url), url, body: lastmod ? `Last modified ${lastmod}` : '' };
|
|
72
|
-
})
|
|
73
|
-
.filter((entry) => !!entry);
|
|
74
|
-
return { entries, sitemapUrls };
|
|
75
|
-
}
|
|
76
|
-
export function searchSitemap(body, indexUrl, query) {
|
|
77
|
-
return toResults(parseSitemap(body).entries, indexUrl, 'sitemap', query);
|
|
78
|
-
}
|
|
79
|
-
function referenceDefinitions(body) {
|
|
80
|
-
const refs = new Map();
|
|
81
|
-
for (const match of body.matchAll(/^\s*\[([^\]]+)\]:\s*(\S+)/gm)) {
|
|
82
|
-
refs.set(match[1].toLowerCase(), match[2]);
|
|
83
|
-
}
|
|
84
|
-
return refs;
|
|
85
|
-
}
|
|
86
|
-
export function searchLlmsTxt(body, indexUrl, query) {
|
|
87
|
-
const refs = referenceDefinitions(body);
|
|
88
|
-
const entries = [];
|
|
89
|
-
let section = '';
|
|
90
|
-
for (const line of body.split(/\r?\n/)) {
|
|
91
|
-
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
92
|
-
if (heading)
|
|
93
|
-
section = heading[1];
|
|
94
|
-
const inline = line.match(/^\s*[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*:?\s*(.*)$/);
|
|
95
|
-
const ref = line.match(/^\s*[-*]\s+\[([^\]]+)\]\s*:?\s*(.*)$/);
|
|
96
|
-
const title = inline?.[1] ?? ref?.[1];
|
|
97
|
-
const url = inline?.[2] ?? (title ? refs.get(title.toLowerCase()) : undefined);
|
|
98
|
-
if (!title || !url)
|
|
99
|
-
continue;
|
|
100
|
-
try {
|
|
101
|
-
entries.push({
|
|
102
|
-
title,
|
|
103
|
-
url: new URL(url, indexUrl).toString(),
|
|
104
|
-
body: [section, inline?.[3] ?? ref?.[2]].filter(Boolean).join(' - '),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
// Ignore malformed links in untrusted machine-readable text.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return toResults(entries, indexUrl, 'llms.txt', query);
|
|
112
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { DocsSearchResult } from './search-index.js';
|
|
2
|
-
export interface RemoteSearchDeps {
|
|
3
|
-
fetchStatic: (url: string) => Promise<{
|
|
4
|
-
content: string;
|
|
5
|
-
finalUrl: string;
|
|
6
|
-
}>;
|
|
7
|
-
fetchText: (url: string) => Promise<{
|
|
8
|
-
content: string;
|
|
9
|
-
status: number;
|
|
10
|
-
}>;
|
|
11
|
-
postJson: (url: string, body: unknown, headers: Record<string, string>) => Promise<string>;
|
|
12
|
-
}
|
|
13
|
-
export interface RemoteSearchOutcome {
|
|
14
|
-
provider: string;
|
|
15
|
-
results: DocsSearchResult[];
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Runs remote docs search for a documentation site. Detects the provider from the page, queries the
|
|
19
|
-
* matching connector, and returns normalized results. Throws when no verified connector applies.
|
|
20
|
-
*/
|
|
21
|
-
export declare function runRemoteDocsSearch(docsUrl: string, query: string, deps: RemoteSearchDeps): Promise<RemoteSearchOutcome>;
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { detectDocsEngine } from './detect.js';
|
|
2
|
-
import { probeLlmsTxt } from './machine-readable.js';
|
|
3
|
-
import { searchMkDocsIndex, searchJekyllIndex, searchSphinxIndex } from './search-index.js';
|
|
4
|
-
import { parseSitemap, searchLlmsTxt, searchSitemap } from './discovery-index.js';
|
|
5
|
-
import { extractAlgoliaConfig, algoliaQueryUrl, parseAlgoliaResponse } from './remote-search.js';
|
|
6
|
-
async function getText(url, deps) {
|
|
7
|
-
const res = await deps.fetchText(url);
|
|
8
|
-
if (res.status === 304 || !res.content)
|
|
9
|
-
throw new Error(`empty search index at ${url}`);
|
|
10
|
-
return res.content;
|
|
11
|
-
}
|
|
12
|
-
function sameHost(url, base) {
|
|
13
|
-
try {
|
|
14
|
-
return new URL(url).hostname === new URL(base).hostname;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
// Conventional static index path + parser per framework (verified by T-25 fixtures).
|
|
21
|
-
async function staticIndexSearch(framework, origin, query, deps) {
|
|
22
|
-
if (framework === 'mkdocs' || framework === 'material-mkdocs') {
|
|
23
|
-
const indexUrl = `${origin}/search/search_index.json`;
|
|
24
|
-
return {
|
|
25
|
-
provider: 'mkdocs-local',
|
|
26
|
-
results: searchMkDocsIndex(await getText(indexUrl, deps), indexUrl, query),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
if (framework === 'sphinx') {
|
|
30
|
-
const indexUrl = `${origin}/searchindex.js`;
|
|
31
|
-
return {
|
|
32
|
-
provider: 'sphinx-searchindex',
|
|
33
|
-
results: searchSphinxIndex(await getText(indexUrl, deps), indexUrl, query),
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
if (framework === 'just-the-docs') {
|
|
37
|
-
const indexUrl = `${origin}/assets/js/search-data.json`;
|
|
38
|
-
return {
|
|
39
|
-
provider: 'jekyll-json',
|
|
40
|
-
results: searchJekyllIndex(await getText(indexUrl, deps), indexUrl, query),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
async function llmsTxtSearch(docsUrl, caps, query, deps) {
|
|
46
|
-
const llms = await probeLlmsTxt(docsUrl, caps, async (url) => {
|
|
47
|
-
const res = await deps.fetchText(url);
|
|
48
|
-
return { ...res, finalUrl: url, contentType: 'text/plain' };
|
|
49
|
-
});
|
|
50
|
-
if (!llms)
|
|
51
|
-
return null;
|
|
52
|
-
return {
|
|
53
|
-
provider: llms.artifact.type,
|
|
54
|
-
results: searchLlmsTxt(llms.body, llms.artifact.url, query),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
async function sitemapSearch(pageUrl, query, deps) {
|
|
58
|
-
const origin = new URL(pageUrl).origin;
|
|
59
|
-
const firstSegment = new URL(pageUrl).pathname.split('/').filter(Boolean)[0];
|
|
60
|
-
const candidates = [
|
|
61
|
-
`${origin}/sitemap.xml`,
|
|
62
|
-
`${origin}/sitemap_index.xml`,
|
|
63
|
-
firstSegment ? `${origin}/${firstSegment}/sitemap.xml` : undefined,
|
|
64
|
-
].filter((url) => !!url);
|
|
65
|
-
for (const sitemapUrl of [...new Set(candidates)]) {
|
|
66
|
-
try {
|
|
67
|
-
const body = await getText(sitemapUrl, deps);
|
|
68
|
-
const results = searchSitemap(body, sitemapUrl, query);
|
|
69
|
-
if (results.length > 0)
|
|
70
|
-
return { provider: 'sitemap', results };
|
|
71
|
-
const index = parseSitemap(body);
|
|
72
|
-
const childResults = [];
|
|
73
|
-
for (const childUrl of index.sitemapUrls.slice(0, 10)) {
|
|
74
|
-
if (!sameHost(childUrl, pageUrl))
|
|
75
|
-
continue;
|
|
76
|
-
const childBody = await getText(childUrl, deps);
|
|
77
|
-
childResults.push(...searchSitemap(childBody, childUrl, query));
|
|
78
|
-
}
|
|
79
|
-
if (childResults.length > 0) {
|
|
80
|
-
return { provider: 'sitemap', results: childResults.sort((a, b) => b.score - a.score) };
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// Try the next conventional sitemap location.
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Runs remote docs search for a documentation site. Detects the provider from the page, queries the
|
|
91
|
-
* matching connector, and returns normalized results. Throws when no verified connector applies.
|
|
92
|
-
*/
|
|
93
|
-
export async function runRemoteDocsSearch(docsUrl, query, deps) {
|
|
94
|
-
const page = await deps.fetchStatic(docsUrl);
|
|
95
|
-
const origin = new URL(page.finalUrl).origin;
|
|
96
|
-
const algolia = extractAlgoliaConfig(page.content);
|
|
97
|
-
if (algolia) {
|
|
98
|
-
const url = algoliaQueryUrl(algolia);
|
|
99
|
-
const body = await deps.postJson(url, { params: `query=${encodeURIComponent(query)}` }, { 'X-Algolia-API-Key': algolia.apiKey, 'X-Algolia-Application-Id': algolia.appId });
|
|
100
|
-
return { provider: 'algolia-docsearch', results: parseAlgoliaResponse(body, url) };
|
|
101
|
-
}
|
|
102
|
-
const caps = detectDocsEngine(page.content, page.finalUrl);
|
|
103
|
-
const staticResult = await staticIndexSearch(caps.framework, origin, query, deps);
|
|
104
|
-
if (staticResult)
|
|
105
|
-
return staticResult;
|
|
106
|
-
const llmsResult = await llmsTxtSearch(page.finalUrl, caps, query, deps);
|
|
107
|
-
if (llmsResult)
|
|
108
|
-
return llmsResult;
|
|
109
|
-
const sitemapResult = await sitemapSearch(page.finalUrl, query, deps);
|
|
110
|
-
if (sitemapResult)
|
|
111
|
-
return sitemapResult;
|
|
112
|
-
throw new Error(`No verified remote search connector for this site (detected: ${caps.framework ?? caps.docsEngine ?? 'unknown'}). ` +
|
|
113
|
-
'Supported: Algolia DocSearch, MkDocs/Material, Sphinx, Just the Docs, llms.txt, sitemap.xml.');
|
|
114
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { DocsSearchResult } from './search-index.js';
|
|
2
|
-
export interface AlgoliaConfig {
|
|
3
|
-
appId: string;
|
|
4
|
-
apiKey: string;
|
|
5
|
-
indexName: string;
|
|
6
|
-
}
|
|
7
|
-
export declare function extractAlgoliaConfig(html: string): AlgoliaConfig | null;
|
|
8
|
-
export declare function algoliaQueryUrl(config: AlgoliaConfig): string;
|
|
9
|
-
export declare function parseAlgoliaResponse(body: string, indexUrl: string): DocsSearchResult[];
|
|
10
|
-
/**
|
|
11
|
-
* Parses a VitePress local search index (MiniSearch documents) and ranks docs by query term hits.
|
|
12
|
-
* `base` is the docs site origin/base used to resolve the relative `id` routes.
|
|
13
|
-
*/
|
|
14
|
-
export declare function searchVitePressLocalIndex(body: string, indexUrl: string, base: string, query: string): DocsSearchResult[];
|