MDN renders its search results client-side, so 'oc mdn search' has gone
through DuckDuckGo with a site: filter. But the page it renders gets its
results from a public JSON endpoint, /api/v1/search, which returns the
site's own ranking with a summary per result in one ~11KB response. A new
'api' shortcut shape names such an endpoint and which response fields
hold the list, title, URL, and snippet; the response becomes the same
synthetic results page a Sphinx search produces and rides the normal
render path, so 'do <n>' follows a result. First view is ~520 tokens and
works for any site whose search answers as JSON.
docs.python.org has no search server: its search page downloads the
whole Sphinx index (searchindex.js, ~900KB gzipped) and ranks matches
in the browser. oc now does the same ranking itself, so 'oc py search'
answers from the site's own index instead of DuckDuckGo.
The index is cached on disk for a day and never printed; what renders
is a numbered result list that rides the normal open path, so 'do <n>'
follows a result. A query that names a symbol exactly (json.dumps)
links straight to its anchor. A site definition opts in with a
'sphinx' field naming the docs root, so any Sphinx site can use the
backend later.
First three languages from #25. Each is one definition in clis/:
- oc py library json, oc py doc reference/datamodel (the .html is
appended, matching how the aws shortcuts spell pages)
- oc mdn js Array/map, oc mdn css grid-template-columns, and a generic
oc mdn doc <path> for everything else under /en-US/docs
- oc node api fs
Python renders search client-side (Sphinx), MDN serves its search page
as a client-rendered shell with only the nav in the HTML, and nodejs.org
has no search, so all three get the DuckDuckGo site: filter fallback the
aws and gcp shortcuts already use. Short names py, mdn, and node join
the alias table; python, mozilla, nodejs, and the full domains resolve
as before through the generic rules.
The feature landed with one sentence in the install paragraph, which named
the three environment variables and nothing else. Anyone actually putting oc
behind a corporate proxy had to read src/fetch.js to learn that an https
target prefers HTTPS_PROXY and falls back to HTTP_PROXY while an http target
uses HTTP_PROXY only, that a bare host:port is read as http://, that a socks
URL is refused rather than ignored, or that NO_PROXY takes suffix, wildcard,
host:port, and CIDR entries. All of that is now in a Proxies section, and
every claim in it was checked against the merged code rather than written
from the diff.
Two limits are documented instead of left to be discovered. oc does not read
ALL_PROXY, but the impers transport is libcurl underneath and reads it on its
own, so a request oc treats as direct can still leave through a proxy; the
same holds for the *.suffix, host:port, and CIDR forms of NO_PROXY, which
libcurl does not parse. Verified live against a third party proxy by watching
the egress IP: with only ALL_PROXY set, or with NO_PROXY=*.host naming the
target, oc reported a direct fetch and the request went through the proxy
anyway. And an IPv6 literal over HTTPS cannot work through a proxy today,
because URL.hostname keeps the brackets, so net.isIP reads 0 and the SNI and
certificate check both treat [2606:...] as a DNS name.
The security properties a reader would otherwise have to assume are stated:
the CONNECT tunnel still verifies the origin certificate (confirmed against
expired, self-signed, and wrong-host endpoints through a real proxy),
credentials in the proxy URL reach the proxy and nothing else including
across redirects, private and internal targets stay refused, and a name that
resolves publicly for oc and internally for the proxy is not something oc
can detect, so the proxy is trusted for its own egress policy.
The skill gets the short version, since an agent needs two things: that no
flag or setup is required, and that a "proxy failed" or "blocked" line is a
transport problem to report rather than a page to retry. llms.txt gets one
fact next to the existing transport fact.
Version bump across package.json, the lockfile, the plugin manifest, the
marketplace entry, and the npx pins in the agent skill.
Adds CHANGELOG.md covering what landed since 0.3.0: dispatched site
shortcuts, the Wikipedia shortcuts, proxy env var support, the loud exit 2
on a page with no readable content, and the MIT LICENSE file.
The skill gains a site shortcuts section, since 0.3.0 documented the
shortcuts in the README but the skill never mentioned them, plus the exit 2
contract and the proxy note. llms.txt gains Wikipedia and the proxy line.
README gains the end to end benchmark: five Wikipedia lookups run as whole
tasks in Claude Code with one tool each. All three tools answered every
task correctly, so it reports cost rather than accuracy.
Article, search, and non English wiki lookups via clis/wikipedia.org.json,
reachable as oc wiki, oc wikipedia, or oc wikipedia.org.
The article and lang commands use ?action=render, which returns the article
HTML without the interlanguage sidebar and Tools menu that otherwise eat
about half of a 500 token budget before any prose. Its links stay root
relative, so oc do <n> still follows them; the Parsoid endpoints render just
as clean but emit ./Title hrefs that resolve against the API path and break
link following.
Search uses the normal results page. The api.php JSON search endpoint is
cheaper on paper but distills to nothing today, since its results sit in a
nested query.search array.
Closes#21
The shortcuts table promised `sub <name>`, `item <id>`, `repo <owner> <name>`
and the rest for 13 sites, and clis/*.json shipped in the published `files`
list, but nothing ever read those files: `oc reddit sub ClaudeAI` answered
`unknown command 'reddit'`. The help text labels `fill` and `submit` as
planned, so an agent reading the table had every reason to treat the
shortcuts as shipped, construct one, and fall back to raw fetching when it
failed, which is the outcome this tool exists to prevent.
src/sites.js resolves `oc <site> <verb> [args]` against clis/*.json at
runtime and hands the URL to the existing open path, so a shortcut cannot
change what a page costs or how it renders, and a new definition needs no
wiring. A site answers to its domain, its bare name, and a short alias
(hn, gh, so, ddg, yt, aws, gcp, learn, finance, twitter), because an agent
that has to guess the spelling is back to guessing URLs.
Two details are worth naming. The last declared argument takes every word
after it, so `oc aws search s3 lifecycle rules` needs no quoting. And a value
filling a path segment keeps its slashes while a value in a query string does
not, so `oc learn doc azure/aks/what-is-aks` reaches that page instead of
asking the site for one impossible segment.
`oc sites` lists every site with its verbs, one line each, so discovery costs
less than a wrong guess. reddit's {sub} and github's {repo} are renamed to
{name} so the usage lines print what the README documents.
Fixes#16
README and package.json both said MIT, but with no LICENSE file the claim was
not a grant: default copyright applies, so vendoring oc, shipping it inside a
corporate toolchain, or passing a license audit were all blocked, and GitHub's
license detection reported null. The published package already carries
"license": "MIT" in its metadata, so the repository and the tarball disagreed
about what users received.
The badge now links to the file instead of an in-page anchor, which is also
what the OpenSSF Scorecard License check reads.
Fixes#15
A JS-only page, a consent wall, and a bot challenge all answer HTTP 200
with markup that carries no text, and oc reported those renders as
successes: a title, an actions line, and "100% saved" in verbose mode,
which is true of a render that saved every token by extracting none.
From the output alone an agent could not tell that from a page that is
genuinely empty, so it never fell back to anything heavier and the empty
result travelled on as evidence.
oc now prints one line on stderr and exits 2 in that case, and --json
carries the same verdict as an always-present 'empty' field, so a caller
can branch on "nothing on this page" vs "oc could not read this page"
without parsing prose. Exit 2 is distinct from the exit 1 every other
failure uses. It sets process.exitCode rather than calling process.exit
so whatever did render still finishes printing.
The thresholds in render.js are measured, not guessed. contentTokens
counts text the page wrote (prose, headings, and link or button labels
over 25 chars, which is what separates a headline from nav chrome), so a
link-list page like Hacker News or a search result still reads as
content. Against live pages the failures land at 47 and 51 tokens
(reddit.com/r/*, instagram.com) while the thinnest page the README
claims support for carries 463 (an X profile), so the floor at 25 and
the thin-vs-HTML-weight rule at 100 tokens against 2500 of markup both
sit in a wide gap. Verified with no false positives on feeds, the Stack
Exchange API, Microsoft Learn RSS, a YouTube watch page, AWS and GCP
docs, a one-line HN item, and example.com.
'oc raw' fails only on genuinely blank output, since raw is the fallback
the compact view's failure line names and must not refuse the same pages.
Closes#14
A tool call inside an agent session costs 23,000 to 33,000 tokens of
overhead whatever it prints, so the page-view win only reaches the
session total if answering a task takes fewer commands. Three places
were charging a command to say what the next command should be, each
found by capturing the command stream of a real agent run rather than
by reading the code.
A search result title is a link. Every engine puts it in an anchor
filling an <h2>, and the walk took the heading's text and returned,
dropping the href, so `do` on the most obvious number on a results page
printed the title back. The agent then spent a second command finding
the number that navigates. The href now rides along when the anchor is
the whole heading, which is the test documentation fails on purpose:
every heading in the Rust book and on an AWS CLI reference page carries
a permalink to its own id, and following one would refetch the page the
agent is already reading.
`find` pointed at its answer. With a single match it printed the block
and a number, and the agent's next command was always the `read` on
that number, so it now prints the region. With several matches it
showed a 200 character snippet of each even when the budget had room
for them whole, so it spends that room, on the same terms `FINISH`
already documents for a page that nearly fits.
A truncated block ended mid sentence. Asked for the first sentence of a
page, an agent was handed it complete, followed by a marker saying 302
characters were cut, and spent a command on `read` to find out whether
the sentence went on. The cut now falls on the last sentence that
finished inside the cap, and measured across five real pages it costs
nothing: four came out within three characters of before.
The package-lock name field catches up with the scoped package name,
which npm rewrites on any install.
Ships JSON API rendering (#3) and the AWS, Google Cloud, and Microsoft Learn
documentation shortcuts (#11) to the beta channel.
Documentation caught up with what the code actually does while it was open:
- Status said the remaining actions land in v0.2, which shipped without them.
fill, submit, and back are now marked planned in the help and the README,
a label that cannot go stale the way a version number does.
- llms.txt names the cloud documentation shortcuts and the JSON rendering.
The skill keeps its npx pin on 0.2.0. A pin is what agents actually execute,
so it moves when a release is stable, not when it enters beta.
Closes#3.
An API answer is a page: jsonToHTML turns a JSON body into one article per
item, and everything downstream (numbering, budget, do, read, next, raw)
treats it as an ordinary document. No per-site logic and no new dependency.
The compact view is the hard part, since a search response carries far more
fields than fit in 500 tokens. So the renderer scores each field by how much
it varies across items against how wide it prints, penalises fields flattened
out of a sub-object (owner.reputation describes the asker, not the answer),
and spends about 60 characters per item on the winners. What every item
shares is stated once at the bottom instead of repeated, empty fields are
named rather than printed, and what was cut says so and points at oc raw,
which keeps every field.
On the Stack Exchange search endpoint that is 30 results in ~960 tokens
against ~5,500 for the raw body, with each title a link and question_id
visible.
Also here:
- clis/stackoverflow.com.json gains search <query>, which is what #3 was
blocking. Results carry question_id, and the question feed reads one in
full, so search now completes without touching the challenged HTML page.
- fetch: the native-fetch path rejected anything that was not HTML or XML.
It now accepts JSON, which also makes the two transports render one URL
the same way, since the impers path never checked the type at all.
- raw threads the URL through so its view of an API response can be titled
and, unlike the compact view, keeps every field.
Deliberately not done, from the notes on the issue: pagination in the
actions line, and API metadata on stderr. There is no stderr channel at the
distill seam, so response-level fields (has_more, quota_remaining) render as
one footer line instead. A columns hint in the clis specs and a --json
passthrough both looked like the wrong trade: the first needs per-site
tuning for something the scoring already handles, the second would break the
machine-stable Page contract.
All three providers render their own docs search client-side, so
distilling the search page yields only nav chrome. Microsoft Learn is
the exception underneath: its public RSS search endpoint serves real
results as a feed, which the engine already renders (same route as
Stack Overflow). AWS and Google Cloud expose search only as JSON (#3),
so until the engine renders JSON their search goes through DuckDuckGo
HTML with a baked-in site: filter, the same endpoint the duckduckgo.com
CLI already relies on. Bing was tried first for that job and rejected:
it silently drops the site: operator on some queries.
Verified live: the Learn RSS endpoint returns titled results for
"app service deploy"; the DuckDuckGo route returns real
docs.aws.amazon.com pages for "lambda timeout" (it can answer with a
rate-limit challenge under rapid-fire automated use, noted in README).
Cloud provider docs are the pages agents hit most while writing
infrastructure code, and they carry the heaviest chrome: nav trees,
version pickers, feedback widgets. One config per provider gives them
tuned shortcuts instead of raw URLs.
The Google config points at docs.cloud.google.com because
cloud.google.com 301s every docs path there; skipping the redirect
saves a round trip, same trick as reddit.com going via old.reddit.com.
All six URL templates were verified against the live sites with
fetch + distill (56 to 4126 blocks, real titles, HTTP 200).
Closes#11
Addresses the indirect-prompt-injection risk category flagged by
skills.sh's Snyk audit — the tool's job is fetching third-party web
content for an agent to read, so that caveat belongs in the docs
agents actually load. Also trims SKILL.md's own prose for token cost.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds free security tooling for a public npm CLI: CodeQL static analysis
on push/PR plus a weekly scan, a Dependency Review check that blocks PRs
introducing vulnerable or malicious packages, --provenance on npm publish
(cheap given existing OIDC trusted publishing), and a weekly OpenSSF
Scorecard run with a README badge.
Trims the README to what a skimming user needs: it turns websites into
a CLI for AI agents, saves tokens, and gets past blocks on some sites.
Drops the repeated bar chart and three full result tables in favor of
one small table and a link to only-cli/benchmarks, which already carries
the full methodology and per-task numbers.
Watch pages need client JS to become interactive, but the title,
description, view count, and caption tracks already ship inline in
the initial HTML as ytInitialPlayerResponse, so this reads that
directly instead of waiting on the v0.3 headless fallback. Each
caption track becomes a numbered link, and oc do on it fetches the
timedtext transcript, collapsed into one block so it pages through
oc next/oc read like any other long document instead of costing one
block per caption line.
Adds youtubeToHTML and transcriptToHTML alongside feedToHTML in the
distiller, a youtube.com.json shortcut, and offline tests against a
fixture watch page.
Fixes#6
Scattered best-per-column checkmarks made the table hard to scan;
the prose right after it already calls out which tool wins which
column, so the marks were redundant noise on top of that.
The AAPL rows repeated the same $310.03/$302.25 price-of-AAPL figure
in the answer column, which was noise. Drop it and add an est. cost
column instead, with a footnote on how each figure is derived since
no real Codex billing data exists in this repo.
The teaser only ever showed the Reddit case. Adding the single-page AAPL
task on the same token scale shows both sides honestly in one place: oc
wins the multi-hop task outright and Claude Code's own default fails it,
but on a single static page Claude Code's default WebFetch is the
cheapest correct answer in the whole comparison.
The teaser bar only showed tokens and a word for the answer. Same numbers
as the Reddit row in the defaults table below, now with turns and a
check mark, so the bar reads standalone without needing the table to
know a run passed. The failing row also moves to the bottom, matching
how the fuller table below already orders it.
Merged the Reddit and AAPL default-tools tables into one, with a task
column, since they shared every other column and split the same
comparison across two tables for no reason. Dropped their ASCII bars too:
the teaser bar at the top of the README already draws the Reddit numbers,
so repeating them twice more added nothing. Also dropped the bar under
the Claude five-tool agent table, since it only restated that table's own
tokens and turns columns, and did so with a raw curl number that had
drifted from the table next to it.
The stock quote task made a fine single-page example but understated the
real gap: on a task with a couple of hops, Claude Code's own WebFetch and
WebSearch are blocked from reddit.com outright and it has no default shell
fallback, so it fails the task rather than just costing more. Codex's
default gets there through its own web search plus curl, at 2.7x oc's
tokens. That comparison now leads the README, with the stock quote case
kept below it as the honest counterexample where a default tool wins.
Also corrects the earlier claim that Codex reads raw HTML with curl by
default: it reaches for its own web search tool first and, on the AAPL
page, never opens the page at all, which is why it reported a stale price.
Yahoo Finance quote, news, history, lookup, markets, gainers, losers, and
trending pages all arrive server rendered (the AAPL quote page carries the
closing price in about 456 tokens of view against 325,000 of raw HTML), so
a definition ships for all eight. The README grows the comparison people
actually ask for: the same stock price task run through Claude Code and
Codex as they ship versus with oc, one live session each, wrong answer and
all. Codex read raw HTML and reported a price that is not on the page;
WebFetch's digest was cheaper than oc this run and the README says so
plainly rather than hiding it. Also a banner, badges, and an llms.txt
pointer, so both people and models skimming the repo can tell what it is.
The budget was being spent on whatever came first in the document, which on
most pages is menus. On the Reddit thread the benchmark uses, all 500 tokens
went to nav, sidebar, and moderator boxes, so an agent that opened the page to
read the discussion had to escalate to oc raw to see a single comment.
distill now finds the content (main, role=main, a single article, else the
densest run of prose) and emits it first, with the rest of the page after it
under a divider. Nothing is dropped, so do <n> still reaches every link.
Link labels that repeat down a page go with it: a thread stamps permalink,
save, and report onto every comment, which cost more than the comments. The
count and three examples are printed in place, and oc raw still has them.
Same thread, before and after: 660 blocks to 254, whole compact page 4,184
tokens to 2,712, and the first view now holds comments instead of a sidebar.
Pages small enough to print whole keep document order.
find searches the distilled page the session already holds, prints one line
per match with the number to read it by, and costs no fetch. It matches the
query as a phrase, case insensitive, and falls back to matching the words
separately when the phrase is not there.
On the reddit thread from the benchmark: 'oc find w3m' is 115 tokens against
9,670 for oc raw, and it lands on the numbers to read.
The compact view was all or nothing: an agent that needed more than the 500
token budget had only oc raw, ten to twenty times the price. Now open saves
the distilled page, next continues it where the view stopped, and read <n>
prints one region in full. Headings and text blocks long enough to be cut are
numbered so they can be addressed, and the marker prices what it left behind.
On one Reddit thread: open 475 tokens, next 455, read 88, raw 9,670.
The compact view hides link URLs because printing them is most of what
makes a page expensive, which left an agent re-fetching the same page as
--json or raw just to learn where [15] pointed. oc open now saves the
handles it numbered to a small JSON file per session under ~/.only-cli
(OC_HOME relocates it), and oc do <n> resolves one and renders the target
exactly as open would. Search engine tracking redirects are unwrapped so a
result link opens the destination instead of a script page.
Errors name the command that fixes them, since agents read them: an
out-of-range number reports the valid range, an input says to use fill, a
button says the page handles it itself.
Two hops on Hacker News cost about 3k characters this way, against roughly
23k for the re-fetch route.
Each claude -p session now loads a skill documenting its tool, so every
condition runs at its best. The multiplier on the page-view table is the
precise one: all six pages for less than half the tokens of the cheapest
rival, a single-screenshot floor.
Page-view table now includes Playwright MCP snapshots, Browser Use state
messages, rendered HTML via Playwright and Selenium, and the computer-use
screenshot floors. New table for the end-to-end claude -p benchmark: the
agent finished all three tasks with oc on the fewest tokens and lowest
cost of any full-success condition, while raw curl, Jina Reader, and
Playwright MCP each burned 13 turns on the Reddit task and returned
nothing.
The feed fixture covers Atom entries (escaped bodies, self-closed
categories, bylines) and an RSS item with a CDATA body. Raw markdown of
a feed exposed an old quirk: cleanDocument removed the head before
toMarkdown read the title, so any page whose body lacked a matching h1
lost its title in raw mode. cleanDocument now captures the title first
and returns both.
setup-node's registry-url writes an npmrc auth-token line with a
placeholder token, which npm used instead of OIDC and got a 404 from
the registry. Dropping registry-url lets trusted publishing work.
New clis: github.com (repo, user, search, trending, issues) and
linkedin.com (profile, company, jobs; public guest views work through
the Chrome fingerprint). Both verified live before shipping.