Commit Graph
124 Commits
Author SHA1 Message Date
Mark CliandGitHub e34304a305 Merge pull request #31 from only-cli/release/0.5.0
release: 0.5.0
2026-08-24 21:32:28 -04:00
only-cli 4b6b81f8a0 release: 0.5.0 2026-08-24 21:30:31 -04:00
Mark CliandGitHub 4b3abf218b Merge pull request #26 from only-cli/feat/language-docs
Add language documentation shortcuts (#25): Python, MDN, Node.js, Ruby, Go, Rust, Java, PHP, TypeScript, C/C++, and .NET
2026-08-24 21:27:21 -04:00
only-cli 870ff2cf73 merge main: authenticated sessions via per-session cookie jars 2026-08-24 21:25:44 -04:00
Mark CliandGitHub 06a46abc39 Merge pull request #30 from RonCodes88/feat/authenticated-sessions
feat: authenticated sessions via per-session cookie jars
2026-08-24 21:07:59 -04:00
RonCodes88 e6218a5a5a test: assert the IPv6 tunnel host directly so node's SAN change stops breaking CI 2026-08-25 09:54:35 +09:00
RonCodes88 7aed46ed56 docs: recommend piping the cookie header and explain what logout removes 2026-08-25 09:38:34 +09:00
RonCodes88 218afd7f83 test: cover http downgrades, bare TLDs, control characters, and jar caps 2026-08-25 09:38:34 +09:00
RonCodes88 fd3e3f7681 feat: read the cookie header from stdin and make logout forget the saved page too 2026-08-25 09:38:34 +09:00
RonCodes88 448ac8b7d8 fix: keep seeded cookies https-only and reject unsafe domains, cookie values, and oversized jars 2026-08-25 09:38:34 +09: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
RonCodes88 f9fde64318 Merge upstream/main into feat/authenticated-sessions 2026-08-25 04:18:27 +09:00
RonCodes88 a21db1fc96 Merge upstream/main into feat/authenticated-sessions 2026-08-25 04:15:41 +09:00
RonCodes88 db1e5bb7ef chore: keep saved sessions and cookie files out of git 2026-08-25 04:09:51 +09:00
RonCodes88 c84496a95e docs: document authenticated sessions and the login/logout commands 2026-08-25 04:09:51 +09:00
RonCodes88 f8d813a3f3 test: cover cookie sessions, login detection, and proxy edge cases 2026-08-25 04:09:51 +09:00
RonCodes88 d761f5604d feat: add authenticated browsing with per-session cookie jars 2026-08-25 04:09:51 +09: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
Mark CliandGitHub 097cbb4203 Merge pull request #24 from only-cli/docs/proxy-support
Document proxy support, and make a stable release refresh the skills.sh page
2026-08-23 23:41:31 -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
Mark CliandGitHub 1acf41a5ba Merge pull request #23 from only-cli/release/0.4.0
release: 0.4.0
v0.4.0
2026-08-23 23:25:10 -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
Mark CliandGitHub f28a959828 Merge pull request #22 from only-cli/feat/wikipedia-shortcuts
feat: add wikipedia site shortcuts
2026-08-23 23:20:25 -04:00
Mark CliandGitHub 76a90bcc69 Merge pull request #17 from RonCodes88/feat/http-proxy-env-vars
feat: route outbound fetches through HTTP_PROXY, HTTPS_PROXY, and NO_PROXY
2026-08-23 23:13:59 -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
RonCodes88 59a693f4a6 fix: address proxy review — SSRF guard, no_proxy wiring, NO_PROXY parsing 2026-08-24 10:23:29 +08:00
Mark CliandGitHub 71b032bea5 Merge pull request #19 from only-cli/site-shortcuts
Dispatch the per-site shortcuts the README documents
2026-08-23 20:56:48 -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
Mark CliandGitHub bf0ce58c96 Merge pull request #18 from only-cli/add-license
Add the MIT LICENSE file the badge and package.json were claiming
2026-08-23 20:52:56 -04:00
Mark CliandGitHub 2f66a103ec Merge pull request #20 from only-cli/no-content-exit
Fail loud when a page distills to no readable content
2026-08-23 20:51:04 -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