@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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
const REDACTION = '[Removed potentially unsafe agent instruction]';
|
|
3
|
+
const ZERO_WIDTH = /[\u200b-\u200f\ufeff]/g;
|
|
4
|
+
const HTML_COMMENT = /<!--[\s\S]*?-->/g;
|
|
5
|
+
const BASE64_TOKEN = /(?<![A-Za-z0-9+/_-])(?:[A-Za-z0-9+/_-]{16,}={0,2})(?![A-Za-z0-9+/_-])/g;
|
|
6
|
+
const HEX_TOKEN = /(?<![a-f0-9])(?:0x)?[a-f0-9]{24,}(?![a-f0-9])/gi;
|
|
7
|
+
const QUOTED_SPAN = /(`[^`\n]+`|"[^"\n]+"|'[^'\n]+')/g;
|
|
8
|
+
const TYPOGLYCEMIA_TARGETS = [
|
|
9
|
+
'ignore',
|
|
10
|
+
'disregard',
|
|
11
|
+
'forget',
|
|
12
|
+
'override',
|
|
13
|
+
'previous',
|
|
14
|
+
'instructions',
|
|
15
|
+
'instruction',
|
|
16
|
+
'directions',
|
|
17
|
+
'prompts',
|
|
18
|
+
'system',
|
|
19
|
+
'developer',
|
|
20
|
+
'agent',
|
|
21
|
+
'prompt',
|
|
22
|
+
'message',
|
|
23
|
+
'reveal',
|
|
24
|
+
'print',
|
|
25
|
+
'send',
|
|
26
|
+
'secrets',
|
|
27
|
+
'tokens',
|
|
28
|
+
'credentials',
|
|
29
|
+
'delete',
|
|
30
|
+
'repository',
|
|
31
|
+
'workspace',
|
|
32
|
+
];
|
|
33
|
+
const HARMFUL_INSTRUCTION_PATTERNS = [
|
|
34
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)ignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|directions?|prompts?)\b/i,
|
|
35
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)disregard\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|directions?|prompts?)\b/i,
|
|
36
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)forget\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|directions?|prompts?)\b/i,
|
|
37
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)override\s+(the\s+)?(system|developer|agent)\s+(prompt|instructions?|message)\b/i,
|
|
38
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)you\s+are\s+now\s+(in\s+)?(developer|admin|root|system)\s+mode\b/i,
|
|
39
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)reveal\s+(your\s+)?(system|developer)\s+(prompt|instructions?|message)\b/i,
|
|
40
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)print\s+(your\s+)?(system|developer)\s+(prompt|instructions?|message)\b/i,
|
|
41
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)(exfiltrate|steal|leak)\s+(all\s+|the\s+|your\s+)?(secrets?|tokens?|api\s*keys?|credentials?|private\s+data)\b/i,
|
|
42
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)send\s+(me\s+|all\s+|the\s+|your\s+)?(secrets?|tokens?|api\s*keys?|credentials?)\s+(to|over|via|using)\b/i,
|
|
43
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)(upload|post|fetch|curl)\s+(all\s+|the\s+|your\s+)?(secrets?|tokens?|api\s*keys?|credentials?)\s+(to|over|via|using)\b/i,
|
|
44
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)delete\s+(all\s+)?(files?|the\s+repository|the\s+workspace|the\s+home\s+directory)\b/i,
|
|
45
|
+
/(^|\b(?:please|assistant|system|user|agent)\s+)run\s+.*\b(rm\s+-rf|curl\s+.*\|\s*(sh|bash)|sudo)\b/i,
|
|
46
|
+
];
|
|
47
|
+
function normalizeForDetection(text) {
|
|
48
|
+
return text
|
|
49
|
+
.normalize('NFKC')
|
|
50
|
+
.replace(ZERO_WIDTH, '')
|
|
51
|
+
.replace(/[^\p{L}\p{N}\s|/.-]+/gu, ' ')
|
|
52
|
+
.replace(/\s+/g, ' ')
|
|
53
|
+
.trim()
|
|
54
|
+
.split(' ')
|
|
55
|
+
.map(normalizeTypoglycemia)
|
|
56
|
+
.join(' ');
|
|
57
|
+
}
|
|
58
|
+
function normalizeTypoglycemia(word) {
|
|
59
|
+
const lower = word.toLowerCase();
|
|
60
|
+
return TYPOGLYCEMIA_TARGETS.find((target) => isTypoglycemiaMatch(lower, target)) ?? word;
|
|
61
|
+
}
|
|
62
|
+
function isTypoglycemiaMatch(word, target) {
|
|
63
|
+
if (word.length !== target.length || word.length < 4)
|
|
64
|
+
return false;
|
|
65
|
+
if (word[0] !== target[0] || word.at(-1) !== target.at(-1))
|
|
66
|
+
return false;
|
|
67
|
+
return sortLetters(word.slice(1, -1)) === sortLetters(target.slice(1, -1));
|
|
68
|
+
}
|
|
69
|
+
function sortLetters(value) {
|
|
70
|
+
return [...value].sort().join('');
|
|
71
|
+
}
|
|
72
|
+
function detectionCandidates(text) {
|
|
73
|
+
return [text, ...decodeBase64Tokens(text), ...decodeHexTokens(text)].map(normalizeForDetection);
|
|
74
|
+
}
|
|
75
|
+
function decodeBase64Tokens(text) {
|
|
76
|
+
return [...text.matchAll(BASE64_TOKEN)]
|
|
77
|
+
.map((match) => decodeBase64(match[0]))
|
|
78
|
+
.filter((decoded) => Boolean(decoded));
|
|
79
|
+
}
|
|
80
|
+
function decodeBase64(token) {
|
|
81
|
+
try {
|
|
82
|
+
const normalized = token.replace(/-/g, '+').replace(/_/g, '/');
|
|
83
|
+
const decoded = Buffer.from(normalized, 'base64').toString('utf8');
|
|
84
|
+
return isReadableDecodedText(decoded) ? decoded : null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function decodeHexTokens(text) {
|
|
91
|
+
return [...text.matchAll(HEX_TOKEN)]
|
|
92
|
+
.map((match) => decodeHex(match[0]))
|
|
93
|
+
.filter((decoded) => Boolean(decoded));
|
|
94
|
+
}
|
|
95
|
+
function decodeHex(token) {
|
|
96
|
+
const hex = token.startsWith('0x') ? token.slice(2) : token;
|
|
97
|
+
if (hex.length % 2 !== 0)
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
const decoded = Buffer.from(hex, 'hex').toString('utf8');
|
|
101
|
+
return isReadableDecodedText(decoded) ? decoded : null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function isReadableDecodedText(text) {
|
|
108
|
+
if (!/[a-z]{3}/i.test(text))
|
|
109
|
+
return false;
|
|
110
|
+
const printable = [...text].filter((char) => /[\t\n\r -~]/.test(char)).length;
|
|
111
|
+
return printable / Math.max(text.length, 1) > 0.85;
|
|
112
|
+
}
|
|
113
|
+
function isUnsafeAgentInstruction(text) {
|
|
114
|
+
return detectionCandidates(text).some((candidate) => HARMFUL_INSTRUCTION_PATTERNS.some((pattern) => pattern.test(candidate)));
|
|
115
|
+
}
|
|
116
|
+
function redactBlock(block) {
|
|
117
|
+
const withSafeSpans = redactUnsafeSpans(block);
|
|
118
|
+
return isUnsafeAgentInstruction(withSafeSpans) ? REDACTION : withSafeSpans;
|
|
119
|
+
}
|
|
120
|
+
function redactUnsafeSpans(block) {
|
|
121
|
+
return block
|
|
122
|
+
.replace(BASE64_TOKEN, (token) => redactTokenIfUnsafe(token, decodeBase64(token)))
|
|
123
|
+
.replace(HEX_TOKEN, (token) => redactTokenIfUnsafe(token, decodeHex(token)))
|
|
124
|
+
.replace(QUOTED_SPAN, (span) => isUnsafeAgentInstruction(stripQuotes(span)) ? REDACTION : span);
|
|
125
|
+
}
|
|
126
|
+
function redactTokenIfUnsafe(token, decoded) {
|
|
127
|
+
return decoded && isUnsafeAgentInstruction(decoded) ? REDACTION : token;
|
|
128
|
+
}
|
|
129
|
+
function stripQuotes(span) {
|
|
130
|
+
if (span.startsWith('`') || span.startsWith('"') || span.startsWith("'")) {
|
|
131
|
+
return span.slice(1, -1);
|
|
132
|
+
}
|
|
133
|
+
return span;
|
|
134
|
+
}
|
|
135
|
+
export function sanitizePromptInjection(markdown) {
|
|
136
|
+
return markdown
|
|
137
|
+
.replace(HTML_COMMENT, (comment) => redactBlock(comment))
|
|
138
|
+
.split(/(\n{2,})/)
|
|
139
|
+
.map((block) => {
|
|
140
|
+
if (/^\n+$/.test(block))
|
|
141
|
+
return block;
|
|
142
|
+
const lineRedacted = block
|
|
143
|
+
.split('\n')
|
|
144
|
+
.map((line) => redactBlock(line))
|
|
145
|
+
.join('\n');
|
|
146
|
+
return lineRedacted === block ? redactBlock(block) : lineRedacted;
|
|
147
|
+
})
|
|
148
|
+
.join('');
|
|
149
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Dispatcher } from 'undici';
|
|
2
|
+
export declare const HTTPS_PROXY_ENV_VARS: string[];
|
|
3
|
+
export declare const HTTP_PROXY_ENV_VARS: string[];
|
|
4
|
+
export declare const NO_PROXY_ENV_VARS: string[];
|
|
5
|
+
export declare const ALL_PROXY_ENV_VARS: string[];
|
|
6
|
+
export declare const PROXY_TUNNEL_REJECTION_PATTERN: RegExp;
|
|
7
|
+
/**
|
|
8
|
+
* Sandboxed execution environments (e.g. Claude Code's remote sandbox) block direct outbound
|
|
9
|
+
* connections to most hosts and instead require routing through a pre-configured HTTP(S) proxy,
|
|
10
|
+
* advertised via the standard *_PROXY env vars. Detecting it lets Bonsai transparently reach
|
|
11
|
+
* sites — like developer.salesforce.com — that are otherwise unreachable from within the sandbox.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isProxyConfigured(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* An undici Dispatcher that honors HTTPS_PROXY/HTTP_PROXY/NO_PROXY, or undefined when no proxy
|
|
16
|
+
* is configured so callers fall back to normal direct networking. The dispatcher is expensive
|
|
17
|
+
* enough to be worth reusing across requests, but undici's own agent captures its proxy targets
|
|
18
|
+
* once at construction time and never re-reads them — so the cache is keyed on a snapshot of the
|
|
19
|
+
* relevant env vars and rebuilt whenever that snapshot changes, instead of being pinned forever
|
|
20
|
+
* to whatever was configured on the first call. The discarded dispatcher is closed (not just
|
|
21
|
+
* dropped) so its pooled keep-alive sockets don't leak past the env change that replaced it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getProxyDispatcher(): Dispatcher | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Chrome CLI flags that route headless navigation through the same proxy as fetch(), or an
|
|
26
|
+
* empty array when no proxy is configured. Chrome has its own network stack (CDP navigation
|
|
27
|
+
* never goes through Node's fetch/undici), so it needs to be told about the proxy separately.
|
|
28
|
+
*
|
|
29
|
+
* Built as a scheme-qualified `--proxy-server` value (`http=...;https=...`) rather than one
|
|
30
|
+
* blanket proxy for every scheme, matching undici's EnvHttpProxyAgent, which picks HTTP_PROXY or
|
|
31
|
+
* HTTPS_PROXY per the target URL's scheme. A single unqualified value would force Chrome to
|
|
32
|
+
* route all its traffic (including plain-http sub-resources) through whichever var happened to
|
|
33
|
+
* be set, silently disagreeing with how fetch() routes the same traffic.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getChromeProxyArgs(): string[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { EnvHttpProxyAgent } from 'undici';
|
|
2
|
+
// Order matters: undici's own EnvHttpProxyAgent (github.com/nodejs/undici lib/dispatcher/
|
|
3
|
+
// env-http-proxy-agent.js) prefers the lowercase variant of each var over its uppercase twin
|
|
4
|
+
// when both are set. These lists are read in the same order everywhere in this module so that
|
|
5
|
+
// Chrome's CLI flags and undici's dispatcher can never disagree about which value "the" proxy is.
|
|
6
|
+
export const HTTPS_PROXY_ENV_VARS = ['https_proxy', 'HTTPS_PROXY'];
|
|
7
|
+
export const HTTP_PROXY_ENV_VARS = ['http_proxy', 'HTTP_PROXY'];
|
|
8
|
+
export const NO_PROXY_ENV_VARS = ['no_proxy', 'NO_PROXY'];
|
|
9
|
+
export const ALL_PROXY_ENV_VARS = [
|
|
10
|
+
...HTTPS_PROXY_ENV_VARS,
|
|
11
|
+
...HTTP_PROXY_ENV_VARS,
|
|
12
|
+
...NO_PROXY_ENV_VARS,
|
|
13
|
+
];
|
|
14
|
+
// The message undici's ProxyAgent throws (nested in a fetch TypeError's `.cause`) when the proxy
|
|
15
|
+
// itself declines to tunnel to the target host — as opposed to any other pre-response transport
|
|
16
|
+
// failure (DNS, refused connection, a genuinely unreachable destination). Shared by fetcher.ts
|
|
17
|
+
// (to decide whether a proxied failure is worth retrying direct) and fetch.ts's failure-guidance
|
|
18
|
+
// patterns (to give proxy-specific advice), so the two stay in sync by construction.
|
|
19
|
+
export const PROXY_TUNNEL_REJECTION_PATTERN = /proxy response.*tunneling/i;
|
|
20
|
+
function firstEnv(names) {
|
|
21
|
+
for (const name of names) {
|
|
22
|
+
const value = process.env[name];
|
|
23
|
+
if (value)
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Sandboxed execution environments (e.g. Claude Code's remote sandbox) block direct outbound
|
|
30
|
+
* connections to most hosts and instead require routing through a pre-configured HTTP(S) proxy,
|
|
31
|
+
* advertised via the standard *_PROXY env vars. Detecting it lets Bonsai transparently reach
|
|
32
|
+
* sites — like developer.salesforce.com — that are otherwise unreachable from within the sandbox.
|
|
33
|
+
*/
|
|
34
|
+
export function isProxyConfigured() {
|
|
35
|
+
return Boolean(firstEnv(HTTPS_PROXY_ENV_VARS) || firstEnv(HTTP_PROXY_ENV_VARS));
|
|
36
|
+
}
|
|
37
|
+
let cachedDispatcher;
|
|
38
|
+
let cachedProxyEnvSnapshot;
|
|
39
|
+
// JSON-encoded rather than joined with a plain delimiter: a delimiter that can also appear inside
|
|
40
|
+
// a value (e.g. a space, and NO_PROXY conventionally uses "host1, host2") lets two different sets
|
|
41
|
+
// of values collide on the same joined string. JSON.stringify's per-element quoting/escaping is a
|
|
42
|
+
// well-tested way to avoid that ambiguity without hand-rolling an escape scheme.
|
|
43
|
+
function proxyEnvSnapshot() {
|
|
44
|
+
return JSON.stringify(ALL_PROXY_ENV_VARS.map((name) => process.env[name] ?? ''));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* An undici Dispatcher that honors HTTPS_PROXY/HTTP_PROXY/NO_PROXY, or undefined when no proxy
|
|
48
|
+
* is configured so callers fall back to normal direct networking. The dispatcher is expensive
|
|
49
|
+
* enough to be worth reusing across requests, but undici's own agent captures its proxy targets
|
|
50
|
+
* once at construction time and never re-reads them — so the cache is keyed on a snapshot of the
|
|
51
|
+
* relevant env vars and rebuilt whenever that snapshot changes, instead of being pinned forever
|
|
52
|
+
* to whatever was configured on the first call. The discarded dispatcher is closed (not just
|
|
53
|
+
* dropped) so its pooled keep-alive sockets don't leak past the env change that replaced it.
|
|
54
|
+
*/
|
|
55
|
+
export function getProxyDispatcher() {
|
|
56
|
+
if (!isProxyConfigured())
|
|
57
|
+
return undefined;
|
|
58
|
+
const snapshot = proxyEnvSnapshot();
|
|
59
|
+
if (!cachedDispatcher || cachedProxyEnvSnapshot !== snapshot) {
|
|
60
|
+
const stale = cachedDispatcher;
|
|
61
|
+
cachedDispatcher = new EnvHttpProxyAgent();
|
|
62
|
+
cachedProxyEnvSnapshot = snapshot;
|
|
63
|
+
void stale?.close().catch(() => { });
|
|
64
|
+
}
|
|
65
|
+
return cachedDispatcher;
|
|
66
|
+
}
|
|
67
|
+
// undici's EnvHttpProxyAgent treats a bare NO_PROXY entry (e.g. "example.com") as matching both
|
|
68
|
+
// the exact host AND any subdomain — it strips a leading "*." or "." before comparing, then
|
|
69
|
+
// checks the hostname for an exact or suffix match. Chromium's --proxy-bypass-list grammar draws
|
|
70
|
+
// a hard line instead: a bare "example.com" matches only that exact host, and subdomain matching
|
|
71
|
+
// requires an explicit "*." prefix. Emitting both forms for every entry reproduces undici's
|
|
72
|
+
// exact-or-subdomain behavior in Chrome too, so the two proxied paths agree on what bypasses.
|
|
73
|
+
function toChromeBypassList(noProxy) {
|
|
74
|
+
const hosts = noProxy
|
|
75
|
+
.split(/[,\s]+/)
|
|
76
|
+
.map((entry) => entry.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map((entry) => entry.replace(/^\*?\./, ''));
|
|
79
|
+
return Array.from(new Set(hosts.flatMap((host) => [host, `*.${host}`]))).join(',');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Chrome CLI flags that route headless navigation through the same proxy as fetch(), or an
|
|
83
|
+
* empty array when no proxy is configured. Chrome has its own network stack (CDP navigation
|
|
84
|
+
* never goes through Node's fetch/undici), so it needs to be told about the proxy separately.
|
|
85
|
+
*
|
|
86
|
+
* Built as a scheme-qualified `--proxy-server` value (`http=...;https=...`) rather than one
|
|
87
|
+
* blanket proxy for every scheme, matching undici's EnvHttpProxyAgent, which picks HTTP_PROXY or
|
|
88
|
+
* HTTPS_PROXY per the target URL's scheme. A single unqualified value would force Chrome to
|
|
89
|
+
* route all its traffic (including plain-http sub-resources) through whichever var happened to
|
|
90
|
+
* be set, silently disagreeing with how fetch() routes the same traffic.
|
|
91
|
+
*/
|
|
92
|
+
export function getChromeProxyArgs() {
|
|
93
|
+
const httpsProxy = firstEnv(HTTPS_PROXY_ENV_VARS);
|
|
94
|
+
const httpProxy = firstEnv(HTTP_PROXY_ENV_VARS);
|
|
95
|
+
if (!httpsProxy && !httpProxy)
|
|
96
|
+
return [];
|
|
97
|
+
const schemeMappings = [];
|
|
98
|
+
if (httpProxy)
|
|
99
|
+
schemeMappings.push(`http=${httpProxy}`);
|
|
100
|
+
if (httpsProxy)
|
|
101
|
+
schemeMappings.push(`https=${httpsProxy}`);
|
|
102
|
+
const args = [`--proxy-server=${schemeMappings.join(';')}`];
|
|
103
|
+
const noProxy = firstEnv(NO_PROXY_ENV_VARS);
|
|
104
|
+
if (noProxy)
|
|
105
|
+
args.push(`--proxy-bypass-list=${toChromeBypassList(noProxy)}`);
|
|
106
|
+
return args;
|
|
107
|
+
}
|
|
@@ -15,6 +15,24 @@ export interface ContentMetrics {
|
|
|
15
15
|
bullets: number;
|
|
16
16
|
headings: number;
|
|
17
17
|
}
|
|
18
|
+
/** Maximum allowed capture-duration regression vs baseline (percent slower). */
|
|
19
|
+
export declare const TIMING_REGRESSION_LIMIT_PCT = 25;
|
|
20
|
+
export interface TimingRegression {
|
|
21
|
+
deltaPct: number | null;
|
|
22
|
+
regressed: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Returns whether `currentMs` exceeds baseline by more than TIMING_REGRESSION_LIMIT_PCT. */
|
|
25
|
+
export declare function timingRegression(currentMs: number, baselineMs: number | null | undefined): TimingRegression;
|
|
26
|
+
export interface TimingBaselineRecord {
|
|
27
|
+
durationMs: number;
|
|
28
|
+
capturedAt: string;
|
|
29
|
+
}
|
|
30
|
+
/** Parses a committed `baseline/<id>.timing.json` file. */
|
|
31
|
+
export declare function parseTimingBaseline(raw: string): TimingBaselineRecord | null;
|
|
32
|
+
/** Serializes a timing baseline record for `baseline/<id>.timing.json`. */
|
|
33
|
+
export declare function formatTimingBaseline(durationMs: number, capturedAt?: string): string;
|
|
34
|
+
/** True when `candidate` carries strictly more structural content than `baseline`. */
|
|
35
|
+
export declare function contentRicherThan(candidate: ContentMetrics, baseline: ContentMetrics): boolean;
|
|
18
36
|
/**
|
|
19
37
|
* Structural content signals used to catch information loss between baseline and current output:
|
|
20
38
|
* a sudden drop in code blocks, tables, images, or steps flags that a fetch change dropped content.
|
|
@@ -38,6 +38,53 @@ export function normalizeRegressionMarkdown(markdown) {
|
|
|
38
38
|
.replace(/\n{3,}/g, '\n\n')
|
|
39
39
|
.trim();
|
|
40
40
|
}
|
|
41
|
+
/** Maximum allowed capture-duration regression vs baseline (percent slower). */
|
|
42
|
+
export const TIMING_REGRESSION_LIMIT_PCT = 25;
|
|
43
|
+
/** Returns whether `currentMs` exceeds baseline by more than TIMING_REGRESSION_LIMIT_PCT. */
|
|
44
|
+
export function timingRegression(currentMs, baselineMs) {
|
|
45
|
+
if (!baselineMs || baselineMs <= 0)
|
|
46
|
+
return { deltaPct: null, regressed: false };
|
|
47
|
+
const deltaPct = Number((((currentMs - baselineMs) / baselineMs) * 100).toFixed(2));
|
|
48
|
+
return { deltaPct, regressed: deltaPct > TIMING_REGRESSION_LIMIT_PCT };
|
|
49
|
+
}
|
|
50
|
+
/** Parses a committed `baseline/<id>.timing.json` file. */
|
|
51
|
+
export function parseTimingBaseline(raw) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (typeof parsed.durationMs !== 'number' || parsed.durationMs <= 0)
|
|
55
|
+
return null;
|
|
56
|
+
return {
|
|
57
|
+
durationMs: parsed.durationMs,
|
|
58
|
+
capturedAt: typeof parsed.capturedAt === 'string' ? parsed.capturedAt : '',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Serializes a timing baseline record for `baseline/<id>.timing.json`. */
|
|
66
|
+
export function formatTimingBaseline(durationMs, capturedAt = new Date().toISOString()) {
|
|
67
|
+
return `${JSON.stringify({ durationMs, capturedAt }, null, 2)}\n`;
|
|
68
|
+
}
|
|
69
|
+
/** Structural signals compared before char count when picking DOM vs API Markdown. */
|
|
70
|
+
const CONTENT_RICHNESS_KEYS = [
|
|
71
|
+
'tables',
|
|
72
|
+
'codeBlocks',
|
|
73
|
+
'headings',
|
|
74
|
+
'images',
|
|
75
|
+
'orderedSteps',
|
|
76
|
+
'chars',
|
|
77
|
+
];
|
|
78
|
+
/** True when `candidate` carries strictly more structural content than `baseline`. */
|
|
79
|
+
export function contentRicherThan(candidate, baseline) {
|
|
80
|
+
for (const key of CONTENT_RICHNESS_KEYS) {
|
|
81
|
+
if (candidate[key] > baseline[key])
|
|
82
|
+
return true;
|
|
83
|
+
if (candidate[key] < baseline[key])
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
41
88
|
/**
|
|
42
89
|
* Structural content signals used to catch information loss between baseline and current output:
|
|
43
90
|
* a sudden drop in code blocks, tables, images, or steps flags that a fetch change dropped content.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ResearchArtifact } from './schema.js';
|
|
2
|
-
export declare function isResearchFile(file: string): boolean;
|
|
3
2
|
/**
|
|
4
3
|
* Scans a cache directory and maps each valid artifact file through a callback.
|
|
5
4
|
* Return null from the callback to skip an entry; all non-null results are collected.
|
|
@@ -2,12 +2,8 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, renameSync, } from 'node:fs';
|
|
3
3
|
import { atomicWriteFile } from '../atomic-write.js';
|
|
4
4
|
import { parseArtifact, serializeArtifact } from './artifact.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
!file.includes('.tmp') &&
|
|
8
|
-
!file.includes('.corrupt') &&
|
|
9
|
-
!file.includes('.superseded'));
|
|
10
|
-
}
|
|
5
|
+
import { loadIndexedArtifactsForDir } from './artifact-index.js';
|
|
6
|
+
import { isResearchFile } from './url.js';
|
|
11
7
|
/**
|
|
12
8
|
* Scans a cache directory and maps each valid artifact file through a callback.
|
|
13
9
|
* Return null from the callback to skip an entry; all non-null results are collected.
|
|
@@ -169,11 +165,17 @@ export function locateArtifact(dataDirs, key) {
|
|
|
169
165
|
*/
|
|
170
166
|
export function scanCacheDirs(dataDirs, fn) {
|
|
171
167
|
const seen = new Set();
|
|
172
|
-
return dataDirs.flatMap((dataDir) =>
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
168
|
+
return dataDirs.flatMap((dataDir) => {
|
|
169
|
+
const researchDir = join(dataDir, 'research');
|
|
170
|
+
const searchable = loadIndexedArtifactsForDir(researchDir);
|
|
171
|
+
return searchable
|
|
172
|
+
.map(({ artifact, filePath }) => {
|
|
173
|
+
const key = artifact.metadata.cache_key;
|
|
174
|
+
if (seen.has(key))
|
|
175
|
+
return null;
|
|
176
|
+
seen.add(key);
|
|
177
|
+
return fn(artifact, filePath);
|
|
178
|
+
})
|
|
179
|
+
.filter((x) => x !== null);
|
|
180
|
+
});
|
|
179
181
|
}
|
|
@@ -17,8 +17,30 @@ export declare function isSafeIp(ip: string): boolean;
|
|
|
17
17
|
* Validates a hostname to ensure it is not blocked (e.g. localhost or literal private IP).
|
|
18
18
|
*/
|
|
19
19
|
export declare function validateHostname(hostname: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the input looks like a URL that simply forgot its `http(s)://` scheme
|
|
22
|
+
* (`example.com`, `docs.nestjs.com/guide`, `192.168.1.1`) rather than unparseable junk.
|
|
23
|
+
* Lets every command turn a scheme-less URL into the same actionable "did you mean https://…" hint
|
|
24
|
+
* that the root `bonsai <url>` shorthand already gives via the command_not_found hook.
|
|
25
|
+
*/
|
|
26
|
+
export declare function looksLikeSchemelessUrl(input: string): boolean;
|
|
20
27
|
/**
|
|
21
28
|
* Parses and normalizes a URL. Strips fragments, credentials, default ports,
|
|
22
29
|
* and sorts query parameters lexicographically.
|
|
23
30
|
*/
|
|
24
31
|
export declare function normalizeUrl(input: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Checks if a URL matches a wildcard pattern (case-insensitive, supports * wildcard).
|
|
34
|
+
*/
|
|
35
|
+
export declare function matchesUrlGlob(url: string, glob: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Checks if artifact metadata contains any URL matching the filter glob.
|
|
38
|
+
*/
|
|
39
|
+
export declare function artifactMatchesUrlFilter(meta: {
|
|
40
|
+
source_urls?: string[];
|
|
41
|
+
source_url?: string;
|
|
42
|
+
}, filterUrl: string | undefined): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Checks if a filename matches the standard research note naming pattern.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isResearchFile(file: string): boolean;
|
package/dist/lib/research/url.js
CHANGED
|
@@ -83,6 +83,30 @@ export function validateHostname(hostname) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
// Dot-separated, non-empty labels: a real domain (`docs.nestjs.com`) or IPv4 (`192.168.1.1`). The
|
|
87
|
+
// URL parser also accepts a leading-dot filename (`.env`) or relative path (`./x`) as a "host", whose
|
|
88
|
+
// suggestion would be a nonsense `https://.env`; requiring well-formed labels rejects those. Anchored
|
|
89
|
+
// with no overlapping quantifiers, so it is ReDoS-safe on the (already length-bounded) host. This is
|
|
90
|
+
// the single owner of the domain-shape test; the command_not_found hook consumes it too.
|
|
91
|
+
const DOMAIN_OR_IPV4 = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i;
|
|
92
|
+
/**
|
|
93
|
+
* Whether the input looks like a URL that simply forgot its `http(s)://` scheme
|
|
94
|
+
* (`example.com`, `docs.nestjs.com/guide`, `192.168.1.1`) rather than unparseable junk.
|
|
95
|
+
* Lets every command turn a scheme-less URL into the same actionable "did you mean https://…" hint
|
|
96
|
+
* that the root `bonsai <url>` shorthand already gives via the command_not_found hook.
|
|
97
|
+
*/
|
|
98
|
+
export function looksLikeSchemelessUrl(input) {
|
|
99
|
+
if (input.includes('://'))
|
|
100
|
+
return false;
|
|
101
|
+
let hostname;
|
|
102
|
+
try {
|
|
103
|
+
hostname = new URL(`https://${input}`).hostname;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return DOMAIN_OR_IPV4.test(hostname);
|
|
109
|
+
}
|
|
86
110
|
/**
|
|
87
111
|
* Parses and normalizes a URL. Strips fragments, credentials, default ports,
|
|
88
112
|
* and sorts query parameters lexicographically.
|
|
@@ -95,7 +119,12 @@ export function normalizeUrl(input) {
|
|
|
95
119
|
catch {
|
|
96
120
|
// new URL() only ever throws the constant "Invalid URL" (no structured cause worth keeping),
|
|
97
121
|
// and every caller prefixes its own "Invalid URL:" — forwarding the native text would stutter.
|
|
98
|
-
//
|
|
122
|
+
// A scheme-less but domain-shaped input is the common "forgot https://" mistake, so steer the
|
|
123
|
+
// user to the corrected URL (clig.dev: show the fix, never auto-run it); otherwise emit a
|
|
124
|
+
// self-contained "couldn't parse" reason.
|
|
125
|
+
if (looksLikeSchemelessUrl(input)) {
|
|
126
|
+
throw new Error(`"${input}" is missing a URL scheme. Did you mean https://${input}? Only http:// and https:// URLs are supported.`);
|
|
127
|
+
}
|
|
99
128
|
throw new Error(`Could not parse "${input}" as a URL; provide a full http(s) URL like https://example.com/page.`);
|
|
100
129
|
}
|
|
101
130
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
@@ -122,3 +151,36 @@ export function normalizeUrl(input) {
|
|
|
122
151
|
url.search = new URLSearchParams(params).toString();
|
|
123
152
|
return url.toString();
|
|
124
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Checks if a URL matches a wildcard pattern (case-insensitive, supports * wildcard).
|
|
156
|
+
*/
|
|
157
|
+
export function matchesUrlGlob(url, glob) {
|
|
158
|
+
// Collapse consecutive wildcards to prevent ReDoS
|
|
159
|
+
const collapsedGlob = glob.replace(/\*+/g, '*');
|
|
160
|
+
const normalizedGlob = collapsedGlob.toLowerCase();
|
|
161
|
+
const normalizedUrl = url.toLowerCase();
|
|
162
|
+
// Escape regex characters except *
|
|
163
|
+
const escapedGlob = normalizedGlob.replace(/[/\-\\^$+?.()|[\]{}]/g, '\\$&');
|
|
164
|
+
// Replace * with .*
|
|
165
|
+
const regexString = '^' + escapedGlob.replace(/\*/g, '.*') + '$';
|
|
166
|
+
const regex = new RegExp(regexString);
|
|
167
|
+
return regex.test(normalizedUrl);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Checks if artifact metadata contains any URL matching the filter glob.
|
|
171
|
+
*/
|
|
172
|
+
export function artifactMatchesUrlFilter(meta, filterUrl) {
|
|
173
|
+
if (!filterUrl)
|
|
174
|
+
return true;
|
|
175
|
+
const urls = meta.source_urls || (meta.source_url ? [meta.source_url] : []);
|
|
176
|
+
return urls.some((u) => matchesUrlGlob(u, filterUrl));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Checks if a filename matches the standard research note naming pattern.
|
|
180
|
+
*/
|
|
181
|
+
export function isResearchFile(file) {
|
|
182
|
+
return (file.endsWith('.md') &&
|
|
183
|
+
!file.includes('.tmp') &&
|
|
184
|
+
!file.includes('.corrupt') &&
|
|
185
|
+
!file.includes('.superseded'));
|
|
186
|
+
}
|
package/dist/lib/text.d.ts
CHANGED
|
@@ -22,9 +22,11 @@ export declare function resultListHeading(total: number, shown: number, labels:
|
|
|
22
22
|
* stdout heading is suppressed and an agent would otherwise get no signal that results were capped.
|
|
23
23
|
*/
|
|
24
24
|
export declare function truncationNotice(total: number, shown: number, labels: ResultListLabels): string | null;
|
|
25
|
+
/** Max edit distance for a plausible typo; scales with input length. */
|
|
26
|
+
export declare function maxFuzzyDistance(input: string): number;
|
|
25
27
|
/**
|
|
26
28
|
* Levenshtein edit distance: the minimum number of single-character insertions, deletions, or
|
|
27
|
-
* substitutions to turn `s1` into `s2`. Shared by
|
|
29
|
+
* substitutions to turn `s1` into `s2`. Shared by command-typo suggestion.
|
|
28
30
|
*/
|
|
29
31
|
export declare function levenshtein(s1: string, s2: string): number;
|
|
30
32
|
/**
|
package/dist/lib/text.js
CHANGED
|
@@ -24,9 +24,17 @@ export function truncationNotice(total, shown, labels) {
|
|
|
24
24
|
return null;
|
|
25
25
|
return `${total} entries matched; returning the ${labels.order} ${shown}. Raise --limit (max ${labels.maxLimit}) to see more.`;
|
|
26
26
|
}
|
|
27
|
+
/** Max edit distance for a plausible typo; scales with input length. */
|
|
28
|
+
export function maxFuzzyDistance(input) {
|
|
29
|
+
if (input.length <= 3)
|
|
30
|
+
return 1;
|
|
31
|
+
if (input.length <= 5)
|
|
32
|
+
return 2;
|
|
33
|
+
return 3;
|
|
34
|
+
}
|
|
27
35
|
/**
|
|
28
36
|
* Levenshtein edit distance: the minimum number of single-character insertions, deletions, or
|
|
29
|
-
* substitutions to turn `s1` into `s2`. Shared by
|
|
37
|
+
* substitutions to turn `s1` into `s2`. Shared by command-typo suggestion.
|
|
30
38
|
*/
|
|
31
39
|
export function levenshtein(s1, s2) {
|
|
32
40
|
if (s1.length < s2.length)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const ALLOWED_DOC_HOSTS: Set<string>;
|
|
2
|
+
export declare function isAllowedDocHost(hostname: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Coveo indexes Help articles under an internal `/help_doccontent` URL; rewrite it to the
|
|
5
|
+
* canonical `/s/articleView` page a human (or our fetcher) can actually open.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeHelpDocContentUrl(rawUrl: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const ALLOWED_DOC_HOSTS = new Set(['help.salesforce.com', 'developer.salesforce.com']);
|
|
2
|
+
export function isAllowedDocHost(hostname) {
|
|
3
|
+
return ALLOWED_DOC_HOSTS.has(hostname.toLowerCase());
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Coveo indexes Help articles under an internal `/help_doccontent` URL; rewrite it to the
|
|
7
|
+
* canonical `/s/articleView` page a human (or our fetcher) can actually open.
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeHelpDocContentUrl(rawUrl) {
|
|
10
|
+
let url;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(rawUrl);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return rawUrl;
|
|
16
|
+
}
|
|
17
|
+
if (url.hostname.toLowerCase() !== 'help.salesforce.com')
|
|
18
|
+
return rawUrl;
|
|
19
|
+
if (!url.pathname.toLowerCase().startsWith('/help_doccontent'))
|
|
20
|
+
return rawUrl;
|
|
21
|
+
const id = url.searchParams.get('id');
|
|
22
|
+
if (!id)
|
|
23
|
+
return rawUrl;
|
|
24
|
+
const articleUrl = new URL('https://help.salesforce.com/s/articleView');
|
|
25
|
+
articleUrl.searchParams.set('id', id.endsWith('.htm') ? id : `${id}.htm`);
|
|
26
|
+
articleUrl.searchParams.set('type', '5');
|
|
27
|
+
for (const key of ['release', 'language']) {
|
|
28
|
+
const value = url.searchParams.get(key);
|
|
29
|
+
if (value)
|
|
30
|
+
articleUrl.searchParams.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
return articleUrl.toString();
|
|
33
|
+
}
|
package/dist/sites/index.js
CHANGED
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
import { salesforce } from './salesforce/index.js';
|
|
2
2
|
import { salesforceDeveloper } from './salesforce-developer/index.js';
|
|
3
|
-
import { nextjs } from './nextjs/index.js';
|
|
4
|
-
import { react, vue, tailwind, jest, cypress, vitest, vitepress } from './algolia.js';
|
|
5
|
-
import { angular, redux, vite, fastify, rollup, vueuse } from './auto.js';
|
|
6
3
|
// The known documentation sites. A plain constant, not a registry: these ship
|
|
7
4
|
// with the CLI, so there is nothing to register at runtime. Sites with custom
|
|
8
|
-
// fetch
|
|
5
|
+
// fetch behavior live in their own module (e.g. ./salesforce). Salesforce Help and
|
|
9
6
|
// Salesforce Developer are separate modules — different domains, different extraction.
|
|
10
7
|
export const SITES = [
|
|
11
|
-
react,
|
|
12
|
-
vue,
|
|
13
|
-
tailwind,
|
|
14
|
-
nextjs,
|
|
15
|
-
jest,
|
|
16
|
-
cypress,
|
|
17
|
-
vitest,
|
|
18
|
-
vitepress,
|
|
19
|
-
angular,
|
|
20
|
-
redux,
|
|
21
|
-
vite,
|
|
22
|
-
fastify,
|
|
23
|
-
rollup,
|
|
24
|
-
vueuse,
|
|
25
8
|
salesforce,
|
|
26
9
|
salesforceDeveloper,
|
|
27
10
|
// TanStack docs embed a GitHub "edit this page" link, so capture resolves the page's source
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchSalesforceDoc } from '../salesforce-doc-fetch.js';
|
|
2
|
-
import { normalizeHelpDocContentUrl } from '
|
|
2
|
+
import { normalizeHelpDocContentUrl } from '../coveo.js';
|
|
3
3
|
// Containers a Help article renders into (inside shadow DOM); the shared fetcher pierces
|
|
4
4
|
// shadow roots to find them, falling back to the page body when none match.
|
|
5
5
|
const CONTENT_SELECTORS = [
|