sf-docs-agent 0.1.1 → 0.1.3

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,312 +1,113 @@
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
-
5
- A Claude Code subagent that answers Salesforce questions from the official docs and hands back quotes, not pages.
3
+ **Read the docs, keep your context.** A Claude Code agent that reads the official Salesforce docs for you and returns just the lines that answer, each with its official URL.
6
4
 
7
- It reads developer.salesforce.com, architect.salesforce.com, help.salesforce.com, admin.salesforce.com and
8
- lightningdesignsystem.com. The model picks the lines and a script copies them from the cached page,
9
- each with the official URL it came from, so you can check any quote with one grep.
5
+ [![npm](https://img.shields.io/npm/v/sf-docs-agent)](https://www.npmjs.com/package/sf-docs-agent)
10
6
 
11
- I built this because every docs lookup was dumping whole pages into my session. I only wanted the lines that answer my question.
7
+ **84% fewer tokens** added to your session than Salesforce's own docs skill · **117 of 117** cited lines still on today's page · **every line** checkable with one `grep`
12
8
 
13
- ## Why an agent, not a skill
9
+ [Install](#install) · [Use](#use) · [How it works](#how-it-works) · [How it compares](#how-it-compares) · [Limitations](#limitations) · [Benchmarks](https://github.com/drsaavedra/sf-docs-agent/blob/main/docs/benchmarks.md)
14
10
 
15
- A skill pulls every page it reads into your main session. The agent reads them in its own context and returns only
16
- the lines you asked for.
11
+ ---
17
12
 
18
- Salesforce's own [sf-skills](https://github.com/forcedotcom/sf-skills) library already has a skill for the same job,
19
- [`platform-docs-get`](https://github.com/forcedotcom/sf-skills/tree/main/skills/platform-docs-get). In its own words,
20
- it is an "official Salesforce documentation retrieval skill" for the same five sites, "especially when pages are
21
- JS-heavy, shell-rendered, or hard to extract with naive fetching."
13
+ | Salesforce's platform-docs-get skill · 4,578 tokens | sf-docs-agent · 303 tokens |
14
+ |---|---|
15
+ | Every page it read lands in your session | Only the lines that answer, each with its official URL |
22
16
 
23
- Here is how the two compare:
17
+ *"How many versions can a flow have?"* Same right answer. 4,578 tokens become 303.
24
18
 
25
- | | platform-docs-get | sf-docs-agent |
26
- |---|---|---|
27
- | Runtime | Python, `playwright`, `playwright-stealth` | Node, `playwright-core` |
28
- | Bot checks | spoofs a desktop user agent, optional stealth | neither |
29
- | developer, architect and LDS pages | renders and scrapes the HTML | reads the `.md` copy where one exists |
30
- | Finding pages | you give the URL | `llms.txt` indexes, sitemaps and link lists |
31
- | What lands in your context | every page | a report of quotes |
32
- | Checking an answer | re-read the page | `grep` one line |
19
+ The agent reads the pages in its own context. The model picks the lines and a script copies them word for word from the cached page, so no quote is written from the model's memory.
33
20
 
34
- The trade-off: your main session trusts the subagent. That is why the model only picks line numbers and the script
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.
21
+ I built this because every docs lookup was dumping whole pages into my Claude Code session. I only wanted the lines that answer my question.
38
22
 
39
- ## 84% less in your context, 63% fewer tokens overall
23
+ ## Install
40
24
 
41
- On 10 questions against Salesforce's own platform-docs-get skill, the agent
42
- added 84% fewer tokens to your context and used 63% fewer tokens over the whole session, with the right answer on
43
- 10 of 10.
25
+ Needs Node 22+ and Claude Code.
44
26
 
45
- Below are the measurements, each read as platform-docs-get → sf-docs.
27
+ ```
28
+ npx sf-docs-agent
29
+ ```
46
30
 
47
- | Question | Site | Tokens added to context | Saved | Session token usage | Saved |
48
- |---|---|---|---|---|---|
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%** |
31
+ Then start a new Claude Code session. The agent is listed as `sf-docs`. Add `--project` to install it for the current project only.
60
32
 
61
33
  <details>
62
- <summary>The 10 questions</summary>
63
-
64
- 1. What are the valid values for edition in a scratch org definition file?
65
- 2. What is the hourly platform event publishing allocation for Enterprise Edition and for Developer Edition?
66
- 3. With a QueryLocator start method, what is the maximum batch scope, and how many records can the QueryLocator return?
67
- 4. Which integration patterns does the architect Integration Patterns guide list?
68
- 5. Which editions include the Salesforce Connect cross-org adapter?
69
- 6. How many versions can a flow have, and how many active flows per flow type in Enterprise vs Professional?
70
- 7. What three technologies is Salesforce Headless 360 built on, and does it replace the Lightning UI?
71
- 8. Should you put an SLDS tooltip on a non-interactive element?
72
- 9. How many records fit in one Bulk API batch, and in one sObject Collections create request?
73
- 10. How many events can a Pub/Sub API client request across all FetchRequests in one Subscribe call?
34
+ <summary>Headless browser, Linux, update, remove, cache</summary>
74
35
 
75
- </details>
36
+ - **Headless browser:** some sites have no `.md` copy of their pages, so the agent opens them in a headless browser. The installer asks before it downloads Chromium (about 200 MB). Pass `--yes` to skip the question.
37
+ - **Linux:** also run `node "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/sf-docs/node_modules/playwright-core/cli.js" install-deps chromium` once.
38
+ - **Update:** `npx sf-docs-agent@latest`
39
+ - **Remove:** delete `~/.claude/agents/sf-docs.md` (or `.claude/agents/sf-docs.md` in the project), then `~/.claude/sf-docs/` once no project uses it. Chromium stays in Playwright's cache (`~/Library/Caches/ms-playwright` on macOS, `~/.cache/ms-playwright` on Linux); delete that too if nothing else uses Playwright.
40
+ - **Cache:** pages stay cached for 24 hours. Set `SF_DOCS_CACHE_HOURS` in your Claude Code `settings.json` to change it (`0` fetches every time).
76
41
 
77
- ## How sf-docs-agent reads each site
78
-
79
- The agent reads the `.md` copy of a page where the site publishes one. Not every Salesforce docs site does yet.
80
-
81
- Give the script an `.html` or bare URL for a new developer docs, architect or LDS page and it fetches the `.md` copy
82
- instead. The same goes for an atlas URL that now redirects to the new docs.
83
-
84
- The agent finds pages through the site's `llms.txt` index or its sitemaps.
85
-
86
- | Site | How | Where it finds pages |
87
- |---|---|---|
88
- | developer.salesforce.com, new docs | the `.md` copy Salesforce publishes | `/docs/llms.txt` and one index per product |
89
- | developer.salesforce.com, atlas docs (Apex, Apex Reference, Metadata API, Object Reference) | renders the page, code samples included | `/docs/llms-product-docs.txt` |
90
- | developer.salesforce.com, API reference pages (`?meta=<operation>`) | renders the page | the product's index |
91
- | architect.salesforce.com | the `.md` copy | `/llms.txt` |
92
- | help.salesforce.com, articles and knowledge articles | renders the page | links in other docs and articles, and Help's product sitemaps |
93
- | admin.salesforce.com | renders the page | `/sitemap_index.xml` and its post sitemaps |
94
- | lightningdesignsystem.com (SLDS 2) | the `.md` copy | the home page's link list |
95
- | v1.lightningdesignsystem.com (SLDS 1) | renders the page | the home page's link list |
42
+ </details>
96
43
 
97
44
  ## Use
98
45
 
99
- Ask Claude to read the Salesforce docs in your own words. You can name the agent in the prompt, or just give it a
100
- Salesforce docs link and ask. It picks up the intent either way.
46
+ Ask in your own words. Name the site or product if you know it, or paste a Salesforce docs link and ask about it.
101
47
 
102
48
  ```
103
49
  Go scan the developer.salesforce.com docs on the current release's governors and limits
104
50
  ```
105
51
 
106
- Name the site or product if you know it, and say what you want to know.
107
-
108
- 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.
109
-
110
- 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
111
- official URLs, and the session turned them into the answer.
112
-
113
- <details>
114
- <summary><b>Sonnet 5.5, effort high, through the agent</b>: 86 seconds, 34k agent tokens</summary>
115
- <br>
116
-
117
- > The `sf-docs` agent finished scanning the Apex governors and limits pages. All limit values below are quoted from the cached concept page, `apex_gov_limits.htm`. The release version is an inference, not a stated fact.
118
- >
119
- > ## Which release
120
- > - The cached pages are the unversioned "current" docs, and they don't name a release.
121
- > - The version dropdown lists **Summer '26 (API 67.0, path `262.0`)** as the newest versioned link.
122
- > - The page's own sidebar links point to `264.0` paths. That is most likely **Winter '27 (API 68.0)**.
123
- > - The agent did not fetch the 262 or 264 versioned pages to confirm.
124
- >
125
- > ## Per-transaction Apex limits
126
- > Limits are listed as synchronous / asynchronous.
127
- >
128
- > | Limit | Sync | Async |
129
- > |---|---|---|
130
- > | SOQL queries | 100 | 200 |
131
- > | SOQL rows retrieved | 50,000 | 50,000 |
132
- > | DML statements | 150 | 150 |
133
- > | DML rows | 10,000 | 10,000 |
134
- > | Callouts | 100 | 100 |
135
- > | `@future` methods per invocation | 50 | 0 in batch and future contexts, 50 in queueable |
136
- > | `System.enqueueJob` | 50 | 1 |
137
- > | Heap | 10 MB | 25 MB |
138
- > | CPU time | 10,000 ms | 60,000 ms |
139
- > | Execution time | 10 minutes | 10 minutes |
140
- >
141
- > - **Enforcement:** "the Apex runtime engine strictly enforces limits", and exceeding a limit raises a runtime exception "that can't be handled."
142
- > - **Reset and scope:** For Batch Apex, limits reset for each `execute` call. Synchronous limits apply to scheduled Apex. Bulk API transactions get the higher of the sync and async limits.
143
- > - **Heap:** Exceeding it gives an uncatchable `System.LimitException`. Email services get a 50 MB heap.
144
- >
145
- > ## Certified managed packages
146
- > - Each certified namespace gets its own per-transaction limits.
147
- > - The cumulative limit across namespaces is 11 times the per-namespace limit. That gives 1,100 SOQL queries and 1,650 DML statements.
148
- > - Heap, CPU, execution time and the unique-namespace count are not counted separately per package.
149
- >
150
- > ## Platform, static and size limits
151
- > - **Async executions per 24 hours:** 250,000, or licenses × 200, whichever is greater.
152
- > - **Concurrent long-running transactions (over 5 seconds):** one per 100 licenses.
153
- > - **Scheduled classes:** 100 (5 in Developer Edition).
154
- > - **Batch jobs queued or active:** 5.
155
- > - **Callout timeout:** 10 seconds by default.
156
- > - **Callout request or response size:** 6 MB synchronous, 12 MB asynchronous.
157
- > - **Apex code per org:** 6 MB.
158
- > - **Method size:** 65,535 bytecode instructions.
159
- > - **Single email:** 5,000 external addresses per day per org (GMT).
160
- > - **Push notifications:** 20,000 iOS and 10,000 Android per hour.
161
- > - **Governor limit email warnings:** The default threshold is 50% of allocated limits, and only per-request limits are checked.
162
- >
163
- > ## Gaps
164
- > - **Not fetched:** The agent did not read the "Elastic Limits for Asynchronous Apex Jobs (Beta)" page or `apex_limits_tips.htm`.
165
- > - **Not quoted:** Several rows on the same page weren't quoted:
166
- > - push notification calls per transaction
167
- > - Apex cursor limits
168
- > - inbound email limits
169
- > - the 450,000 `MAX_DML_ROWS` limit in tests
170
- > - **"Lightweight limits":** This isn't a section name in the docs.
171
- >
172
- > ## Sources
173
- > - https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm
174
- > - https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits_emails.htm
175
- >
176
- > The pages are cached locally, so you can check any quote with `grep -c -F '<quote>' <cache path>`. The cache is under `~/.claude/sf-docs/cache/developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/`.
177
- >
178
- > I can have the agent fetch the 262 and 264 versioned pages to pin down the release, or cover the elastic limits page.
179
-
180
- </details>
181
-
182
- <details>
183
- <summary><b>Opus 5.5, effort high, through the agent</b>: 89 seconds, 39k agent tokens</summary>
184
- <br>
52
+ Part of what came back (Opus 5.5, 89 seconds):
185
53
 
186
54
  > I read the Apex Developer Guide page **Execution Governors and Limits**. I spot-checked several of the quotes against the cached copy of the page and they match.
187
55
  >
188
- > **Which release:** the page doesn't name its release. Its links point to the 264.0 doc set, and its list of older versions starts at Summer '26 (API 67.0). So this is most likely **Winter '27 (API 68.0)**, but that's my inference, not something the page says. The page also doesn't mark any limit as new or changed in this release.
189
- >
190
- > ### Per-transaction Apex limits (synchronous / asynchronous)
191
56
  > | Limit | Sync | Async |
192
57
  > |---|---|---|
193
58
  > | SOQL queries | 100 | 200 |
194
- > | SOQL rows retrieved | 50,000 | 50,000 |
195
- > | `Database.getQueryLocator` rows | 10,000 | 10,000 |
196
- > | SOSL queries / rows per query | 20 / 2,000 | 20 / 2,000 |
197
59
  > | DML statements / rows | 150 / 10,000 | 150 / 10,000 |
198
- > | Trigger recursion stack depth | 16 | 16 |
199
- > | Callouts / cumulative callout timeout | 100 / 120 s | 100 / 120 s |
200
- > | `@future` calls | 50 | 0 in batch and future; 50 in queueable |
201
- > | `System.enqueueJob` | 50 | 1 |
202
- > | `sendEmail` calls | 10 | 10 |
203
60
  > | Heap | 10 MB | 25 MB |
204
61
  > | CPU time | 10,000 ms | 60,000 ms |
205
- > | Maximum execution time | 10 min | 10 min |
206
- > | `EventBus.publish` calls (publish immediately) | 150 | 150 |
207
- > | Apex cursor rows / `Cursor.fetch` calls | 50M / 100 | 50M / 100 |
208
- > | Pagination cursors: rows / instances / rows per page | 100,000 / 50 / 2,000 | same |
209
- >
210
- > **Notes that change how these apply:**
211
- > - **Scheduled Apex** runs under the synchronous limits.
212
- > - **Bulk API** transactions get the higher of the sync and async values.
213
- > - **Parent-child subqueries** each count as an extra query.
214
- > - **Custom metadata queries** don't count toward the SOQL limit.
215
- > - **Email services** get a 50 MB heap.
216
- > - **CPU time** leaves out time spent waiting on the database and on callouts.
217
- > - **Batch Apex** limits reset for each `execute` chunk.
218
- >
219
- > ### Org-wide Platform limits
220
- > - **Async executions per 24 hours:** 250,000 or 200 × licenses, whichever is greater. Batch Apex checks up front that the whole job fits under this limit before it starts.
221
- > - **Elastic async executions (beta)** can add up to 2 million more per day.
222
- > - **Cursors per 24 hours:** 10,000 cursor instances, 100M cursor rows, and 200,000 pagination cursors.
223
- > - **Long-running synchronous transactions (over 5 s):** 1 per 100 licenses, minimum 10 and maximum 50.
224
- > - **Scheduled classes:** 100 (5 in Developer Edition).
225
- > - **Batch Apex:**
226
- > - Flex queue jobs in Holding status: 100
227
- > - Jobs queued or active: 5
228
- > - Concurrent `start` methods: 1
229
- > - **Test classes queued per 24 hours:** the greater of 500 or 10× your test classes in production (20× in sandbox and Developer Edition).
230
62
  >
231
- > ### Static and size limits
232
- > - **Callouts:** 10 s default timeout; request or response up to 6 MB sync or 12 MB async, and this counts toward heap.
233
- > - **SOQL run time:** 120 s.
234
- > - **Deployments:** 7,500 code units each.
235
- > - **Trigger batch size:** 200, or 2,000 for platform events and Change Data Capture.
236
- > - **`QueryLocator`:** 50M records.
237
- > - **Code size:**
238
- > - 1M characters per class or trigger.
239
- > - 6 MB of Apex per org (10 MB in scratch orgs). Support can raise it, and managed-package and `@isTest` code don't count.
240
- > - 65,535 bytecode instructions per method.
241
- >
242
- > ### Certified managed packages
243
- > - Each certified namespace gets its own copy of the per-transaction limits.
244
- > - The total across namespaces is capped at 11× the per-namespace limit: for example, 1,100 SOQL queries or 1,650 DML statements.
245
- > - Heap, CPU time, execution time and the number of namespaces are shared across the whole transaction.
246
- > - Code in non-certified packages counts against your org's own limits.
247
- >
248
- > ### Other limits on the page
249
- > - **Single email:** 5,000 external addresses per day and 150 recipients per message.
250
- > - **Push notifications:** 20,000 iOS and 10,000 Android per hour per org.
251
- > - **Tests:** 450,000 DML rows per synchronous test run.
252
- > - **ConnectApi:** each write costs one DML statement.
253
- > - **Data.com Clean:** 200 SOQL queries and 10 future calls per batch.
254
- >
255
- > **Not covered:** I didn't read the Asynchronous Callout Limits page or the Winter '27 release notes. If you need to know what changed since Summer '26, the release notes are the next place to look.
256
- >
257
- > **Source:** https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm
63
+ > **Not covered:** I didn't read the Asynchronous Callout Limits page or the Winter '27 release notes.
258
64
 
259
- </details>
65
+ It tells you what it did not read. A gap you can see beats a guess you can't.
260
66
 
261
- ## Install
67
+ It covers developer.salesforce.com, architect.salesforce.com, help.salesforce.com, admin.salesforce.com and lightningdesignsystem.com. Full outputs on Opus and Sonnet: [docs/examples.md](https://github.com/drsaavedra/sf-docs-agent/blob/main/docs/examples.md)
68
+
69
+ ## How it works
262
70
 
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).
71
+ 1. **Finds the page.** It searches the site's `llms.txt` index or sitemaps, or starts from the link you give it.
72
+ 2. **Reads the cleanest copy.** Where Salesforce publishes a `.md` copy of a page (the new developer docs, architect, SLDS 2), it reads that. Help, admin and some older guides have none, so it renders them in a headless browser. No fake user agent, no stealth plugin.
73
+ 3. **Picks lines, not prose.** The model reads the cached page in the agent's own context and picks line numbers. A script copies those lines, each with its official URL.
74
+ 4. **Names what it missed.** A page it did not read, or a link it did not follow, comes back as a gap.
75
+
76
+ To check a line, replace `<cache path>` (the agent's Sources name it) and `<quote>` below. Apostrophes are safe, and the cache saves curly quotes as straight ones, so a quote you retype still matches. Keep one quote on one line with no blank line around it, since grep counts a blank line as matching every line. Any count above 0 means the line is on the page.
265
77
 
266
78
  ```
267
- npx sf-docs-agent # for all your projects
268
- npx sf-docs-agent --project # for the project in the current folder only
79
+ grep -c -F -f - <cache path> <<'EOF'
80
+ <quote>
81
+ EOF
269
82
  ```
270
83
 
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.
84
+ Every site, and how it reads each one: [docs/how-it-works.md](https://github.com/drsaavedra/sf-docs-agent/blob/main/docs/how-it-works.md)
275
85
 
276
- Then start a new Claude Code session. The agent is listed as `sf-docs`.
86
+ ## How it compares
277
87
 
278
- **Update:** `npx sf-docs-agent@latest`. Without `@latest`, npx runs any copy already installed globally or in the
279
- current project.
88
+ Salesforce has two official options for the same job. I ran both on the same 10 questions.
280
89
 
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`.
90
+ **[platform-docs-get](https://github.com/forcedotcom/sf-skills/tree/main/skills/platform-docs-get)**, a skill in Salesforce's sf-skills library, pulls every page it reads into your session. Against it, sf-docs-agent added 84% fewer tokens to your session and used 34% fewer tokens overall. Both got 39 of 40 runs right.
284
91
 
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):
92
+ **[Salesforce Docs MCP](https://labs.agentforce.com/docs/salesforce-docs-mcp)** searches a closed index with no dates on its results. It is the same trade-off you accept with [Context7](https://github.com/upstash/context7), whose README says its parsing and crawling engines are private: you can't see what the index holds or when it was taken. In my runs it returned no page from admin.salesforce.com or SLDS 2. In a subagent it got 24 of 30 right against sf-docs-agent's 29 of 30, and only 135 of its 177 quotes were on today's page, against 117 of 117. Called from the main session it got 28 of 30, but added 10 times as many tokens to your session.
287
93
 
288
- ```json
289
- { "env": { "SF_DOCS_CACHE_HOURS": "72" } }
290
- ```
94
+ **Where sf-docs-agent loses:** the MCP in a subagent is faster and cheaper: 218 seconds against 346 for all 10 questions, and 29% fewer tokens. If you are fine trusting the index, it is a good choice. I wanted something I control and can check against the live sites.
95
+
96
+ My numbers are from a rerun on 2 November 2026, after the agent started following two links. The other tools ran on 30 and 31 October 2026 and were not rerun, and the docs changed in between.
97
+
98
+ Every number, per question, with the method: [docs/benchmarks.md](https://github.com/drsaavedra/sf-docs-agent/blob/main/docs/benchmarks.md)
291
99
 
292
100
  ## Limitations
293
101
 
294
- - On help, admin, SLDS 1, API reference pages and the atlas guides, the agent finds the article text by where it sits on
295
- the page. If Salesforce changes a layout, it reads the largest block of text instead, which can be one section of the
296
- article, and an article under 1,500 characters can't be read. A daily check in this repo flags the change.
297
- - 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
298
- `.md` copy of a page it redirected to).
299
- - No Help search. Help's `robots.txt` disallows it. Help's sitemaps list article ids but not titles.
300
- - It reads the current docs, which don't always name their release.
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.
304
- - If a site starts blocking headless browsers, the agent stops and reports it. It does not try to get around the block.
305
- - What leaves your machine: the pages the agent fetches from those sites (and each site's `robots.txt`, which raw
306
- fetches load first). No telemetry. Installing downloads `playwright-core` from npm and, if you agree, Chromium from
307
- Playwright's servers.
308
- - The agent has Bash. Its brief limits it to the script and the cache, but that is an instruction to the model, not a
309
- sandbox.
102
+ - Help, admin and some older developer guides have no `.md` copy, so those pages are slower. If Salesforce changes their layout, the agent may read only part of an article. A daily check in this repo flags it.
103
+ - No Help search, because Help's `robots.txt` disallows it.
104
+ - From a page it found, it follows at most two more links in a row, and only toward a missing part of the answer. Anything further comes back as a gap.
105
+ - On a long list it sometimes stops a row early. The quotes it gives are still right.
106
+ - If a site blocks headless browsers, it stops and tells you. It does not try to get around the block.
107
+ - The agent has Bash. Its instructions limit it to its script and cache, but that is an instruction to the model, not a sandbox.
108
+ - No telemetry. Only the docs pages it fetches leave your machine.
109
+
110
+ How it reads each site, and the full list: [docs/how-it-works.md](https://github.com/drsaavedra/sf-docs-agent/blob/main/docs/how-it-works.md)
310
111
 
311
112
  ## Develop
312
113
 
@@ -317,8 +118,9 @@ sed "s|/path/to/sf-docs-agent|$PWD|g" agents/sf-docs.md > "${CLAUDE_CONFIG_DIR:-
317
118
  npm test
318
119
  ```
319
120
 
320
- `agents/sf-docs.md` names its script and cache as `/path/to/sf-docs-agent`. The `sed` line writes your copy with your
321
- clone's path; the installer does the same for everyone else. Run it again after you edit the agent.
121
+ `agents/sf-docs.md` names its script and cache as `/path/to/sf-docs-agent`. The `sed` line writes your copy with your clone's path. Run it again after you edit the agent.
122
+
123
+ **Release:** open a PR that only bumps the version (`npm version patch --no-git-tag-version`, which changes `package.json` and `package-lock.json`). Merging it runs [`release.yml`](.github/workflows/release.yml): the tests, an install of the packed tarball, `npm publish` through npm trusted publishing (no token), a check that npm holds the tarball that was tested, then the `v<version>` tag and GitHub Release.
322
124
 
323
125
  ## License
324
126
 
package/agents/sf-docs.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sf-docs
3
- description: Answers a question from official Salesforce docs (developer, architect, help, admin and lightningdesignsystem.com) with verbatim quotes the script copies from the cached page, each tied to its official URL and line. Use it proactively whenever the user wants something read, scanned, checked or looked up in Salesforce documentation, even without naming this agent ("what does Help say about X", "check the Apex guide for Y", "scan the docs on Z"), and when you do not know which page holds the answer. Brief it with the question, the site or product if known, and the width ("the one concept page" or "the whole doc set"). Delegate even for one page you already know, with its URL in the brief: a page is thousands of tokens in your context and the agent's report is about 400. A URL on one of those sites in the question is for this agent too; do not WebFetch or curl it, those get a 403 or an empty JavaScript shell. Show the user its Answer and Sources. To check a quote, run `grep -c -F '<quote>' <cache path>`. Reads docs only. Not for org state.
3
+ description: Answers a question from official Salesforce docs (developer, architect, help, admin and lightningdesignsystem.com) with verbatim quotes the script copies from the cached page, each tied to its official URL and line. Use it proactively whenever the user wants something read, scanned, checked or looked up in Salesforce documentation, even without naming this agent ("what does Help say about X", "check the Apex guide for Y", "scan the docs on Z"), and when you do not know which page holds the answer. Brief it with the question, the site or product if known, and the width ("the one concept page" or "the whole doc set"). Delegate even for one page you already know, with its URL in the brief: a page is thousands of tokens in your context and the agent's report is about 400. A URL on one of those sites in the question is for this agent too; do not WebFetch or curl it, those get a 403, an empty JavaScript shell or a raw HTML page. Show the user its Answer and Sources. To check a quote, run `grep -c -F -f - <cache path>` with the quote on stdin through a quoted heredoc (`<<'EOF'`), so an apostrophe in it cannot break the shell: one quote on one line, no blank line, since grep counts a blank line as matching every line. Reads docs only. Not for org state.
4
4
  model: claude-sonnet-5-5
5
5
  tools: Bash, Read
6
6
  ---
@@ -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,13 +85,17 @@ 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
+ Leave quote marks out of a grep term and search the words on either side: a `'` ends the shell quote, and a
89
+ curly one from the question misses a page saved with straight ones.
88
90
  Never read a page whole. When the answer is a list or a table (patterns, editions, values), Read from its first
89
91
  item to the next heading before you quote: a `--terms` match list stops at its cap, and the last rows may not
90
92
  hold your term.
91
- 4. **One hop.** If a matched line, a `.links.txt` match or a sentence you will quote names another page that holds
92
- part of the answer (a child article, a "Limitations for ..." heading with no text under it, an index entry whose
93
- title matches the question, another post), fetch it now with `--terms`. One hop only: a page named by that page
94
- stays a Gap. A parent Help article often lists no editions or permissions, and its child article does.
93
+ 4. **Two hops at most.** If a matched line, a `.links.txt` match or a sentence you will quote names another page that
94
+ holds part of the answer (a child article, a "Limitations for ..." heading with no text under it, an index entry
95
+ whose title matches the question, another post), fetch it now with `--terms`. From that page, follow one more link
96
+ the same way when that link names the part still missing. A page named by the page the second hop reached stays a
97
+ Gap. Follow a link only for a missing part of the answer. A parent Help article often lists no editions or
98
+ permissions, and its child article does.
95
99
  5. **Quote.** Pick the line numbers that carry the answer, from the matches or the Read. Then let the script copy them:
96
100
  `node "/path/to/sf-docs-agent/fetch-docs.mjs" --quote <cache file>:<line>,<line> [<cache file>:<line> ...]`
97
101
  (cache file relative to the cache dir or absolute). It prints the Evidence and Sources blocks verbatim, with the
@@ -102,7 +106,8 @@ Every tool call costs you the whole conversation again, so use as few Bash calls
102
106
 
103
107
  - Run the script as written. Do not add a stealth plugin, a spoofed user agent or any other bot-check bypass:
104
108
  plain headless Chromium gets 200 today. If the site answers 403 or a challenge page, stop and report
105
- `BLOCKED: <status>`; do not try another route. WebFetch and curl get 403 and are not routes.
109
+ `BLOCKED: <status>`; do not try another route. WebFetch and curl are not routes: they get a 403, an empty
110
+ JavaScript shell on Help, or raw HTML with no Source line for `--quote`.
106
111
  - If the script prints a `BLOCKED:` line, or Node cannot find `playwright-core`, stop and report the error lines as
107
112
  printed (after `BLOCKED:` they include the install command). Do not install anything yourself.
108
113
  - Bash is for running the script and reading the cache dir. Write nothing anywhere else.
@@ -114,7 +119,8 @@ Every tool call costs you the whole conversation again, so use as few Bash calls
114
119
  ## Reading discipline
115
120
 
116
121
  - Go to the exact concept page. A landing, overview or index page that only links onward is not evidence. The
117
- caller's width ("the one concept page") still allows the child pages that hold what the question asks for.
122
+ caller's width ("the one concept page") still allows the linked pages, up to two hops (step 4), that hold what the
123
+ question asks for.
118
124
  - Quote verbatim. Carry every option an enumerating sentence lists, and quote any sentence that limits the scope
119
125
  of the one you rely on ("X is intended only for Y").
120
126
  - Two lines that disagree, on two pages or on one: quote both, name the files, do not pick one.
package/fetch-docs.mjs CHANGED
@@ -74,8 +74,9 @@ const SOURCE = /^Source: [^\n]*\n/;
74
74
  const isShell = b => /^\s*<(!doctype|html)/i.test(b);
75
75
  // A twin is missing (render instead, and remember it) on a 404, or a 200 whose body is an HTML shell or empty.
76
76
  const missingTwin = (status, body) => status === 404 || (status === 200 && (isShell(body) || !body.trim()));
77
- // One page, whatever the #anchor or trailing slash; a Help article is told apart by its ?id=.
78
- const sameDoc = (a, b) => a.origin === b.origin && a.pathname.replace(/\/$/, '') === b.pathname.replace(/\/$/, '')
77
+ // One page, whatever the scheme (0.1.2 saved http:// Source lines), #anchor or trailing slash; a Help article is
78
+ // told apart by its ?id=.
79
+ const sameDoc = (a, b) => a.host === b.host && a.pathname.replace(/\/$/, '') === b.pathname.replace(/\/$/, '')
79
80
  && a.searchParams.get('id') === b.searchParams.get('id');
80
81
  // Paths the agent will paste into Git Bash on Windows: forward slashes, which Node accepts too.
81
82
  const slash = p => p.split(sep).join('/');
@@ -119,8 +120,9 @@ const grep = (file, t) => {
119
120
  if (args[0] === '--quote') {
120
121
  const files = [], ev = [];
121
122
  for (const spec of args.slice(1)) {
122
- const at = spec.lastIndexOf(':');
123
- const file = isAbsolute(spec.slice(0, at)) ? spec.slice(0, at) : join(out, spec.slice(0, at));
123
+ const m = spec.match(/^(.+):(\d+(?:,\d+)*)$/); // greedy: a Windows C:\... colon stays in the path
124
+ if (!m) { console.error(`not <cache file>:<line>[,<line>...]: ${spec}`); continue; }
125
+ const file = isAbsolute(m[1]) ? m[1] : join(out, m[1]);
124
126
  // Only cached official pages: inside the cache dir and starting with their Source: line.
125
127
  const inside = relative(out, file);
126
128
  let lines;
@@ -128,7 +130,7 @@ if (args[0] === '--quote') {
128
130
  catch { lines = []; }
129
131
  if (!lines[0]?.startsWith('Source: ')) { console.error(`not a cached page: ${slash(file)}`); continue; }
130
132
  if (!files.includes(file)) files.push(file);
131
- for (const n of spec.slice(at + 1).split(',').map(Number)) {
133
+ for (const n of m[2].split(',').map(Number)) {
132
134
  const text = (lines[n - 1] || '').trim();
133
135
  if (text && n > 1) ev.push(`- "${text}" [${files.indexOf(file) + 1}:${n}]`);
134
136
  else console.error(`skipped ${slash(relative(out, file))}:${n}: empty, out of range or the Source line`);
@@ -186,7 +188,7 @@ const parse = a => {
186
188
  const given = [], refused = [];
187
189
  for (const a of args) {
188
190
  const u = parse(a);
189
- if (u && SITES.includes(u.host)) given.push(u);
191
+ if (u && SITES.includes(u.host)) { u.protocol = 'https:'; given.push(u); } // all https: one key, one Source line
190
192
  else refused.push(u ? `${u.href}: not an official doc site` : `${a}: not a URL`);
191
193
  }
192
194
  // New developer docs, architect and LDS pages have a .md twin: fetch it raw rather than render the HTML page.
@@ -451,8 +453,8 @@ try {
451
453
  if (got.sel === 'body') got = await page.evaluate(extract, [CONTENT, true])
452
454
  .catch(e => { console.error(`fallback failed on ${u.href}: ${e.message.split('\n')[0]}`); return got; });
453
455
  const { sel, text, links, hold } = got;
454
- // A redirect is shown and cited: a mistyped atlas topic lands on its guide's landing page with status 200, and a
455
- // Help article can move to another ?id= on the same path.
456
+ // A redirect is shown and cited: a mistyped atlas topic lands on its guide's landing page with status 200 (a 404 in
457
+ // a guide that moved to the new docs), and a Help article can move to another ?id= on the same path.
456
458
  const to = new URL(page.url()), hop = sameDoc(to, u) ? '' : ` [redirected ${to.href}]`;
457
459
  // Old atlas URLs (and the LDS home) redirect to pages that have a .md twin: keep the rendered links, read the twin.
458
460
  const moved = hop && twinOf(to);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sf-docs-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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": {