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 +120 -32
- package/agents/sf-docs.md +8 -5
- package/bin/install.mjs +1 -1
- package/fetch-docs.mjs +13 -7
- 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,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
|
|
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
|
-
##
|
|
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
|
|
39
|
-
|
|
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
|
|
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 |
|
|
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** | |
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
in
|
|
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
|
-
|
|
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
|
|
285
|
-
the
|
|
286
|
-
article, and an article under 1,500 characters can't
|
|
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` (`/
|
|
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
|
|
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
|
|
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
|
}
|
|
@@ -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
|
|
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.
|
|
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.
|
|
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",
|