Commit Graph
92 Commits
Author SHA1 Message Date
only-cli 537ddf7d3b docs: copy edits 2026-08-24 21:33:14 -04:00
only-cli 4b6b81f8a0 release: 0.5.0 2026-08-24 21:30:31 -04:00
only-cli 870ff2cf73 merge main: authenticated sessions via per-session cookie jars 2026-08-24 21:25:44 -04:00
only-cli b0d99cdc14 docs: list the new language references in llms.txt and the changelog 2026-08-24 16:07:11 -04:00
only-cli 86ac90c35f skill: say the docs-search note in a third of the tokens
The skill is paid for on every load, so the note naming which search
verbs run locally now says it in one clause instead of four.
2026-08-24 16:00:49 -04:00
only-cli c880918f3b merge main: scorecard hardening and the fetch/render bounding fixes 2026-08-24 14:00:06 -04:00
only-cli b18e9e5179 fix: cap page-written scalars at the render boundary
The title and every heading are the page's to write, and both skipped
truncate(), so one hostile scalar could print unbounded output whatever
the budget said. The compact view now cuts titles, headings, and input
names at the same cap and marker every other block gets, and read cuts
even a first block bigger than its whole budget, since 'up to N tokens'
is a promise the page must not be able to break. The distilled page
keeps the full values: --json stays the machine-stable view, bounded by
the fetch cap, and machines cut for themselves.

Fixes #28
2026-08-24 13:59:27 -04:00
only-cli 886ce58e27 fix: judge an unreadable page by evidence, not by length alone
contentFailure called every render under 25 tokens a failure, which
made exit 2 claim a terse page was JavaScript-only, gated, or
challenged when it had simply arrived terse. The verdict now needs
evidence: nothing extracted is empty whatever the page weighed, and a
short render is only a failure when the markup behind it was far too
big to have carried only that. A status endpoint or a one-line answer
now exits 0, script-only shells and consent walls still exit 2, and
raw applies the same rule before refusing. The --json empty field
follows the same policy, so callers keep a machine-stable distinction
between empty and merely short.

Fixes #29
2026-08-24 13:59:27 -04:00
only-cli 8e645bc4ea fix: bound response bodies before buffering them
Both transports buffered a text response whole before the distiller saw
it, so a hostile URL was an unbounded allocation. Every transport now
enforces one cap, 25MB decoded, three times the largest body oc fetches
on purpose. Content-Length is checked first to fail before the bytes
arrive, and because that header is optional and untrusted, the stream
is counted as it lands too: native fetch and the proxy transport abort
the transfer at the cap, and impers, which buffers inside its own
binding, is checked before its body travels any further. Decoded bytes
are what get counted, which is what stops a decompression bomb.

Fixes #27
2026-08-24 13:59:11 -04:00
only-cli 793e108a5c chore: drop the npm self-upgrade from the publish workflow
Node 24 has bundled an npm new enough for trusted publishing since
24.4, so upgrading npm at publish time only added an unpinned install
to the release path. If a runner ever serves an older 24.x, npm publish
fails loudly rather than shipping anything.
2026-08-24 12:49:50 -04:00
only-cli bbe894bb96 chore: resolve the Scorecard alerts a repo change can fix
Every GitHub action is now pinned to a full commit SHA with its version
in a trailing comment, verified against each upstream repo's tags, so a
retagged release cannot swap code under CI. The publish workflow pins
npm to an exact version instead of @latest, the experiment Dockerfile
pins its base image by digest, and codeql.yml gets a top-level read-all
permissions block so the default token grants nothing the analyze job
does not ask for.

SECURITY.md points reports at GitHub private vulnerability reporting,
which is now enabled, and scopes out experiments/. Branch protection on
main now blocks force pushes and deletions.
2026-08-24 12:47:19 -04:00
only-cli 378d566765 feat: add Go, Rust, Java, PHP, TypeScript, and C/C++ docs shortcuts
The rest of the languages from #25, each probed for the cheapest shape
that works:

- go: pkg.go.dev renders search on the server, so 'search' is a plain
  URL and 'pkg <path>' opens a package page.
- php: manual-lookup.php sends an exact function name straight to its
  manual page, so 'fn array_map' lands on the function doc; fuzzy
  search goes through DuckDuckGo since the suggestions render behind
  the manual's own TOC.
- cpp: cppreference's new static site has clean /cpp/ and /c/ paths;
  its native search page is bare links oc refuses as unreadable, so
  'search' goes through DuckDuckGo.
- rust: std and doc page opens; rustdoc's search-index.js format is
  version-fragile, so 'search' goes through DuckDuckGo.
- java: Javadoc opens pinned to javase/26, the current GA (27
  redirects there); the typeSearchIndex file lacks the module names a
  URL needs, so 'search' goes through DuckDuckGo.
- ts: handbook page opens; the site searches via Algolia client-side,
  so DuckDuckGo again.
- learn gets a 'dotnet <api>' verb, since .NET API docs already live
  there.

README table and prose and the skill notes cover the new sites.
2026-08-24 12:27:17 -04:00
only-cli fa41abbb2e feat: rank Ruby docs search locally via the RDoc index
RDoc publishes a site's whole search index as one static JS file, the
same way Sphinx and the Node.js docs do, so 'oc ruby search' fetches
docs.ruby-lang.org's index (en/3.4, where the file exists), caches it
under the shared day cache, and ranks it with the same scorer the Node
backend uses. Class methods print as Class.name(params), instance
methods as Class#name(params), and every entry links straight to its
method anchor. 'oc ruby class Array' opens a class page directly.

cli.js now dispatches the local search backends through one map, and
the site-resolution guard keys on the query field, which only a search
shape ever carries.
2026-08-24 12:27:05 -04:00
only-cli 6a399cf448 feat: rank Node.js docs search locally from the docs' own reference
nodejs.org has no search results page: the site's search box is a
JavaScript modal asking a third-party service, so the search verb went
through DuckDuckGo. But the API docs publish their entire reference as
one static JSON file, all.json, much the way a Sphinx site publishes
its search index, so a new nodedoc backend ranks that file locally:
every module, class, method, property, and event heading becomes a
result linking to its own anchor, and oc node search prints them as a
normal numbered page.

The file is ~8MB (~1MB over the wire) and static, so the day cache the
Sphinx backend used moves to a shared cache module both backends call:
one directory per backend, one file per host, parsed before written so
a block page never poisons it.

A typical result list costs under 100 tokens and answers from disk in
under 100ms once cached.
2026-08-24 10:49:46 -04:00
only-cli 5706aab2a1 let MDN search ask MDN
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.
2026-08-24 10:29:28 -04:00
only-cli e9d5ad4660 walk the title instead of regexing it
CodeQL cannot see that the optional '>' already keeps every bracket out
of the flattened title, and it re-flags each shape of the regex. The
character walk does the same thing in a form whose completeness is plain
to read: nothing between or including brackets survives, closed or not.
2026-08-24 10:20:21 -04:00
only-cli d56ddb30a0 fix: never let a '<' survive title flattening
CodeQL flagged the tag strip in plainTitle: requiring a closing '>' means
a title whose markup never closes keeps its '<script' fragment. The '>'
is optional now, so every '<' starts a strip and none reaches the results
page, and the output is trimmed. Both call sites already HTML-escape the
result; this hardens the flattening itself. Covered by a new test.
2026-08-24 10:18:05 -04:00
only-cli e834309363 prototype a Sphinx search backend for the Python docs
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.
2026-08-24 09:59:34 -04:00
only-cli 4ae8cffdd2 readme: benchmark table covers the three new documentation pages 2026-08-24 09:44:56 -04:00
only-cli f3f5a93466 add Python, MDN, and Node.js documentation shortcuts
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.
2026-08-24 09:26:41 -04:00
only-cli c5bf068b9e give cli.js the exec bit
package.json points bin.oc at src/cli.js, so an npm link resolves the
global oc to this file directly and running it needs the bit. Without
it every oc invocation dies with Permission denied while node src/cli.js
keeps working, which is easy to miss.
2026-08-24 09:24:16 -04:00
only-cli a1b89d4c53 skill: copy edits 2026-08-24 09:13:13 -04:00
only-cli 672dce3e60 readme: sync the benchmarks section to the 12-task 0.4.0 run
Quote the new page view totals (oc 9,487 across twelve pages, Jina
90,929, raw fetch 1,183,149) with the run date and version.
2026-08-24 09:13:05 -04:00
only-cli b084e080bb ci: make a stable release refresh the skills.sh page
skills.sh renders skills/web-browsing-cli/SKILL.md straight from GitHub, and
the release checklist said there was nothing to do for it because the skills
CLI reads that file live off main. That is true of the install path and false
of the page: the page showed the 0.2.0 pin from 2026-08-20 while main had
already shipped 0.3.0 and 0.4.0 the same day, so every reader was handed a
two-release-old install command.

The site offers exactly one lever. Its documented API is read only, with no
refresh or re-index endpoint, and the skills CLI has no publish or sync
command; a repository is re-read after the telemetry service sees an install
from it, and repo pages are cached on top of that. So a stable publish now
runs one `skills add` against the repo, which is the invocation the
install-remove-loop experiment already proved out. It costs one install on the
counter per release, which is the price of the only mechanism there is. The
job is continue-on-error and runs after npm publish has already succeeded: a
page that catches up late is a smaller problem than a red release.

The refresh is worthless if the pin it publishes is stale, which is the actual
root cause here, so a latest publish now fails when SKILL.md disagrees with
package.json. Beta and dev skip the check, because a pin moves when a release
is stable rather than when it enters beta, which is the rule 0.3.0-beta.1
already followed. Verified both ways against the current tree: it passes on
0.4.0 with a matching pin and refuses a 0.4.0 release still pinning 0.2.0.

The channel the earlier step resolves is now a job output, so the refresh job
can gate on it instead of re-deriving it from the version string.
2026-08-23 23:40:08 -04:00
only-cli b71ca15eba docs: document the proxy support that shipped in #17
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.
2026-08-23 23:40:00 -04:00
only-cli 433bc82df6 release: 0.4.0
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.
2026-08-23 23:22:46 -04:00
only-cli d5e12ab710 feat: add wikipedia site shortcuts
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
2026-08-23 22:58:56 -04:00
only-cli 7f09363963 feat: dispatch the site shortcuts the README already documented
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
2026-08-23 20:54:55 -04:00
only-cli a55d64c576 docs: add the MIT license text the badge was only claiming
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
2026-08-23 20:50:20 -04:00
only-cli 8f0716ab11 fix: fail loud when a page distills to no readable content
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
2026-08-23 20:49:14 -04:00
only-cli 1103798914 release: 0.3.0 2026-08-23 09:22:16 -04:00
only-cli 5567b31d6a test: prove the redirect guard without a third party
The test for revalidating redirect hops drove httpbin.org, so httpbin being
down failed the suite. It is down now, returning 503, which fails CI on main
and would have failed the release: the publish workflow runs npm test before
it ships, so a stable version could not have reached npm while a third
party's app tier was unwell.

It was also testing less than it looked. Each transport carried its own copy
of the redirect loop, and the live test only ever exercised whichever one was
installed, so the guarantee held in one copy and was unproven in the other.
A check that matters twice is a check a change can fix once.

Both transports now share one loop that takes the request as a callback,
which is what makes the hop check provable against a transport that never
leaves the process. Three offline tests replace the live one: a hop to a
private address is refused and never asked for, a hop to a public address is
still followed (a loop that rejected everything would have passed the first
test and broken every redirect on the web), and a cycle gives up. Removing
the hop check fails the first of them, which is more than the httpbin test
could say for the transport it did not run.

Verified live afterwards on both paths: an http to https chain through
impers, a plain page, and a literal and a resolved private address both
still refused.
2026-08-23 09:22:04 -04:00
only-cli 90d5c20ef9 fix: hand over code an agent can actually run
A syntax highlighter gives every token of a command its own element, so
`s3://bucket/` reached the walk as `s3`, `:`, `//`, `bucket`, `/`, and the
rule that reassembles text fragments only glues the ones sharing a parent.
The rest were space-joined. The AWS CLI reference handed over

  aws s3 cp test . txt s3 : // amzn - s3 - demo - bucket / test2 . txt

and Node's fs docs handed over `console . log` and `fd ?. close ()`. Any
command or snippet an agent took from a docs page was wrong, and nothing in
the output said so. Highlighting is not an edge case: of 172 pre elements on
the AWS CLI reference, the Rust book, the Node API docs and the Python
library docs, 159 are split this way.

A pre or code subtree is now read as one string. That costs nothing to
follow, because not one of those 172 blocks contains a link, and it also
fixes inline code, which was inserting a space into `Byte ( u8 only)`.

Two things fall out of it.

Page furniture had to stop riding along. Node puts a language label beside a
copy button inside every code block, so the subtree's text ended in
`javascriptcopy`. A control is chrome, and so is the block-level element
holding it, which is how the label leaves with the button it sits beside.
The test stays on block-level wrappers because a highlighter's own elements
are inline, so a stray control can never take a line of code out with it.

Code blocks keep their lines. Collapsing them was survivable while the code
was already wrecked; once it reads correctly, `// read it back` in front of
the statements that followed it is worse, because the output now looks
trustworthy. Blank runs and the indentation the whole block shares carry no
meaning and still go. A cut lands on a line end for the same reason it
already lands on a sentence end, and a snippet stays one line, because that
is what find's index promises.

Measured over five real pages, the compact view moves by -15, -7, +208, -45
and 0 characters, about 35 tokens in total, all of the growth being Python's
pretty-printed output getting its indentation back.
2026-08-23 09:12:25 -04:00
only-cli 780318a780 release: 0.3.0-beta.2 2026-08-23 08:52:10 -04:00
only-cli f84074701d perf: spend one command where the tool used to need two
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.
2026-08-23 08:46:10 -04:00
only-cli 29ab00b5c6 fix: read a json resource as the resource, not as the array beside it
Three faults in the JSON renderer, all of which the npm registry's package
endpoint hits at once, where the render came out as a truncated blob titled
after the package's two maintainers.

mainArray took the longest array of objects at the top level, so `maintainers`
became the subject and the package itself was pushed into the metadata line. A
root carrying its own name is the resource, and an array hanging off it
describes that resource rather than standing in for it. Conventional container
keys are checked first, so a named collection is still read as a collection.

The metadata line capped nothing. One long scalar there, a readme in npm's
case, cost more than the rest of the page put together; a summary line has to
stay a line, so a long one becomes its own block.

An oversized markup field distilled into more blocks than the item it hangs
off had fields. Under BODY_CAP a body is still rendered in place with its links
followable, which is what the Stack Exchange withbody shape wants; over it, one
numbered line, with `oc raw` still holding the whole thing.
2026-08-23 08:23:50 -04:00
only-cli 75fc1a0da3 fix: drop a node_modules symlink committed by the 0.3.0-beta.1 release
The release commit was made from a scratch worktree whose node_modules was a
symlink to another checkout, and .gitignore listed node_modules/ with a
trailing slash, which matches a directory and not a symlink. So the link
itself went into the tree, pointing at an absolute path on one machine.

Anyone cloning main got a dangling node_modules before npm was ever run. The
npm tarball is unaffected: the files whitelist decides what ships, and npm
never packs node_modules, which the 0.3.0-beta.1 pack listing confirms.

The ignore rule loses its trailing slash so it matches either shape.
2026-08-22 15:32:02 -04:00
only-cli f7c8a5583b fix: refuse binary responses on the impers transport too
Testing the 0.3.0-beta.1 build against live URLs turned up a gap the beta
notes claimed was closed: only the native-fetch path checked the content
type, and impers is the default whenever the optional dependency installs.
So 'oc open' on a PNG rendered eight kilobytes of mojibake as a page, with
numbered blocks, an actions footer, and a straight face.

The check now lives in one exported assertReadableType that both transports
call, so a refusal cannot depend on which client happened to get the page.

While the gate was being written down it also grew a correct allow list.
The old one matched the substring html, xml, or json anywhere in the header,
which let application/vnd.ms-htmlhelp through and, worse, refused text/plain:
a robots.txt or an llms.txt is exactly the kind of small text file an agent
asks for, and the fetch path was answering that it was not a page. Readable
now means any text/* type plus the application/* types that are really text,
including the +json and +xml families a feed answers with. A missing header
stays readable, since small servers omit it and the page behind it is fine.

Tested offline against the header strings themselves rather than the network.
2026-08-22 14:40:28 -04:00
only-cli bf478f1bd4 release: 0.3.0-beta.1
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.
2026-08-22 14:30:17 -04:00
only-cli fa79b0db53 Merge remote-tracking branch 'origin/main' into render-json-apis
# Conflicts:
#	README.md
2026-08-22 14:26:56 -04:00
only-cli 0f362708f8 feat: render JSON API responses as pages
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.
2026-08-22 14:20:01 -04:00
only-cli e8f2b6172d feat: add docs search to the cloud provider CLIs
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).
2026-08-22 12:29:56 -04:00
only-cli 5e7f54c4bb feat: add site CLIs for AWS, GCP, and Azure documentation
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
2026-08-22 12:16:14 -04:00
only-cli 126d5d9e54 feat: make web browsing skill discoverable 2026-08-20 09:21:34 -04:00
only-cliandClaude Sonnet 5 28d8b0d8aa docs: warn agents that fetched page content is data, not instructions
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>
2026-08-20 08:52:14 -04:00
only-cliandClaude Sonnet 5 6894abe396 readme: document plugin marketplace install path
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 19:17:47 -04:00
only-cliandClaude Sonnet 5 8bab2500b6 add plugin marketplace manifest
Lets users run /plugin marketplace add only-cli/oc and install the
existing skill as a plugin, alongside the skills.sh and manual-copy
distribution paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 19:11:50 -04:00
only-cli bf9fd98137 fix scorecard-action pin: v2 doesn't exist as a tag upstream
ossf/scorecard-action only publishes point-release tags (v2.4.4, etc),
no floating v2 major tag, so the workflow failed to even resolve the
action and never published results -- which is why the README badge
showed "invalid repo path". Pin to the v2.4.4 commit SHA instead.
2026-08-19 16:25:14 -04:00
only-cli d0516a6a4c harden CI: CodeQL, dependency review, npm provenance, OpenSSF Scorecard
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.
2026-08-19 16:21:46 -04:00
only-cli 951fd306cd release: 0.2.0 2026-08-19 14:47:12 -04:00
only-cli 51eea327b0 readme: condense to the core pitch, cut the duplicated benchmark tables
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.
2026-08-19 14:47:12 -04:00
only-cli 3ca462e353 fix: validate resolved IPs, not hostname strings, in the SSRF guard
PR #5's guard pattern-matched the URL's hostname against a regex, which
both under- and over-blocked: IPv4-mapped IPv6 loopback ([::ffff:127.0.0.1]),
0.0.0.0, and any DNS name that merely resolves to a private address all
sailed through, while a legitimate public hostname like 10.example.com was
wrongly rejected because it starts with "10.". It also only checked the
original URL, so a public URL that redirects to an internal address was
never re-validated.

This replaces the regex with net.isIP + dns.lookup: IP literals are checked
directly (including decoding IPv4-mapped/-compatible IPv6), and DNS names
are resolved first so every address they point to is validated before
connecting. The same check now reruns on every redirect hop for both the
impers and native-fetch transports. Resolving before connecting doesn't
pin the address for the actual connection (neither impers nor fetch expose
that here), so a name that re-resolves differently between this check and
the real connect remains a known, documented residual gap.
2026-08-19 14:43:43 -04:00
only-cli 01b607ac81 support youtube watch pages and transcripts
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
2026-08-19 13:27:52 -04:00
only-cli ea3afeb25a readme: carry the est. cost column into the top-of-README bar chart too
Same fix as the table further down: drop the repeated AAPL price
figures and show what each run actually cost instead.
2026-08-19 13:27:13 -04:00
only-cli e9eda512d7 readme: drop checkmarks from the end-to-end agent benchmark table
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.
2026-08-19 13:27:13 -04:00
only-cli 10ecafd81b readme: replace inline dollar amounts with an estimated cost column
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.
2026-08-19 13:27:13 -04:00
only-cli ac568a5823 add AAPL to the top-of-README comparison, on the same bar scale
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.
2026-08-19 09:00:32 -04:00
only-cli eae6f396e6 carry turns and pass/fail into the top-of-README bar chart
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.
2026-08-19 08:56:08 -04:00
only-cli 040dfdb440 condense the benchmark tables, several were saying the same thing twice
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.
2026-08-19 08:51:46 -04:00
only-cli f26a8d2898 lead with the Reddit multi-hop case, and fix a wrong claim about Codex's default
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.
2026-08-19 08:44:52 -04:00
only-cli 943192351a warn when a session can't be saved, instead of failing silently
Found while chasing an anomalous benchmark rerun: under a sandbox that
blocks writes outside its workspace (codex exec's default), 'oc open'
still renders fine but its session save throws, and the bare catch {}
swallowed it completely. The next 'oc do' then resolves against whatever
session last saved successfully, silently, with no sign the state is
stale. In one repro this served a Hacker News agent a Yahoo Finance page
under 'oc do 5' and it answered from that instead of erroring, which is
how a multi-step task ballooned to 250k tokens without a single wrong
tool call visible in the log.

This does not fix the staleness itself, that needs a design for detecting
a save failure across separate 'oc open' and 'oc do' invocations. It makes
the failure visible on stderr instead of invisible, which is the cheap
half of 'fail loud and cheap' and better than nothing while the real fix
gets designed. Verified manually against an unwritable OC_HOME; cli.js's
main() always calls the network fetcher, so an automated test here would
need fetch-mocking infrastructure this project has none of yet, and none
of cli.js has test coverage today.
2026-08-19 08:34:27 -04:00
only-cli 9873b7f492 add the Yahoo Finance cli and measure each agent's default browsing against oc
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.
2026-08-19 07:59:38 -04:00
only-cli 42906ced60 spend turns, not tokens: do <n> reads text, near-budget pages print whole, timelines render readably
Three changes with one motive: a second command costs an agent more than
the lines it saves. 'oc do' on a heading or text block now prints the read
instead of refusing, since refusing spends a whole turn naming the command
that should have run. A page that would finish within about four times the
budget is printed whole rather than cut, because the cut moves tokens into
a second command instead of saving them. And social timelines stopped
rendering as one fused paragraph: linkedom splits text nodes around
apostrophes, so fragments are merged back by parent node and edge
whitespace, block elements now end lines, and repeated button labels are
trimmed sooner than links because a button label is never the content.
With that, x.com profiles and posts read without a login, so a six line
cli definition ships for the two x.com pages that work.
2026-08-19 07:59:29 -04:00
only-cli 0435b386ae Lead the render with the page's main content
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.
2026-08-18 17:20:55 -04:00
only-cli 6aaa8f1975 add oc find <query> so a long page answers a lookup in one command
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.
2026-08-18 16:16:43 -04:00
only-cli 0f7a38d44a add oc next and oc read <n> so a long page costs a screenful, not a refetch
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.
2026-08-18 16:11:46 -04:00
only-cli 79ddeebc2e implement oc do so agents can follow a numbered link
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.
2026-08-18 15:00:36 -04:00
only-cli 3738f41825 readme: codex numbers from the six task run 2026-08-18 13:26:40 -04:00
only-cli babd5959d9 readme: six task agent benchmark, including multi step navigation 2026-08-18 13:15:07 -04:00
only-cli 3c735166f0 readme: note jina routes browsing through a third party 2026-08-18 13:02:16 -04:00
only-cli c59a9a7ae1 readme: codex agent results paragraph 2026-08-18 12:50:58 -04:00
only-cli 28fb94bc58 readme: turns, spend chart, and winner marks in the agent table 2026-08-18 12:39:46 -04:00
only-cli 45157e6441 readme: agent benchmark numbers from the skills run
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.
2026-08-18 11:02:49 -04:00
only-cli 46d1695949 readme: benchmark section covers all eleven methods and the agent run
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.
2026-08-18 10:47:49 -04:00
only-cli 853bbca853 feed reading: offline tests, readme row, and a raw-mode title fix
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.
2026-08-18 10:40:08 -04:00
only-cli bb214dcf59 read Atom and RSS feeds: Stack Overflow answers through the open /feeds door
Sites behind hard bot challenges often leave their feeds open. distill()
now detects feed XML, converts entries (title, byline, link, escaped HTML
body) into a plain HTML document, and everything downstream is unchanged.
fetch accepts xml content types. Ships a stackoverflow.com spec mapping
question/tag/user onto the feed URLs.

Live: oc open on a Stack Overflow question feed renders ~493 tokens vs
~29k for the page HTML, HTTP 200 where the HTML page is challenged.
2026-08-18 10:36:36 -04:00
only-cli 858755f979 readme: benchmark table now includes Jina Reader and lynx baselines 2026-08-18 10:09:23 -04:00
only-cli dfd3bd01b5 readme: refresh benchmark numbers from the 0.2.0-beta.1 run 2026-08-18 10:05:17 -04:00
only-cli b698ad468d ci: align setup-node with v7 used elsewhere 2026-08-18 10:02:24 -04:00
only-cli 97940355b2 release 0.2.0-beta.1: GitHub and LinkedIn site shortcuts 2026-08-18 10:01:13 -04:00
only-cli 12256c7fea publish: drop OIDC debug logging now that trusted publishing works 2026-08-18 10:01:12 -04:00
only-cli b218ba367e publish: print OIDC claims to debug trusted publisher matching 2026-08-18 09:54:51 -04:00
only-cli 3e1fbb6c87 publish: temporary http-level logging to trace the OIDC exchange 2026-08-18 09:49:31 -04:00
only-cli b536cc2ccf publish fix, GitHub and LinkedIn site definitions
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.
2026-08-18 09:48:04 -04:00
only-cli 3e5e2979a5 dependabot: weekly update PRs for npm deps and CI actions 2026-08-18 09:42:30 -04:00
only-cli 6b11896336 repo is only-cli/oc, matching the npm name @only-cli/oc
With the package scoped, owner/repo and scope/name line up exactly:
github.com/only-cli/oc is npm @only-cli/oc is the oc command. The skill
installs with npx skills add only-cli/oc.
2026-08-18 09:39:09 -04:00
only-cli 0b8bfa2a7b publish as @only-cli/oc
npm rejects the unscoped name only-cli as too similar to the existing
onlycli package, so the package lives in the org scope with the binary
name. The command is still oc and the repo is still only-cli/only-cli;
install snippets across README, SKILL.md, llms.txt, and the benchmark
docs now say npx @only-cli/oc.
2026-08-18 09:38:28 -04:00
only-cli f81ff04fa8 readme: supported websites, benchmark results, llms.txt for discovery
The README now lists the tuned site shortcuts (Hacker News, Reddit,
DuckDuckGo, Bing) next to the generic engine, and cites the live
benchmark numbers from only-cli/benchmarks. llms.txt gives AI assistants
a one-page summary to index. npm keywords added for search.

The internal build spec moves out of the published repo.
2026-08-18 09:32:42 -04:00
only-cli c758ab2298 commit the lockfile and add npm release channels
npm ci in CI requires package-lock.json, so stop ignoring it; it pins CI
and contributor installs only, consumers never see it.

publish.yml now maps release channels to npm dist-tags: latest, beta,
alpha (from the version suffix of a GitHub release) and dev (manual runs
stamp a unique 0.x.y-dev.N version per build). Publishing a prerelease
version to latest fails loud.
2026-08-18 09:24:29 -04:00
only-cli b50e9ca395 ci: run tests on push and pull request 2026-08-18 09:23:00 -04:00
only-cli 9b68dc2ec9 point repo references at only-cli/only-cli
The GitHub repo is renamed from oc to only-cli so the repo, the npm
package, and the skills.sh install path all share one name.
2026-08-18 09:16:41 -04:00
only-cli dcc0531ef3 only-cli v0.1: turn websites into a compact CLI for AI agents
Generic distillation engine (no per-site adapters): fetch via impers
impersonating Chrome with a firefox-fingerprint retry, distill to an
interaction tree, render under a hard token budget with numbered action
handles. Raw mode emits markdown via turndown or cleaned HTML. Per-site
CLI definitions for HN, Reddit, Bing, DuckDuckGo. Offline test suite,
agent skill, OIDC publish workflow.
2026-08-18 09:01:41 -04:00