Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
Notable changes per release. Releases before 0.4.0 are listed at
[github.com/only-cli/oc/releases](https://github.com/only-cli/oc/releases).

## Unreleased

### Changed

- `oc reddit` reads the Atom feeds on www.reddit.com instead of old.reddit.com
pages. Reddit has sent every logged-out old.reddit.com request to a login
page since 30 June 2026, and the `.json` views on www.reddit.com have
answered 403 to anything without an OAuth token since 30 May, whatever the
User-Agent or TLS fingerprint. The feeds still answer, so `sub`, `post`,
`user`, and `search` point at them, and `new <name>` and `top <name>` join
the verbs. A subreddit renders in about 480 tokens and a thread with 22
comments in about 1,000. The feeds carry no scores or comment counts, and
anonymous reddit.com allows roughly ten requests a minute per address, so a
burst of shortcuts ends in a 429 that takes minutes to clear. (#52)

## 0.5.1

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Works on any mostly-static site with no per-site setup: news sites, blogs, docum
| website | command | shortcuts |
| --- | --- | --- |
| Hacker News | `oc hn` | `top`, `new`, `item <id>`, `user <name>` |
| Reddit | `oc reddit` (via old.reddit.com) | `sub <name>`, `post <id>`, `user <name>`, `search <query>` |
| Reddit | `oc reddit` (via the Atom feeds on www.reddit.com) | `sub <name>`, `new <name>`, `top <name>`, `post <id>`, `user <name>`, `search <query>` |
| GitHub | `oc gh` | `repo <owner> <name>`, `user <name>`, `search <query>`, `trending`, `issues <owner> <name>` |
| X | `oc x` | `user <name>`, `post <id>` |
| LinkedIn | `oc linkedin` | `profile <name>`, `company <name>`, `jobs <query>` (public guest views) |
Expand Down
10 changes: 6 additions & 4 deletions clis/reddit.com.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"domain": "reddit.com",
"commands": {
"sub": { "open": "https://old.reddit.com/r/{name}", "args": ["name"] },
"post": { "open": "https://old.reddit.com/comments/{id}", "args": ["id"] },
"user": { "open": "https://old.reddit.com/user/{name}", "args": ["name"] },
"search": { "open": "https://old.reddit.com/search?q={query}", "args": ["query"] }
"sub": { "open": "https://www.reddit.com/r/{name}/.rss", "args": ["name"] },
"new": { "open": "https://www.reddit.com/r/{name}/new/.rss", "args": ["name"] },
"top": { "open": "https://www.reddit.com/r/{name}/top/.rss?t=week", "args": ["name"] },
"post": { "open": "https://www.reddit.com/comments/{id}/.rss", "args": ["id"] },
"user": { "open": "https://www.reddit.com/user/{name}/.rss", "args": ["name"] },
"search": { "open": "https://www.reddit.com/search.rss?q={query}", "args": ["query"] }
}
}
24 changes: 24 additions & 0 deletions tests/distill.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,30 @@ test('atom feeds render as pages: entries become headings, bodies unescape', ()
assert.ok(!text.includes('&lt;'), 'entry body left escaped');
});

test('a reddit post feed renders as the post followed by its comments', () => {
// Reddit closed old.reddit.com and its .json views to logged-out readers in
// 2026; the Atom feeds on www.reddit.com are what oc reddit rides now. A
// post feed is one entry for the post and one per comment, each comment
// titled "/u/name on <post title>", so the whole thread reads as one page.
const xml = readFileSync(new URL('./pages/reddit_post.xml', import.meta.url), 'utf8');
const p = distill(xml, 'https://www.reddit.com/comments/1fixture/.rss');
assert.equal(p.title, 'Why does the budget flag round up? : reddit.com');
const headings = p.blocks.filter((b) => b.type === 'heading').map((b) => b.text);
assert.deepEqual(headings, [
'Why does the budget flag round up?',
'/u/first_reply on Why does the budget flag round up?',
'/u/second_reply on Why does the budget flag round up?',
]);
const text = p.blocks.map((b) => b.text).join(' ');
assert.ok(text.includes('Is that on purpose?'), 'post body missing');
assert.ok(text.includes('by /u/fixture_poster, 2026-09-01'), 'post byline missing');
assert.ok(text.includes('One extra tool call costs more'), 'comment body missing');
assert.ok(!text.includes('SC_OFF'), 'reddit markup comments leaked into the text');
const rendered = render(p).text;
assert.ok(rendered.includes('## [1] Why does the budget flag round up?'), 'post is not the first numbered heading');
assert.ok(estimateTokens(rendered) < 500, `three-entry thread should fit the default budget, got ${estimateTokens(rendered)}`);
});

test('feed entry code blocks survive raw markdown', () => {
const md = toMarkdown(feed);
assert.ok(md.startsWith('# Why is the sky blue? - Fixture Overflow'));
Expand Down
1 change: 1 addition & 0 deletions tests/pages/reddit_post.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><category term=" reddit.com" label="r/ reddit.com"/><updated>2026-09-04T12:39:19+00:00</updated><id>/comments/1fixture/.rss</id><link rel="self" href="https://www.reddit.com/comments/1fixture/.rss" type="application/atom+xml" /><link rel="alternate" href="https://www.reddit.com/comments/1fixture/" type="text/html" /><title>Why does the budget flag round up? : reddit.com</title><entry><author><name>/u/fixture_poster</name><uri>https://www.reddit.com/user/fixture_poster</uri></author><category term="FixtureSub" label="r/FixtureSub"/><content type="html">&lt;!-- SC_OFF --&gt;&lt;div class=&quot;md&quot;&gt;&lt;p&gt;A page that runs a little over the budget prints whole instead of being cut. Is that on purpose?&lt;/p&gt; &lt;/div&gt;&lt;!-- SC_ON --&gt; &amp;#32; submitted by &amp;#32; &lt;a href=&quot;https://www.reddit.com/user/fixture_poster&quot;&gt; /u/fixture_poster &lt;/a&gt; &amp;#32; to &amp;#32; &lt;a href=&quot;https://www.reddit.com/r/FixtureSub/&quot;&gt; r/FixtureSub &lt;/a&gt; &lt;br/&gt; &lt;span&gt;&lt;a href=&quot;https://www.reddit.com/r/FixtureSub/comments/1fixture/why_does_the_budget_flag_round_up/&quot;&gt;[link]&lt;/a&gt;&lt;/span&gt; &amp;#32; &lt;span&gt;&lt;a href=&quot;https://www.reddit.com/r/FixtureSub/comments/1fixture/why_does_the_budget_flag_round_up/&quot;&gt;[comments]&lt;/a&gt;&lt;/span&gt;</content><id>t3_1fixture</id><link href="https://www.reddit.com/r/FixtureSub/comments/1fixture/why_does_the_budget_flag_round_up/" /><updated>2026-09-01T11:18:08+00:00</updated><published>2026-09-01T11:18:08+00:00</published><title>Why does the budget flag round up?</title></entry><entry><author><name>/u/first_reply</name><uri>https://www.reddit.com/user/first_reply</uri></author><category term="FixtureSub" label="r/FixtureSub" /><content type="html">&lt;!-- SC_OFF --&gt;&lt;div class=&quot;md&quot;&gt;&lt;p&gt;Yes. One extra tool call costs more than the tokens it would save.&lt;/p&gt; &lt;/div&gt;&lt;!-- SC_ON --&gt;</content><id>t1_c0000001</id><link href="https://www.reddit.com/r/FixtureSub/comments/1fixture/why_does_the_budget_flag_round_up/c0000001/"/><updated>2026-09-01T13:50:03+00:00</updated><title>/u/first_reply on Why does the budget flag round up?</title></entry><entry><author><name>/u/second_reply</name><uri>https://www.reddit.com/user/second_reply</uri></author><category term="FixtureSub" label="r/FixtureSub" /><content type="html">&lt;!-- SC_OFF --&gt;&lt;div class=&quot;md&quot;&gt;&lt;p&gt;The README calls it a target rather than a hard cap.&lt;/p&gt; &lt;/div&gt;&lt;!-- SC_ON --&gt;</content><id>t1_c0000002</id><link href="https://www.reddit.com/r/FixtureSub/comments/1fixture/why_does_the_budget_flag_round_up/c0000002/"/><updated>2026-09-01T14:02:11+00:00</updated><title>/u/second_reply on Why does the budget flag round up?</title></entry></feed>
13 changes: 13 additions & 0 deletions tests/sites.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ test('a real site with a missing or unknown verb names the verbs it has', () =>
/not a reddit\.com shortcut.*sub <name>/s);
});

test('reddit verbs reach the www.reddit.com atom feeds, not old.reddit.com', () => {
// old.reddit.com sends every logged-out request to a login page and the
// .json views on www answer 403, so the feeds are the only public reading.
assert.equal(resolveSite('reddit', ['sub', 'ClaudeAI']).url, 'https://www.reddit.com/r/ClaudeAI/.rss');
assert.equal(resolveSite('reddit', ['post', '1w48zcr']).url, 'https://www.reddit.com/comments/1w48zcr/.rss');
assert.equal(resolveSite('reddit', ['search', 'claude code']).url, 'https://www.reddit.com/search.rss?q=claude%20code');
for (const verb of Object.keys(sites().get('reddit').commands)) {
const { url } = resolveSite('reddit', [verb, 'x']);
assert.ok(url.startsWith('https://www.reddit.com/'), `${verb} left www: ${url}`);
assert.ok(/\.rss(\?|$)/.test(url), `${verb} is not a feed: ${url}`);
}
});

test('a shortcut called with too few args says what it needs', () => {
assert.throws(() => resolveSite('gh', ['repo', 'only-cli']), /usage: oc gh repo <owner> <name>/);
});
Expand Down