sf-docs-agent 0.1.0 → 0.1.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 +37 -25
- package/agents/sf-docs.md +6 -3
- package/bin/install.mjs +1 -1
- package/fetch-docs.mjs +11 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# sf-docs-agent
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/sf-docs-agent)
|
|
4
|
+
|
|
3
5
|
A Claude Code subagent that answers Salesforce questions from the official docs and hands back quotes, not pages.
|
|
4
6
|
|
|
5
7
|
It reads developer.salesforce.com, architect.salesforce.com, help.salesforce.com, admin.salesforce.com and
|
|
@@ -30,29 +32,31 @@ Here is how the two compare:
|
|
|
30
32
|
| Checking an answer | re-read the page | `grep` one line |
|
|
31
33
|
|
|
32
34
|
The trade-off: your main session trusts the subagent. That is why the model only picks line numbers and the script
|
|
33
|
-
copies the lines. Every quote is the page
|
|
35
|
+
copies the lines. Every quote is copied word for word from the page, not written from the model's memory, and you can
|
|
36
|
+
check it with one `grep` against the cached copy, which saves curly quotes as straight ones and drops image links and
|
|
37
|
+
the padding in table rows.
|
|
34
38
|
|
|
35
|
-
##
|
|
39
|
+
## 84% less in your context, 63% fewer tokens overall
|
|
36
40
|
|
|
37
41
|
On 10 questions against Salesforce's own platform-docs-get skill, the agent
|
|
38
|
-
added
|
|
42
|
+
added 84% fewer tokens to your context and used 63% fewer tokens over the whole session, with the right answer on
|
|
39
43
|
10 of 10.
|
|
40
44
|
|
|
41
45
|
Below are the measurements, each read as platform-docs-get → sf-docs.
|
|
42
46
|
|
|
43
47
|
| Question | Site | Tokens added to context | Saved | Session token usage | Saved |
|
|
44
48
|
|---|---|---|---|---|---|
|
|
45
|
-
| 1. Scratch org editions | developer |
|
|
46
|
-
| 2. Platform event allocation | developer |
|
|
47
|
-
| 3. QueryLocator batch limits | developer |
|
|
48
|
-
| 4. Integration patterns | architect | 1,
|
|
49
|
-
| 5. Salesforce Connect editions | help |
|
|
50
|
-
| 6. Flow version limits | help |
|
|
51
|
-
| 7. Headless 360 | admin |
|
|
52
|
-
| 8. SLDS tooltips | lightningdesignsystem |
|
|
53
|
-
| 9. Bulk API batch size | developer | 2,
|
|
54
|
-
| 10. Pub/Sub fetch limit | developer |
|
|
55
|
-
| **All** | |
|
|
49
|
+
| 1. Scratch org editions | developer | 2,178 → **289** | **86.7%** | 338,852 → **119,355** | **64.8%** |
|
|
50
|
+
| 2. Platform event allocation | developer | 4,107 → **490** | **88.1%** | 387,598 → **120,396** | **68.9%** |
|
|
51
|
+
| 3. QueryLocator batch limits | developer | 2,568 → **581** | **77.4%** | 406,878 → **113,611** | **72.1%** |
|
|
52
|
+
| 4. Integration patterns | architect | 1,104 → **484** | **56.2%** | 154,068 → **125,540** | **18.5%** |
|
|
53
|
+
| 5. Salesforce Connect editions | help | 1,948 → **322** | **83.5%** | 382,751 → **157,746** | **58.8%** |
|
|
54
|
+
| 6. Flow version limits | help | 4,578 → **290** | **93.7%** | 339,464 → **164,888** | **51.4%** |
|
|
55
|
+
| 7. Headless 360 | admin | 3,772 → **380** | **89.9%** | 427,542 → **117,258** | **72.6%** |
|
|
56
|
+
| 8. SLDS tooltips | lightningdesignsystem | 3,876 → **414** | **89.3%** | 340,914 → **112,252** | **67.1%** |
|
|
57
|
+
| 9. Bulk API batch size | developer | 2,134 → **411** | **80.7%** | 506,340 → **125,870** | **75.1%** |
|
|
58
|
+
| 10. Pub/Sub fetch limit | developer | 1,199 → **716** | **40.3%** | 236,766 → **130,110** | **45.0%** |
|
|
59
|
+
| **All** | | 27,464 → **4,377** | **84.1%** | 3,521,173 → **1,287,026** | **63.4%** |
|
|
56
60
|
|
|
57
61
|
<details>
|
|
58
62
|
<summary>The 10 questions</summary>
|
|
@@ -256,24 +260,30 @@ official URLs, and the session turned them into the answer.
|
|
|
256
260
|
|
|
257
261
|
## Install
|
|
258
262
|
|
|
259
|
-
|
|
263
|
+
Needs Node 22+ and Claude Code. The installer is on npm as
|
|
264
|
+
[`sf-docs-agent`](https://www.npmjs.com/package/sf-docs-agent).
|
|
265
|
+
|
|
260
266
|
```
|
|
261
267
|
npx sf-docs-agent # for all your projects
|
|
262
|
-
```
|
|
263
|
-
Project scope:
|
|
264
|
-
```
|
|
265
268
|
npx sf-docs-agent --project # for the project in the current folder only
|
|
266
269
|
```
|
|
267
270
|
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
The installer adds `playwright-core` (13 MB) and asks before it downloads headless Chromium (about 200 MB). Pass
|
|
272
|
+
`--yes` to skip the question in scripts. If you say no, it prints the command to run before first use. On Linux, also
|
|
273
|
+
run `node "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/sf-docs/node_modules/playwright-core/cli.js" install-deps chromium` once
|
|
274
|
+
for the system libraries. It asks for your password itself.
|
|
275
|
+
|
|
276
|
+
Then start a new Claude Code session. The agent is listed as `sf-docs`.
|
|
277
|
+
|
|
278
|
+
**Update:** `npx sf-docs-agent@latest`. Without `@latest`, npx runs any copy already installed globally or in the
|
|
279
|
+
current project.
|
|
270
280
|
|
|
271
|
-
|
|
272
|
-
in
|
|
273
|
-
`~/.claude`.
|
|
281
|
+
**Remove:** delete `~/.claude/agents/sf-docs.md` (or `.claude/agents/sf-docs.md` in the project). The script and cache
|
|
282
|
+
live in `~/.claude/sf-docs/` for every scope, so delete that folder last, once no project uses the agent. If
|
|
283
|
+
`CLAUDE_CONFIG_DIR` is set, the installer uses that instead of `~/.claude`.
|
|
274
284
|
|
|
275
|
-
|
|
276
|
-
(`0` fetches every time):
|
|
285
|
+
**Cache:** pages stay cached for 24 hours, so a follow-up question does not fetch them again. To change that, set
|
|
286
|
+
`SF_DOCS_CACHE_HOURS` in your Claude Code `settings.json` (`0` fetches every time):
|
|
277
287
|
|
|
278
288
|
```json
|
|
279
289
|
{ "env": { "SF_DOCS_CACHE_HOURS": "72" } }
|
|
@@ -289,6 +299,8 @@ Pages stay cached for 24 hours, so a follow-up question does not fetch them agai
|
|
|
289
299
|
- No Help search. Help's `robots.txt` disallows it. Help's sitemaps list article ids but not titles.
|
|
290
300
|
- It reads the current docs, which don't always name their release.
|
|
291
301
|
- It follows at most one link from a page it read. An answer that sits further away comes back as a gap.
|
|
302
|
+
- On a long list or table, the agent now and then stops a row early, for example five of six integration patterns.
|
|
303
|
+
Every quote it gives is still right; the missing row is the problem.
|
|
292
304
|
- If a site starts blocking headless browsers, the agent stops and reports it. It does not try to get around the block.
|
|
293
305
|
- What leaves your machine: the pages the agent fetches from those sites (and each site's `robots.txt`, which raw
|
|
294
306
|
fetches load first). No telemetry. Installing downloads `playwright-core` from npm and, if you agree, Chromium from
|
package/agents/sf-docs.md
CHANGED
|
@@ -17,7 +17,7 @@ you do not fill gaps from memory. If the docs do not say it, the answer is "not
|
|
|
17
17
|
| developer.salesforce.com, new docs (`/docs/platform/...` and other paths in a per-product index) | `.md` twin, fetched raw | `developer.salesforce.com/docs/llms.txt` lists one `/docs/llms-<product>.txt` per product (see Find for the two that terms can miss) |
|
|
18
18
|
| developer.salesforce.com, atlas docs (`/docs/atlas.en-us.*`: Apex Developer Guide, Metadata API, Object Reference and more) | `.htm` page, rendered. There is no `.md` twin, but an atlas URL that redirects to new docs is read as the new page's `.md` (`[twin ...]` in the output) | `developer.salesforce.com/docs/llms-product-docs.txt` (3 MB, 19,000+ `.htm` links) |
|
|
19
19
|
| architect.salesforce.com | `.md`, fetched raw | `https://architect.salesforce.com/llms.txt` |
|
|
20
|
-
| help.salesforce.com | rendered, about 10 s a page | `'https://help.salesforce.com/s/articleView?id=<id>.htm&type=5'`. With an id from the brief, a doc, or a fetched Help page's `.links.txt` (`/
|
|
20
|
+
| help.salesforce.com | rendered, about 10 s a page | `'https://help.salesforce.com/s/articleView?id=<id>.htm&type=5'`. With an id from the brief, a doc, or a fetched Help page's `.links.txt` (`/s/articleView?id=<id>.htm&language=en_US&type=5` lines; knowledge articles are `?id=<number>&type=1`), fetch it. With no id, use the sitemaps: fetch `'https://help.salesforce.com/apex/Help_SiteMapIndexExternal?producttype=<platform|service|xcloud|sales|ai|data|experience|analytics|mktg|commerce|cc|ind|sfdo|slack|release-notes>'` with `--terms`; an index prints every page sitemap it lists, whatever the terms (46 for platform). Fetch those 20 a call with `--terms '<word_word>'`. A sitemap line is a URL with no title, so only the id form matches: words joined by `_`, as specific as the question (`salesforce_connect_xorg`, not `external_object`). Each match prints the article's `<loc>` line, up to 40 a page. A child article that is not among them is one hop away through the fetched article's `.links.txt`. Do not use Help search: robots.txt disallows `/s/search-result` |
|
|
21
21
|
| admin.salesforce.com | rendered | `admin.salesforce.com/sitemap_index.xml` lists the post sitemaps (`/post-sitemap.xml`, `/post-sitemap2.xml` ...); fetch them all in one call with `--terms` and use the matching `<loc>` URLs. `/blog` shows only the latest 3 posts |
|
|
22
22
|
| lightningdesignsystem.com | `.md` twin, fetched raw (the home page itself is rendered) | the `.links.txt` of the home page, as `/2e1ef8501/p/<id>-<slug>` |
|
|
23
23
|
| v1.lightningdesignsystem.com (SLDS 1, still used by orgs not on SLDS 2) | rendered | the `.links.txt` of `https://v1.lightningdesignsystem.com/`, as `/components/<name>/` and `/utilities/<name>/` |
|
|
@@ -85,7 +85,9 @@ Every tool call costs you the whole conversation again, so use as few Bash calls
|
|
|
85
85
|
next page.
|
|
86
86
|
3. **Look.** To see the lines around a match, use Read with `offset` and `limit`, or `grep -n -C3 -e '<term>' <file>`:
|
|
87
87
|
both number lines as `--quote` counts them. `sed -n` and `head` do not, so never take a line number from them.
|
|
88
|
-
Never read a page whole.
|
|
88
|
+
Never read a page whole. When the answer is a list or a table (patterns, editions, values), Read from its first
|
|
89
|
+
item to the next heading before you quote: a `--terms` match list stops at its cap, and the last rows may not
|
|
90
|
+
hold your term.
|
|
89
91
|
4. **One hop.** If a matched line, a `.links.txt` match or a sentence you will quote names another page that holds
|
|
90
92
|
part of the answer (a child article, a "Limitations for ..." heading with no text under it, an index entry whose
|
|
91
93
|
title matches the question, another post), fetch it now with `--terms`. One hop only: a page named by that page
|
|
@@ -93,7 +95,8 @@ Every tool call costs you the whole conversation again, so use as few Bash calls
|
|
|
93
95
|
5. **Quote.** Pick the line numbers that carry the answer, from the matches or the Read. Then let the script copy them:
|
|
94
96
|
`node "/path/to/sf-docs-agent/fetch-docs.mjs" --quote <cache file>:<line>,<line> [<cache file>:<line> ...]`
|
|
95
97
|
(cache file relative to the cache dir or absolute). It prints the Evidence and Sources blocks verbatim, with the
|
|
96
|
-
official URL from each file's first line. Never type a quote yourself
|
|
98
|
+
official URL from each file's first line. Never type a quote yourself, and paste the script's output unchanged:
|
|
99
|
+
not one character, apostrophes included.
|
|
97
100
|
|
|
98
101
|
## Hard limits
|
|
99
102
|
|
package/bin/install.mjs
CHANGED
|
@@ -47,7 +47,7 @@ const ask = async q => {
|
|
|
47
47
|
return /^y(es)?$/i.test(a.trim());
|
|
48
48
|
};
|
|
49
49
|
if (present) console.log('Headless Chromium is already installed.');
|
|
50
|
-
else if (process.argv.includes('--yes') || await ask('Download headless Chromium (about
|
|
50
|
+
else if (process.argv.includes('--yes') || await ask('Download headless Chromium (about 200 MB)? [y/N] ')) {
|
|
51
51
|
try { execFileSync(process.execPath, [cli, 'install', '--only-shell', 'chromium'], { stdio: 'inherit' }); }
|
|
52
52
|
catch { console.error(`\nBrowser download failed (see above). Retry later with:\n node "${cli}" install --only-shell chromium`); }
|
|
53
53
|
} else console.log(`Skipped the browser download. Before first use, run:\n node "${cli}" install --only-shell chromium`);
|
package/fetch-docs.mjs
CHANGED
|
@@ -23,8 +23,10 @@ if (oi >= 0) out = args.splice(oi, 2)[1];
|
|
|
23
23
|
// No page starts after the budget: each one left is printed 'not fetched (time limit)', so the caller can rerun it.
|
|
24
24
|
const bi = args.indexOf('--budget');
|
|
25
25
|
const deadline = bi >= 0 ? Date.now() + Number(args.splice(bi, 2)[1]) * 1000 : Infinity;
|
|
26
|
+
// Curly quotes are saved straight: a model that retypes a quote writes ' and ", and the grep check must still match.
|
|
27
|
+
const plain = text => text.replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"');
|
|
26
28
|
// --terms a,b: after the fetch, print each saved page's lines that hold a term, so the caller needs no grep.
|
|
27
|
-
const termsOf = v => (v ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
29
|
+
const termsOf = v => plain(v ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean); // the cache holds straight quotes
|
|
28
30
|
const ti = args.indexOf('--terms');
|
|
29
31
|
const terms = ti >= 0 ? termsOf(args.splice(ti, 2)[1]) : [];
|
|
30
32
|
if (!args.length || !out || Number.isNaN(deadline) || (ti >= 0 && !terms.length)) { console.error('usage: node fetch-docs.mjs [--out dir] [--budget seconds] [--terms a,b] <url | /docs/path.md> ...\n node fetch-docs.mjs [--out dir] --search a,b [--site <host>]\n node fetch-docs.mjs [--out dir] --quote <cache file>:<line>[,<line>...] ...'); process.exit(2); }
|
|
@@ -82,7 +84,8 @@ const slash = p => p.split(sep).join('/');
|
|
|
82
84
|
// page order. Caps set on a clean 20-question sample (looser caps printed twice the old grep's 2k chars a call).
|
|
83
85
|
const MAX_HITS = 20, MAX_LINKS = 10, SHORT_LINES = 40, SHORT_CHARS = 4000, PER_FILE = 3;
|
|
84
86
|
const matches = (name, text, t) => text.split('\n').flatMap((line, i, ls) => {
|
|
85
|
-
|
|
87
|
+
// plain(): a sidecar and a page cached before quotes were saved straight still hold curly ones.
|
|
88
|
+
const low = i || !ls[0].startsWith('Source: ') ? plain(line).toLowerCase() : '', hit = t.filter(s => low.includes(s)); // a page's line 1 is its Source line; a sidecar has none
|
|
86
89
|
return hit.length ? [{ name, n: i + 1, line, hit, at: Math.min(...hit.map(s => low.indexOf(s))) }] : [];
|
|
87
90
|
});
|
|
88
91
|
const rank = (ms, max = MAX_HITS) => {
|
|
@@ -215,8 +218,11 @@ function extract([sels, fallback]) {
|
|
|
215
218
|
for (let i = 0; i < roots.length; i++) for (const el of roots[i].querySelectorAll('*')) if (el.shadowRoot) roots.push(el.shadowRoot);
|
|
216
219
|
// Same-site links (nav and TOC included) so pages without an llms.txt index can still be discovered.
|
|
217
220
|
const links = new Set();
|
|
221
|
+
// Path and query: every Help article sits at /s/articleView and differs only by ?id=, and it links itself with
|
|
222
|
+
// &language= added, so a link is the page itself when its id matches.
|
|
223
|
+
const self = u => u.pathname + (new URLSearchParams(u.search).get('id') ?? u.search), here = self(location);
|
|
218
224
|
for (const r of roots) for (const a of r.querySelectorAll('a[href]')) {
|
|
219
|
-
if (a.host === location.host && a
|
|
225
|
+
if (a.host === location.host && self(a) !== here) links.add(`${a.pathname}${a.search} ${(a.innerText || '').trim().replace(/\s+/g, ' ').slice(0, 80)}`);
|
|
220
226
|
}
|
|
221
227
|
const page = t => `# ${document.title}\n\n${t}\n`;
|
|
222
228
|
const list = [...links].join('\n');
|
|
@@ -321,7 +327,7 @@ const emit = r => {
|
|
|
321
327
|
// Markdown only (a .md URL, or a twin read as r.md): rendered text has no code fences, so lean could reach a code
|
|
322
328
|
// sample. Cached pages are lean already.
|
|
323
329
|
const md = r.md || /\.md$/.test(new URL(key).pathname);
|
|
324
|
-
const body = status === 200 && !cached
|
|
330
|
+
const body = status === 200 && !cached ? plain(md ? lean(r.body) : r.body) : r.body;
|
|
325
331
|
const h = createHash('sha1').update(body).digest('hex').slice(0, 12);
|
|
326
332
|
const u = new URL(key);
|
|
327
333
|
const file = fileFor(u);
|
|
@@ -367,7 +373,7 @@ if (!urls.every(isFresh) && Date.now() < deadline) { // a spent budget skips eve
|
|
|
367
373
|
// The exact command, not "reinstall": a reinstall without a terminal skips the browser again.
|
|
368
374
|
const cli = slash(join(dirname(fileURLToPath(import.meta.url)), 'node_modules/playwright-core/cli.js'));
|
|
369
375
|
console.error(`Install the browser with: node "${cli}" install --only-shell chromium`);
|
|
370
|
-
console.error(
|
|
376
|
+
console.error(`On Linux, also run once: node "${cli}" install-deps chromium`);
|
|
371
377
|
process.exit(3);
|
|
372
378
|
}
|
|
373
379
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sf-docs-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Claude Code subagent that answers from official Salesforce docs (developer, architect, help, admin, lightningdesignsystem.com) with grep-checked quotes",
|
|
6
6
|
"devDependencies": {
|
|
7
|
-
"playwright-core": "1.
|
|
7
|
+
"playwright-core": "1.64.1"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
10
|
"sf-docs-agent": "bin/install.mjs"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=22"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"claude-code",
|