Author SHA1 Message Date
only-cli ba49303527 docs: lead the Benchmarks section with headline numbers, group auth and proxies under Features 2026-08-26 09:32:03 -04:00
Mark CliandGitHub 7f1f109b8e Merge pull request #38 from only-cli/docs/sync-benchmarks
docs: sync the Benchmarks section with the benchmarks README
2026-08-25 10:53:39 -04:00
only-cli 2a5f97204e docs: sync the Benchmarks section with the benchmarks README
Add the language docs suite and the Playwright MCP row, fold the wiki
and docs results into one table, and note where the Codex runs split.
2026-08-25 10:52:12 -04:00
only-cli ccc0cf4476 docs: explain the search shapes a site definition can use, cover every DuckDuckGo search verb
CONTRIBUTING described a command as a URL template only, so a contributor
adding a docs site had no way to learn that a search verb can also name a
static index (sphinx, rdoc, nodedoc), a JSON endpoint (api), or fall back
to DuckDuckGo with a site: filter, or how to pick between them. It also
still counted six files in src/.

The rust, java, ts, and php search verbs had no test; only cpp's did. #25
asked for both verbs per language to be covered offline.
2026-08-25 09:46:23 -04:00
Mark CliandGitHub c12ecf060d Merge pull request #37 from only-cli/chore/codeql-action-v4
ci: bump github/codeql-action to v4.37.8, group its Dependabot updates
2026-08-25 07:55:28 -04:00
only-cliandClaude Fable 5 59964c5dca ci: bump github/codeql-action to v4.37.8, group its Dependabot updates
Dependabot opened separate PRs for init (#35) and analyze (#34). Each
failed because the two steps must run the same version of the action.
Bump init, analyze and upload-sarif together and add a Dependabot group
so future codeql-action bumps arrive as one PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 07:54:03 -04:00
Mark CliandGitHub 2c8b8f20c4 Merge pull request #36 from only-cli/dependabot/github_actions/actions/dependency-review-action-5.0.0
chore(deps): bump actions/dependency-review-action from 4.9.0 to 5.0.0
2026-08-25 07:53:24 -04:00
Mark CliandGitHub c3952be485 Merge pull request #33 from only-cli/dependabot/github_actions/ossf/scorecard-action-2d1146689b8cda280b9bc96326124645441f03bc
chore(deps): bump ossf/scorecard-action from 55891bbd73f2425e97637d96e306fc9d491d0b21 to 2d1146689b8cda280b9bc96326124645441f03bc
2026-08-25 07:49:10 -04:00
Mark CliandGitHub 0cf453d02c Merge pull request #32 from only-cli/dependabot/github_actions/actions/upload-artifact-7.0.1
chore(deps): bump actions/upload-artifact from 4.6.2 to 7.0.1
2026-08-25 07:48:28 -04:00
dependabot[bot]andGitHub 8d4c4ca4eb chore(deps): bump actions/dependency-review-action from 4.9.0 to 5.0.0
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.9.0 to 5.0.0.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/2031cfc080254a8a887f58cffee85186f0e49e48...a1d282b36b6f3519aa1f3fc636f609c47dddb294)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 04:45:55 +00:00
dependabot[bot]andGitHub 625048a7d1 chore(deps): bump ossf/scorecard-action
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 55891bbd73f2425e97637d96e306fc9d491d0b21 to 2d1146689b8cda280b9bc96326124645441f03bc.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](https://github.com/ossf/scorecard-action/compare/55891bbd73f2425e97637d96e306fc9d491d0b21...2d1146689b8cda280b9bc96326124645441f03bc)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-version: 2d1146689b8cda280b9bc96326124645441f03bc
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 04:44:13 +00:00
dependabot[bot]andGitHub a274761a7b chore(deps): bump actions/upload-artifact from 4.6.2 to 7.0.1
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 04:44:10 +00:00
only-cli cad6fb4596 docs: sync benchmark numbers to the 0.5.0 run, fix the skill's site list 2026-08-24 21:50:31 -04:00
9 changed files with 119 additions and 85 deletions
+5
View File
@@ -11,3 +11,8 @@ updates:
directory: /
schedule:
interval: weekly
groups:
# init and analyze must run the same version, so bump them in one PR.
codeql-action:
patterns:
- github/codeql-action*
+2 -2
View File
@@ -25,10 +25,10 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: github/codeql-action/init@42947a340483f03ba47bb1a039b2c519aab3df85 # v3.37.8
- uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
languages: javascript-typescript
- uses: github/codeql-action/analyze@42947a340483f03ba47bb1a039b2c519aab3df85 # v3.37.8
- uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
category: "/language:javascript-typescript"
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
+3 -3
View File
@@ -26,18 +26,18 @@ jobs:
with:
persist-credentials: false
- uses: ossf/scorecard-action@55891bbd73f2425e97637d96e306fc9d491d0b21 # v2.4.4
- uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
- uses: github/codeql-action/upload-sarif@42947a340483f03ba47bb1a039b2c519aab3df85 # v3.37.8
- uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
sarif_file: results.sarif
+10 -1
View File
@@ -23,7 +23,7 @@ Node 20+. Tests run fully offline against saved fixtures in `tests/pages/`, so a
## Code style
Plain JavaScript, ESM, JSDoc types, no build step. Match the code around you. Comments explain constraints and trade-offs, not what the next line does; if a comment restates the code, delete it. Small functions, few files: if you are adding a new file to `src/`, pause and check whether the logic belongs in one of the six that exist.
Plain JavaScript, ESM, JSDoc types, no build step. Match the code around you. Comments explain constraints and trade-offs, not what the next line does; if a comment restates the code, delete it. Small functions, few files: if you are adding a new file to `src/`, pause and check whether the logic belongs in one of the files that already exist.
## Writing style
@@ -37,6 +37,15 @@ A definition needs no wiring: `oc <site> <verb> [args]` resolves against `clis/*
Per-site CLIs live in `clis/`, one JSON file per domain: the domain plus a `commands` map of name, help line, and URL template, exactly like the existing files. Keep it under 50 lines, no OpenAPI. If the site has a public JSON API, point the commands at that instead of the HTML pages. If your definition needs logic, it is trying to become an adapter, and the answer is to improve the generic engine instead.
A `search` verb deserves a moment's thought, because most documentation sites render search in the browser and hand oc an empty page. Check what the site actually ships, in this order:
- **A server-rendered results page.** Point `open` at it, the way `pkg.go.dev` and Microsoft Learn's RSS endpoint do. Nothing else is needed.
- **A public JSON endpoint behind the results page.** Use the `api` shape: `api` is the endpoint template, `page` the human URL to remember for the session, `results` the dot path to the list, `fields` the paths to each result's `title`, `url`, and `text`, and `total` the path to the count. `clis/developer.mozilla.org.json` is the model. The site's own ranking comes back as a numbered results page for a few hundred tokens.
- **A static search index.** Sphinx sites (docs.python.org, most Read the Docs projects) publish `searchindex.js`; RDoc sites publish `js/search_index.js`; the Node.js API docs publish `all.json`. Set `sphinx`, `rdoc`, or `nodedoc` to the docs root and oc fetches the index once a day, ranks it locally, and prints only the result list. Any Sphinx or RDoc site works with no code.
- **None of the above.** Fall back to DuckDuckGo with a baked-in filter: `"open": "https://html.duckduckgo.com/html/?q=site%3Aexample.org+{query}"`, as `rust`, `java`, `ts`, `php`, and `cpp` do. Say so in the README row ("search via DuckDuckGo") so nobody mistakes it for the site's own search.
Adding a fifth search backend is a change to `src/`, not to a JSON file, and needs the same case as any other new code: a site that many agents reach for, an index or endpoint that a template cannot describe, and offline tests against a saved copy of the index.
## Reporting bugs
Open an issue with the exact command, the output you got, and the output you expected. If the page is public, include the URL. If distillation mangled a page, a saved copy of the HTML as a fixture is the most useful thing you can attach.
+84 -76
View File
@@ -29,35 +29,6 @@ npm install -g @only-cli/oc
Requires Node 20+. Requests impersonate Chrome via [impers](https://github.com/lexiforest/impers); falls back to native fetch if impers is unavailable.
### Proxies
Outbound fetches honor the usual environment variables, in upper or lower case, with nothing to pass on the command line:
```
HTTP_PROXY=http://proxy.example:8080 # http:// targets
HTTPS_PROXY=http://proxy.example:8080 # https:// targets, tunneled with CONNECT
NO_PROXY=internal.example,*.corp.example # reached directly instead
```
An `https://` target prefers `HTTPS_PROXY` and falls back to `HTTP_PROXY`; an `http://` target uses `HTTP_PROXY` only. A value with no scheme is read as `http://`, so `proxy.example:8080` works. Only HTTP and HTTPS proxies are supported, and another scheme such as `socks5://` is refused by name rather than silently ignored.
Credentials in the proxy URL are sent as `Proxy-Authorization` to the proxy and to nothing else, including across redirects:
```
HTTPS_PROXY=http://user:pass@proxy.example:8080 oc open https://example.com
```
`NO_PROXY` accepts an exact host, a `.suffix` or `*.suffix` pattern, a `host:port` entry, a CIDR block, and `*` for everything.
An `https://` page is tunneled with CONNECT and its certificate is verified the same way it would be without a proxy, so a proxy in the path cannot read or rewrite the page.
Two limits are worth knowing:
- oc does not read `ALL_PROXY`. The impers transport is libcurl underneath and reads it on its own, so a request oc treats as direct can still leave through an `ALL_PROXY`. The same holds for the `*.suffix`, `host:port`, and CIDR forms of `NO_PROXY`, which libcurl does not parse. Set `HTTP_PROXY` and `HTTPS_PROXY` explicitly and keep `NO_PROXY` to plain host and suffix entries when the two need to agree.
- An IPv6 literal target over HTTPS does not currently work through a proxy.
Private and internal addresses are refused whether or not a proxy is set. With a proxy configured, a hostname that does not resolve locally is refused too, because the proxy would otherwise resolve it on a network oc cannot see. A name that resolves publicly for oc and internally for the proxy (split horizon DNS) is not something oc can detect, so a proxy is trusted to enforce its own egress policy.
### Agent skill
Install the [web-browsing-cli skill](https://www.skills.sh/only-cli/oc/web-browsing-cli) for Claude Code, Cursor, Codex, Copilot, and other compatible agents:
@@ -103,30 +74,6 @@ oc logout [session] forget a session: cookies and saved page
Flags: `--budget <tokens>` (default 500), `--json`, `--html` (raw as cleaned HTML), `--session <name>`, `--verbose`/`-v` (metrics on stderr, or export `OC_VERBOSE=1`).
### Authenticated sessions
Pages behind a login need cookies. Seed them once per session, then browse normally:
```bash
printf %s "session=...; auth=..." | oc login --cookie - --domain example.com --expires 2h --session work
oc open https://example.com/dashboard --session work
oc logout work
```
Prefer `--cookie -`, which reads the header from stdin. The flag also takes the header inline (`--cookie "session=..."`), but an argument is a live credential in `ps` for as long as `oc` runs and in your shell history afterwards.
Copy the `Cookie` header from your browser's devtools (Application → Cookies, or the Network tab on a request); a leading `Cookie:` is stripped for you. `--domain` is the site hostname those cookies belong to, and it has to be a real hostname: a bare TLD like `com` is refused, because the match is a suffix match and those cookies would go to every `.com` host the session ever fetched. Cookie names and values are checked at login too, so a stray control character fails there rather than deep inside the HTTP client.
Seeded cookies are https-only. They almost always come from an https browser session, so `oc` marks them secure and never sends them over plain `http`, including on a hop an `https` page redirects into, where you never typed the downgrade. A site that really is http-only needs `--allow-http` at login. Cookies a site sets over https are pinned the same way.
Cookies live in a separate sidecar file (`<session>.cookies.json`) under `~/.only-cli/sessions/`, mode `0600`, not in the page-state JSON and never in `--json` output. The default lifetime is one hour (`--expires 1h`), and a jar holds at most 50 cookies so a page cannot bloat it. When cookies expire or the site returns a login page, `oc` says so plainly (exit 2) instead of distilling the login form as content.
`oc logout` forgets the whole session, not just its cookies: a page saved under that name can hold the distilled text of something only the login could reach, so the snapshot goes with the jar.
`oc open` remembers the page it rendered in a JSON file per session under `~/.only-cli` (override with `OC_HOME`), so `oc do 3` follows `[3]` without the agent ever handling a URL. A result title on a search page is a link, so `oc do` on it opens the result rather than repeating the title. Pages longer than the budget say what they left out; `oc find`, `oc read <n>`, and `oc next` read the rest without refetching the page, and a `find` with a single match prints that region instead of the number to read it with. The budget is a target rather than a hard cap: a page that would only run a little long is printed whole rather than cut, since one extra tool call costs far more than the tokens it would have saved.
When a page comes back with no readable text (JavaScript-only, a consent wall, a bot challenge), `oc` says so in one line on stderr and exits 2 instead of printing a title and calling it a render. That is a different exit code from every other failure, and `--json` carries the same verdict as an `empty` field, so an agent can tell "this page has nothing on it" from "oc could not read this page" and pay for a browser only when it is worth it.
## Supported websites
Works on any mostly-static site with no per-site setup: news sites, blogs, documentation, forums, search engines. A JSON API is a page here too: `oc open` on an endpoint that answers with JSON renders one numbered item per record, keeps the fields that actually differ between items, and says once what every item shares. On top of that, `clis/` ships tuned shortcuts, so `oc hn item 4711` or `oc gh repo only-cli oc` gets there without the agent knowing how that site spells its URLs. Name the site by its short name, its bare name, or its domain (`oc hn`, `oc ycombinator`, `oc news.ycombinator.com`), and `oc sites` prints the whole list with its verbs:
@@ -164,36 +111,97 @@ A few of these (X, Stack Overflow, YouTube, Microsoft Learn search) read pages t
Want a website on that list? Open a pull request, or an issue naming the site; see [CONTRIBUTING.md](CONTRIBUTING.md).
## Features
### Authenticated sessions
Pages behind a login need cookies. Seed them once per session, then browse normally:
```bash
printf %s "session=...; auth=..." | oc login --cookie - --domain example.com --expires 2h --session work
oc open https://example.com/dashboard --session work
oc logout work
```
Prefer `--cookie -`, which reads the header from stdin. The flag also takes the header inline (`--cookie "session=..."`), but an argument is a live credential in `ps` for as long as `oc` runs and in your shell history afterwards.
Copy the `Cookie` header from your browser's devtools (Application → Cookies, or the Network tab on a request); a leading `Cookie:` is stripped for you. `--domain` is the site hostname those cookies belong to, and it has to be a real hostname: a bare TLD like `com` is refused, because the match is a suffix match and those cookies would go to every `.com` host the session ever fetched. Cookie names and values are checked at login too, so a stray control character fails there rather than deep inside the HTTP client.
Seeded cookies are https-only. They almost always come from an https browser session, so `oc` marks them secure and never sends them over plain `http`, including on a hop an `https` page redirects into, where you never typed the downgrade. A site that really is http-only needs `--allow-http` at login. Cookies a site sets over https are pinned the same way.
Cookies live in a separate sidecar file (`<session>.cookies.json`) under `~/.only-cli/sessions/`, mode `0600`, not in the page-state JSON and never in `--json` output. The default lifetime is one hour (`--expires 1h`), and a jar holds at most 50 cookies so a page cannot bloat it. When cookies expire or the site returns a login page, `oc` says so plainly (exit 2) instead of distilling the login form as content.
`oc logout` forgets the whole session, not just its cookies: a page saved under that name can hold the distilled text of something only the login could reach, so the snapshot goes with the jar.
`oc open` remembers the page it rendered in a JSON file per session under `~/.only-cli` (override with `OC_HOME`), so `oc do 3` follows `[3]` without the agent ever handling a URL. A result title on a search page is a link, so `oc do` on it opens the result rather than repeating the title. Pages longer than the budget say what they left out; `oc find`, `oc read <n>`, and `oc next` read the rest without refetching the page, and a `find` with a single match prints that region instead of the number to read it with. The budget is a target rather than a hard cap: a page that would only run a little long is printed whole rather than cut, since one extra tool call costs far more than the tokens it would have saved.
When a page comes back with no readable text (JavaScript-only, a consent wall, a bot challenge), `oc` says so in one line on stderr and exits 2 instead of printing a title and calling it a render. That is a different exit code from every other failure, and `--json` carries the same verdict as an `empty` field, so an agent can tell "this page has nothing on it" from "oc could not read this page" and pay for a browser only when it is worth it.
### Proxies
Outbound fetches honor the usual environment variables, in upper or lower case, with nothing to pass on the command line:
```
HTTP_PROXY=http://proxy.example:8080 # http:// targets
HTTPS_PROXY=http://proxy.example:8080 # https:// targets, tunneled with CONNECT
NO_PROXY=internal.example,*.corp.example # reached directly instead
```
An `https://` target prefers `HTTPS_PROXY` and falls back to `HTTP_PROXY`; an `http://` target uses `HTTP_PROXY` only. A value with no scheme is read as `http://`, so `proxy.example:8080` works. Only HTTP and HTTPS proxies are supported, and another scheme such as `socks5://` is refused by name rather than silently ignored.
Credentials in the proxy URL are sent as `Proxy-Authorization` to the proxy and to nothing else, including across redirects:
```
HTTPS_PROXY=http://user:pass@proxy.example:8080 oc open https://example.com
```
`NO_PROXY` accepts an exact host, a `.suffix` or `*.suffix` pattern, a `host:port` entry, a CIDR block, and `*` for everything.
An `https://` page is tunneled with CONNECT and its certificate is verified the same way it would be without a proxy, so a proxy in the path cannot read or rewrite the page.
Two limits are worth knowing:
- oc does not read `ALL_PROXY`. The impers transport is libcurl underneath and reads it on its own, so a request oc treats as direct can still leave through an `ALL_PROXY`. The same holds for the `*.suffix`, `host:port`, and CIDR forms of `NO_PROXY`, which libcurl does not parse. Set `HTTP_PROXY` and `HTTPS_PROXY` explicitly and keep `NO_PROXY` to plain host and suffix entries when the two need to agree.
- An IPv6 literal target over HTTPS does not currently work through a proxy.
Private and internal addresses are refused whether or not a proxy is set. With a proxy configured, a hostname that does not resolve locally is refused too, because the proxy would otherwise resolve it on a network oc cannot see. A name that resolves publicly for oc and internally for the proxy (split horizon DNS) is not something oc can detect, so a proxy is trusted to enforce its own egress policy.
## Benchmarks
Full methodology, per-task numbers, and other agents/models live in [only-cli/benchmarks](https://github.com/only-cli/benchmarks). The short version, measured with oc 0.4.0 on 2026-08-24 against live sites across a news front page, a Reddit discussion, a search results page, a stock quote, three cloud CLI reference pages, the Python, MDN, and Node.js references, and more:
Full methodology, per-task rows, and the Codex runs live in [only-cli/benchmarks](https://github.com/only-cli/benchmarks). Where things stand (oc 0.5.0, August 2026, live sites):
| method | tokens for 15 real pages | notes |
- **142x fewer tokens than raw HTML** across 15 real pages: 10,936 against 1,552,491. 14x fewer than Jina Reader, 49x fewer than Playwright MCP's accessibility snapshot.
- **The only reader that returned real content on every page.** Reddit blocked curl, Jina Reader and Playwright; Jina also failed LinkedIn and Yahoo Finance; DuckDuckGo blocked lynx. oc's Chrome impersonation read all fifteen.
- **Half the cost of Claude Code's built-in `WebSearch`** on Wikipedia lookups: $0.27 against $0.52 for five questions, both 5/5 correct, on 29x less fresh input.
- **23% cheaper than `WebFetch` and 35% cheaper than `WebSearch`** on eleven language docs lookups, at equal or better accuracy.
The tables behind those numbers:
**Tokens per page, no model in the loop.** Fifteen real pages: a news front page, a Reddit discussion, search results, a stock quote, three cloud CLI references, the Python, MDN, and Node.js references, and more.
| method | tokens for 15 pages | notes |
| --- | ---: | --- |
| `oc open` | 10,973 | only method that returned real content on every page |
| Jina Reader | 170,505 | blocked on both Reddit pages, failed the LinkedIn page |
| raw HTML fetch | 1,535,791 | the stock quote page alone is 375,721 tokens |
| `oc open` | 10,936 | the only reader that returned real content on every page |
| Jina Reader | 148,479 | both Reddit results are block pages; failed LinkedIn and Yahoo Finance outright |
| Playwright MCP | 531,335 | accessibility snapshots; both Reddit snapshots are block pages |
| raw HTML fetch | 1,552,491 | the stock quote page alone is 399,881 tokens |
Read cost is one thing, but what an agent actually spends is another, so a
second suite runs whole tasks end to end in Claude Code and compares `oc`
against the tools the agent already has. Five Wikipedia lookups, one tool per
run, Sonnet driving:
oc's budget keeps every page near 500 tokens however much it weighs: the Yahoo Finance quote is 399,881 tokens raw and 456 through oc, Node's `fs` reference 273,820 against 475.
| tool | answered correctly | input tokens | cost | turns | avg time |
| --- | ---: | ---: | ---: | ---: | ---: |
| `oc wiki` | 5/5 | 5,535 | $0.27 | 22 | 11s |
| built-in `WebFetch` | 5/5 | 128,792 | $0.37 | 25 | 14s |
| built-in `WebSearch` | 5/5 | 160,431 | $0.52 | 27 | 22s |
**Whole tasks against the agent's built-in web tools.** Read cost is one thing, what an agent actually spends is another, so a second set of suites runs full lookups end to end in Claude Code (`claude-sonnet-5`), one tool per run, and grades every answer. Five Wikipedia lookups and eleven language documentation lookups:
All three got every answer right, so this is a cost result, not an accuracy one.
Input tokens are the fresh context each tool put in front of the model, which is
the number the page size drives; totals including cache reads sit closer together
because the agent's own prompt dominates them. The spread widens with the page:
`oc` cost 5.7x less than `WebFetch` on a short stub and 35x less on a long
article, because the 500 token budget makes it flat at about 1,100 tokens per
page while a full fetch pays for whatever the page weighs. `WebSearch` was given
only the question, not the article URL, which is the honest way to use it and
part of why it costs the most.
| suite | tool | correct | input tokens | cost | avg time |
| --- | --- | ---: | ---: | ---: | ---: |
| Wikipedia | `oc wiki` | 5/5 | 5,535 | $0.27 | 11s |
| | built-in `WebFetch` | 5/5 | 128,792 | $0.37 | 14s |
| | built-in `WebSearch` | 5/5 | 160,431 | $0.52 | 22s |
| Language docs | `oc docs` | 11/11 | 12,965 | $0.57 | 9s |
| | built-in `WebFetch` | 10/11 | 203,489 | $0.74 | 11s |
| | built-in `WebSearch` | 11/11 | 209,782 | $0.89 | 15s |
Input tokens are the fresh context each tool put in front of the model, which is the number the page size drives; totals including cache reads sit closer together because the agent's own prompt dominates them. oc stays flat at roughly 1,100 to 1,200 tokens per task, while `WebFetch` pays for whatever the page weighs, from 5.7x more on a short Wikipedia stub to 35x more on the German Berlin article. `WebFetch`'s one wrong answer is an access result: cppreference returns 403 to it, while oc's Chrome impersonation reads the same page. `WebSearch` was given only the question, never the URL, which is the honest way to use it and part of why it costs the most.
The same suites through Codex (`gpt-5.6-sol`) split. On Wikipedia, `oc wiki` was cheaper and also right where Codex's own search quoted a stale Berlin population. On the docs lookups Codex's search won by 16%: those facts are already in its snippets, and it answered most tasks in two turns without opening a page.
## Status
+1 -1
View File
@@ -9,7 +9,7 @@ Key facts:
- Default output budget is 500 tokens per page; `--budget <n>` adjusts it, and `find`, `read <n>`, or `next` collect what the budget cut without refetching the page
- The budget is a target rather than a hard cap: a page that would finish within about four times it is printed whole, because a second command costs the agent far more than the lines the cut would have saved
- The render leads with the page's main content and puts navigation, sidebar, and footer after it, so the budget is spent on what was asked for rather than on menus
- Benchmarked at roughly 45x fewer tokens than reading raw HTML, with per-task numbers at https://github.com/only-cli/benchmarks
- Benchmarked at roughly 140x fewer tokens than reading raw HTML across fifteen live pages, with per-task numbers at https://github.com/only-cli/benchmarks
- Works on any mostly-static website; tuned shortcuts ship for Hacker News, Reddit, GitHub, X, LinkedIn (public guest views), DuckDuckGo, Bing, Stack Overflow (via its Atom feeds and the Stack Exchange API), Yahoo Finance (quotes, history, markets), Wikipedia (articles, search, and other language editions), the AWS, Google Cloud, and Microsoft Learn documentation sites (guides, CLI reference, and search), and the language documentation for Python, JavaScript (MDN), Node.js, Ruby, Go, Rust, Java, PHP, TypeScript, C and C++ (cppreference), and .NET (the Microsoft Learn API browser)
- JSON APIs render like pages: an endpoint that answers with JSON becomes one numbered item per record, with the fields that differ between items kept and the ones every item shares stated once, so a search endpoint reads like a results page for a few hundred tokens
- A page that comes back with no readable text (JavaScript-only, a consent wall, a bot challenge) prints one line on stderr and exits 2, rather than reporting an empty render as a success. `--json` carries the same verdict as an `empty` field, so a caller can tell "nothing on this page" from "oc could not read this page" and fall back to a browser only when it is worth it
+1 -1
View File
@@ -32,7 +32,7 @@ oc ddg search claude code oc so question 231767 oc learn doc azur
oc py library json oc mdn js Array/map oc node api fs
```
Sites: `hn`, `reddit`, `gh`, `x`, `linkedin`, `ddg`, `bing`, `so`, `finance`, `yt`, `aws`, `gcp`, `learn`, `wiki`, `py`, `mdn`, `node`, `ruby`, `go`, `rust`, `java`, `php`, `cpp`, `ts`. Name one by short name, bare name, or domain (`oc hn`, `oc ycombinator`, `oc news.ycombinator.com`). The last argument takes every word after it, so a query or title needs no quoting. `oc sites` lists every site with its verbs, which is cheaper than guessing one.
Sites: `hn`, `reddit`, `gh`, `x`, `linkedin`, `ddg`, `bing`, `so`, `yahoo`, `yt`, `aws`, `gcp`, `learn`, `wiki`, `py`, `mdn`, `node`, `ruby`, `go`, `rust`, `java`, `php`, `cpp`, `ts`. Name one by short name, bare name, or domain (`oc hn`, `oc ycombinator`, `oc news.ycombinator.com`). The last argument takes every word after it, so a query or title needs no quoting. `oc sites` lists every site with its verbs, which is cheaper than guessing one.
Prefer a shortcut over a hand-built URL when one exists for the site, and prefer `oc wiki article <title>` over a search when you already know the article's name.
+12
View File
@@ -117,12 +117,24 @@ test('the second wave of language docs resolves the same way', () => {
assert.equal(
resolveSite('rust', ['std', 'vec/struct.Vec']).url,
'https://doc.rust-lang.org/std/vec/struct.Vec.html');
assert.equal(
resolveSite('rust', ['search', 'Vec', 'retain']).url,
'https://html.duckduckgo.com/html/?q=site%3Adoc.rust-lang.org+Vec%20retain');
assert.equal(
resolveSite('java', ['api', 'java.base/java/util/HashMap']).url,
'https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/util/HashMap.html');
assert.equal(
resolveSite('java', ['search', 'HashMap', 'computeIfAbsent']).url,
'https://html.duckduckgo.com/html/?q=site%3Adocs.oracle.com+javase+HashMap%20computeIfAbsent');
assert.equal(
resolveSite('ts', ['handbook', '2/everyday-types']).url,
'https://www.typescriptlang.org/docs/handbook/2/everyday-types.html');
assert.equal(
resolveSite('ts', ['search', 'satisfies']).url,
'https://html.duckduckgo.com/html/?q=site%3Atypescriptlang.org+satisfies');
assert.equal(
resolveSite('php', ['search', 'array', 'functions']).url,
'https://html.duckduckgo.com/html/?q=site%3Aphp.net+array%20functions');
assert.equal(
resolveSite('learn', ['dotnet', 'system.string']).url,
'https://learn.microsoft.com/en-us/dotnet/api/system.string');