sf-docs-agent 0.1.0 → 0.1.2

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # sf-docs-agent
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/sf-docs-agent)](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,43 @@ 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's own text, never the model's memory, and you can check it with one `grep`.
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.
38
+
39
+ ## Why not use Salesforce Docs MCP
40
+
41
+ Salesforce's [salesforce-docs-mcp](https://labs.agentforce.com/docs/salesforce-docs-mcp) works, and behind a
42
+ subagent it was faster and cheaper than sf-docs-agent in my tests. But it searches a closed index you can't see into, with
43
+ no date on its results.
44
+
45
+ I wanted an alternative I control and can validate against the live sites. sf-docs-agent is open source, reads the page
46
+ Salesforce publishes today, and every quote can be checked against that page with one `grep`. If you're fine
47
+ trusting the index, the MCP in a subagent is a good choice. [The comparison](#compared-with-salesforce-docs-mcp) has
48
+ the numbers and the trade-offs.
34
49
 
35
- ## 86% less in your context, 64% fewer tokens overall
50
+ ## 84% less in your context, 38% fewer tokens than platform-docs-get
36
51
 
37
52
  On 10 questions against Salesforce's own platform-docs-get skill, the agent
38
- added 86% fewer tokens to your context and used 64% fewer tokens over the whole session, with the right answer on
39
- 10 of 10.
53
+ added 84% fewer tokens to your context and used 38% fewer tokens over the whole session, with the right answer on
54
+ 39 of 40 runs for each. In the second of four rounds, platform-docs-get missed question 5 and sf-docs-agent missed
55
+ question 4.
40
56
 
41
- Below are the measurements, each read as platform-docs-get → sf-docs.
57
+ Below are the medians of those four rounds, each read as platform-docs-get → sf-docs-agent.
42
58
 
43
59
  | Question | Site | Tokens added to context | Saved | Session token usage | Saved |
44
60
  |---|---|---|---|---|---|
45
- | 1. Scratch org editions | developer | 906 → **242** | **73.3%** | 236,882 → **86,172** | **63.6%** |
46
- | 2. Platform event allocation | developer | 3,120 → **471** | **84.9%** | 387,764 → **126,998** | **67.2%** |
47
- | 3. QueryLocator batch limits | developer | 692 → **458** | **33.8%** | 287,129 → **105,756** | **63.2%** |
48
- | 4. Integration patterns | architect | 1,223 → **528** | **56.8%** | 154,149 → **102,635** | **33.4%** |
49
- | 5. Salesforce Connect editions | help | 5,591 → **649** | **88.4%** | 477,647 → **311,396** | **34.8%** |
50
- | 6. Flow version limits | help | 5,940 → **293** | **95.1%** | 354,017 → **171,752** | **51.5%** |
51
- | 7. Headless 360 | admin | 4,247 → **341** | **92.0%** | 675,859 → **121,840** | **82.0%** |
52
- | 8. SLDS tooltips | lightningdesignsystem | 5,983 → **463** | **92.3%** | 360,244 → **112,618** | **68.7%** |
53
- | 9. Bulk API batch size | developer | 2,328 → **476** | **79.6%** | 454,224 → **91,811** | **79.8%** |
54
- | 10. Pub/Sub fetch limit | developer | 2,212 → **625** | **71.7%** | 448,205 → **141,343** | **68.5%** |
55
- | **All** | | 32,242 → **4,546** | **85.9%** | 3,836,120 → **1,372,321** | **64.2%** |
61
+ | 1. Scratch org editions | developer | 2,178 → **289** | **86.7%** | 186,768 → **119,355** | **36.1%** |
62
+ | 2. Platform event allocation | developer | 4,107 → **490** | **88.1%** | 245,482 → **120,396** | **51.0%** |
63
+ | 3. QueryLocator batch limits | developer | 2,568 → **581** | **77.4%** | 217,691 → **113,611** | **47.8%** |
64
+ | 4. Integration patterns | architect | 1,104 → **484** | **56.2%** | 127,043 → **125,540** | **1.2%** |
65
+ | 5. Salesforce Connect editions | help | 1,948 → **322** | **83.5%** | 201,770 → **157,746** | **21.8%** |
66
+ | 6. Flow version limits | help | 4,578 → **290** | **93.7%** | 217,138 → **164,888** | **24.1%** |
67
+ | 7. Headless 360 | admin | 3,772 → **380** | **89.9%** | 250,200 → **105,388** | **57.9%** |
68
+ | 8. SLDS tooltips | lightningdesignsystem | 3,876 → **414** | **89.3%** | 201,906 → **112,252** | **44.4%** |
69
+ | 9. Bulk API batch size | developer | 2,134 → **411** | **80.7%** | 255,340 → **125,870** | **50.7%** |
70
+ | 10. Pub/Sub fetch limit | developer | 1,199 → **716** | **40.3%** | 141,826 → **130,110** | **8.3%** |
71
+ | **All** | | 27,464 → **4,377** | **84.1%** | 2,045,164 → **1,275,156** | **37.7%** |
56
72
 
57
73
  <details>
58
74
  <summary>The 10 questions</summary>
@@ -70,6 +86,68 @@ Below are the measurements, each read as platform-docs-get → sf-docs.
70
86
 
71
87
  </details>
72
88
 
89
+ ## Compared with Salesforce Docs MCP
90
+
91
+ Aside from the platform-docs-get skill, Salesforce also has
92
+ [salesforce-docs-mcp](https://labs.agentforce.com/docs/salesforce-docs-mcp), a closed-source public MCP server that
93
+ searches an index of Help articles, developer guides and release notes. I ran the same 10 questions through it in two
94
+ setups:
95
+
96
+ a. MCP in main session: the main agent calls the MCP tools directly
97
+
98
+ b. MCP in a subagent: a subagent whose only tools are the MCP's, with the same report format as sf-docs-agent
99
+
100
+ Three rounds each, medians, same model.
101
+
102
+ | | sf-docs-agent | a. MCP in main session | b. MCP in a subagent |
103
+ |---|---|---|---|
104
+ | Tokens added to context | **4,470** | 45,636 | 5,327 |
105
+ | Session token usage | 1,291,504 | 1,052,673 | **937,229** |
106
+ | Seconds, all 10 | 433 | 239 | **218** |
107
+ | Right answers, of 30 | **30** | 28 | 24 |
108
+ | Quotes that match the text the tools returned, all runs | **113 of 113** | 173 of 188 | 172 of 177 |
109
+ | Quotes on today's page, checked with sf-docs-agent's fetcher, all runs | **113 of 113** | 159 of 188 | 135 of 177 |
110
+
111
+ The MCP in a subagent is faster and uses 27% fewer session tokens than sf-docs-agent, and a. MCP in main session uses
112
+ 18% fewer. Its results come back as markdown excerpts, so I suspect Salesforce keeps a `.md` snapshot of the docs taken
113
+ at some point. Because the index is closed, you can't see what it holds or when it was taken, and that has some costs:
114
+
115
+ - **Coverage.** Across 91 searches it returned no admin.salesforce.com page and no SLDS 2 content page. The
116
+ subagent answered "not found" or wrong on Headless 360 and SLDS tooltips in all three rounds, and those two
117
+ questions hold all 8 wrong MCP answers. In the main session, the model also reached for web search or fetch on 27
118
+ of 30 runs and skipped the MCP on 4; its Headless 360 answers came from admin.salesforce.com through web fetch, not
119
+ the index.
120
+ - **Freshness.** Results carry no date, and some come from the old atlas copy of a page. On question 2 the quotes were
121
+ exact, but the source URL now opens a page without them. On question 1 the wording is older than today's page, with
122
+ the same values. On both questions the values matched the answer key in every MCP run.
123
+ - **Open vs closed.** The index has no published source or refresh schedule. sf-docs-agent reads the live page every time,
124
+ and every quote can be checked with one `grep`.
125
+
126
+ Most of the speed gap is page rendering. Of sf-docs-agent's 30 runs, the 18 that read at least one `.md` page took a
127
+ median 26.5 seconds, against 20.5 for the MCP subagent on the same questions and rounds. Help, admin and the
128
+ Apex Developer Guide had no `.md` copy, so sf-docs-agent rendered them in a headless browser, and the 12 runs that
129
+ rendered every page took a median 68.5 seconds, against 20 for the MCP subagent. The Apex Developer Guide and Apex
130
+ Reference had `.md` copies by 2026-10-31, after these runs; Help, admin, Metadata API and Object Reference still have
131
+ none.
132
+
133
+ <details>
134
+ <summary>Per question, sf-docs-agent / a. MCP in main session / b. MCP in a subagent</summary>
135
+
136
+ | Question | Tokens added to context | Seconds | Right answers, of 3 |
137
+ |---|---|---|---|
138
+ | 1. Scratch org editions | 248 / 5,011 / 421 | 23 / 22 / 18 | 3 / 3 / 3 |
139
+ | 2. Platform event allocation | 526 / 6,177 / 713 | 29 / 28 / 21 | 3 / 3 / 3 |
140
+ | 3. QueryLocator batch limits | 728 / 8,304 / 531 | 68 / 18 / 20 | 3 / 3 / 3 |
141
+ | 4. Integration patterns | 463 / 4,863 / 788 | 25 / 21 / 22 | 3 / 3 / 3 |
142
+ | 5. Salesforce Connect editions | 300 / 3,556 / 381 | 84 / 17 / 18 | 3 / 3 / 3 |
143
+ | 6. Flow version limits | 307 / 2,817 / 291 | 59 / 20 / 19 | 3 / 3 / 3 |
144
+ | 7. Headless 360 | 374 / 4,232 / 772 | 60 / 31 / 31 | 3 / 3 / 0 |
145
+ | 8. SLDS tooltips | 403 / 1,231 / 617 | 32 / 40 / 33 | 3 / 1 / 0 |
146
+ | 9. Bulk API batch size | 489 / 6,284 / 529 | 27 / 22 / 20 | 3 / 3 / 3 |
147
+ | 10. Pub/Sub fetch limit | 632 / 3,161 / 284 | 26 / 20 / 16 | 3 / 3 / 3 |
148
+
149
+ </details>
150
+
73
151
  ## How sf-docs-agent reads each site
74
152
 
75
153
  The agent reads the `.md` copy of a page where the site publishes one. Not every Salesforce docs site does yet.
@@ -82,7 +160,8 @@ The agent finds pages through the site's `llms.txt` index or its sitemaps.
82
160
  | Site | How | Where it finds pages |
83
161
  |---|---|---|
84
162
  | developer.salesforce.com, new docs | the `.md` copy Salesforce publishes | `/docs/llms.txt` and one index per product |
85
- | developer.salesforce.com, atlas docs (Apex, Apex Reference, Metadata API, Object Reference) | renders the page, code samples included | `/docs/llms-product-docs.txt` |
163
+ | developer.salesforce.com, Apex Developer Guide and Apex Reference | the `.md` copy (atlas URLs redirect to the new docs, by 2026-10-31) | `/docs/llms-product-docs.txt` |
164
+ | developer.salesforce.com, atlas docs (Metadata API, Object Reference) | renders the page, code samples included | `/docs/llms-product-docs.txt` |
86
165
  | developer.salesforce.com, API reference pages (`?meta=<operation>`) | renders the page | the product's index |
87
166
  | architect.salesforce.com | the `.md` copy | `/llms.txt` |
88
167
  | help.salesforce.com, articles and knowledge articles | renders the page | links in other docs and articles, and Help's product sitemaps |
@@ -103,7 +182,7 @@ Name the site or product if you know it, and say what you want to know.
103
182
 
104
183
  The sample prompt is broad on purpose: I wanted to see the agent decide what "governors and limits" covers, and whether it could find everything in a broad question.
105
184
 
106
- Here are the results of that prompt, one run each on Opus 5.5 and Sonnet 5.5 at effort high. Each session called the sf-docs agent, which returned quotes with their
185
+ Here are the results of that prompt, one run each on Opus 5.5 and Sonnet 5.5 at effort high. Each session called sf-docs-agent, which returned quotes with their
107
186
  official URLs, and the session turned them into the answer.
108
187
 
109
188
  <details>
@@ -256,24 +335,30 @@ official URLs, and the session turned them into the answer.
256
335
 
257
336
  ## Install
258
337
 
259
- User scope:
338
+ Needs Node 22+ and Claude Code. The installer is on npm as
339
+ [`sf-docs-agent`](https://www.npmjs.com/package/sf-docs-agent).
340
+
260
341
  ```
261
342
  npx sf-docs-agent # for all your projects
262
- ```
263
- Project scope:
264
- ```
265
343
  npx sf-docs-agent --project # for the project in the current folder only
266
344
  ```
267
345
 
268
- Needs Node 18.17+ and Claude Code. The installer adds `playwright-core` (8.5 MB). It also needs a headless Chromium
269
- (about 180 MB). On Linux, also run `npx playwright-core install-deps chromium` once.
346
+ The installer adds `playwright-core` (13 MB) and asks before it downloads headless Chromium (about 200 MB). Pass
347
+ `--yes` to skip the question in scripts. If you say no, it prints the command to run before first use. On Linux, also
348
+ run `node "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/sf-docs/node_modules/playwright-core/cli.js" install-deps chromium` once
349
+ for the system libraries. It asks for your password itself.
350
+
351
+ Then start a new Claude Code session. The agent is listed as `sf-docs`.
352
+
353
+ **Update:** `npx sf-docs-agent@latest`. Without `@latest`, npx runs any copy already installed globally or in the
354
+ current project.
270
355
 
271
- To update, run it again. To remove it, delete `~/.claude/agents/sf-docs.md` (or `.claude/agents/sf-docs.md`
272
- in the project) and `~/.claude/sf-docs/`. If `CLAUDE_CONFIG_DIR` is set, the installer uses that instead of
273
- `~/.claude`.
356
+ **Remove:** delete `~/.claude/agents/sf-docs.md` (or `.claude/agents/sf-docs.md` in the project). The script and cache
357
+ live in `~/.claude/sf-docs/` for every scope, so delete that folder last, once no project uses the agent. If
358
+ `CLAUDE_CONFIG_DIR` is set, the installer uses that instead of `~/.claude`.
274
359
 
275
- Pages stay cached for 24 hours, so a follow-up question does not fetch them again. To change that, set `SF_DOCS_CACHE_HOURS` in your Claude Code `settings.json`
276
- (`0` fetches every time):
360
+ **Cache:** pages stay cached for 24 hours, so a follow-up question does not fetch them again. To change that, set
361
+ `SF_DOCS_CACHE_HOURS` in your Claude Code `settings.json` (`0` fetches every time):
277
362
 
278
363
  ```json
279
364
  { "env": { "SF_DOCS_CACHE_HOURS": "72" } }
@@ -281,14 +366,17 @@ Pages stay cached for 24 hours, so a follow-up question does not fetch them agai
281
366
 
282
367
  ## Limitations
283
368
 
284
- - On help, admin, SLDS 1, API reference pages and the atlas guides, the agent finds the article text by where it sits on
285
- the page. If Salesforce changes a layout, it reads the largest block of text instead, which can be one section of the
286
- article, and an article under 1,500 characters can't be read. A daily check in this repo flags the change.
369
+ - On help, admin, SLDS 1, API reference pages and the atlas guides that still render (Metadata API, Object Reference and
370
+ more), the agent finds the article text by where it sits on the page. If Salesforce changes a layout, it reads the
371
+ largest block of text instead, which can be one section of the article, and an article under 1,500 characters can't
372
+ be read. A daily check in this repo flags the change.
287
373
  - A rendered page can take up to 80 seconds before the agent gives up on it (45 s to load, 20 s for content, 15 s for the
288
374
  `.md` copy of a page it redirected to).
289
375
  - No Help search. Help's `robots.txt` disallows it. Help's sitemaps list article ids but not titles.
290
376
  - It reads the current docs, which don't always name their release.
291
377
  - It follows at most one link from a page it read. An answer that sits further away comes back as a gap.
378
+ - On a long list or table, the agent now and then stops a row early, for example five of six integration patterns.
379
+ Every quote it gives is still right; the missing row is the problem.
292
380
  - If a site starts blocking headless browsers, the agent stops and reports it. It does not try to get around the block.
293
381
  - What leaves your machine: the pages the agent fetches from those sites (and each site's `robots.txt`, which raw
294
382
  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` (`/apex/HTViewHelpDoc?id=<id>.htm` 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` |
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>/` |
@@ -43,8 +43,8 @@ that, the printed output is the limit, not time.
43
43
  `duplicate-of <url>` means the body matches an earlier URL: read only the first copy. `same <url> as <url>` means
44
44
  two arguments are one page, fetched once. `(cached)` means it came from disk. Status `0` carries the error.
45
45
  `[redirected <url>]` means the page moved and the Source line names where: check that it is the page you asked for,
46
- since a mistyped atlas topic redirects to its guide's landing page, which is not evidence. That rule also decides a
47
- line that adds `[twin <path>]` (the redirected page read as its `.md`) or `[twin failed: <reason>]` (its rendered
46
+ since a mistyped atlas topic redirects to its guide's landing page, which is not evidence; in the Apex Developer
47
+ Guide, which moved to the new docs, it answers 404 and nothing is saved. That rule also decides a line that adds `[twin <path>]` (the redirected page read as its `.md`) or `[twin failed: <reason>]` (its rendered
48
48
  text kept); a cached run shows `(cached) [redirected <url>]` and no `[twin]`. `not saved` means a shell, login wall or missing page (Help
49
49
  answers 200 for a missing article), or `[api docs did not load]` on an API reference page: not evidence, but a rerun
50
50
  may work. `not saved: <code>` means the page loaded but the cache write failed: rerun. `not saved: left the doc
@@ -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 180 MB)? [y/N] ')) {
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
- const low = i || !ls[0].startsWith('Source: ') ? line.toLowerCase() : '', hit = t.filter(s => low.includes(s)); // a page's line 1 is its Source line; a sidecar has none
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.pathname !== location.pathname) links.add(`${a.pathname}${a.search} ${(a.innerText || '').trim().replace(/\s+/g, ' ').slice(0, 80)}`);
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 && md ? lean(r.body) : r.body;
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('On Linux, also run once: npx playwright-core install-deps chromium');
376
+ console.error(`On Linux, also run once: node "${cli}" install-deps chromium`);
371
377
  process.exit(3);
372
378
  }
373
379
  }
@@ -445,8 +451,8 @@ try {
445
451
  if (got.sel === 'body') got = await page.evaluate(extract, [CONTENT, true])
446
452
  .catch(e => { console.error(`fallback failed on ${u.href}: ${e.message.split('\n')[0]}`); return got; });
447
453
  const { sel, text, links, hold } = got;
448
- // A redirect is shown and cited: a mistyped atlas topic lands on its guide's landing page with status 200, and a
449
- // Help article can move to another ?id= on the same path.
454
+ // A redirect is shown and cited: a mistyped atlas topic lands on its guide's landing page with status 200 (a 404 in
455
+ // a guide that moved to the new docs), and a Help article can move to another ?id= on the same path.
450
456
  const to = new URL(page.url()), hop = sameDoc(to, u) ? '' : ` [redirected ${to.href}]`;
451
457
  // Old atlas URLs (and the LDS home) redirect to pages that have a .md twin: keep the rendered links, read the twin.
452
458
  const moved = hop && twinOf(to);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "sf-docs-agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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.56.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": ">=18.17"
23
+ "node": ">=22"
24
24
  },
25
25
  "keywords": [
26
26
  "claude-code",