cli-web-reddit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. cli_web_reddit-0.1.0/PKG-INFO +15 -0
  2. cli_web_reddit-0.1.0/cli_web/reddit/README.md +68 -0
  3. cli_web_reddit-0.1.0/cli_web/reddit/__init__.py +3 -0
  4. cli_web_reddit-0.1.0/cli_web/reddit/__main__.py +6 -0
  5. cli_web_reddit-0.1.0/cli_web/reddit/commands/__init__.py +0 -0
  6. cli_web_reddit-0.1.0/cli_web/reddit/commands/actions.py +268 -0
  7. cli_web_reddit-0.1.0/cli_web/reddit/commands/auth_cmd.py +73 -0
  8. cli_web_reddit-0.1.0/cli_web/reddit/commands/feed.py +115 -0
  9. cli_web_reddit-0.1.0/cli_web/reddit/commands/me.py +139 -0
  10. cli_web_reddit-0.1.0/cli_web/reddit/commands/post.py +93 -0
  11. cli_web_reddit-0.1.0/cli_web/reddit/commands/search.py +66 -0
  12. cli_web_reddit-0.1.0/cli_web/reddit/commands/subreddit.py +184 -0
  13. cli_web_reddit-0.1.0/cli_web/reddit/commands/user.py +90 -0
  14. cli_web_reddit-0.1.0/cli_web/reddit/core/__init__.py +0 -0
  15. cli_web_reddit-0.1.0/cli_web/reddit/core/auth.py +204 -0
  16. cli_web_reddit-0.1.0/cli_web/reddit/core/client.py +475 -0
  17. cli_web_reddit-0.1.0/cli_web/reddit/core/exceptions.py +63 -0
  18. cli_web_reddit-0.1.0/cli_web/reddit/core/models.py +253 -0
  19. cli_web_reddit-0.1.0/cli_web/reddit/reddit_cli.py +174 -0
  20. cli_web_reddit-0.1.0/cli_web/reddit/skills/SKILL.md +143 -0
  21. cli_web_reddit-0.1.0/cli_web/reddit/tests/TEST.md +109 -0
  22. cli_web_reddit-0.1.0/cli_web/reddit/tests/__init__.py +0 -0
  23. cli_web_reddit-0.1.0/cli_web/reddit/tests/conftest.py +9 -0
  24. cli_web_reddit-0.1.0/cli_web/reddit/tests/test_core.py +568 -0
  25. cli_web_reddit-0.1.0/cli_web/reddit/tests/test_e2e.py +312 -0
  26. cli_web_reddit-0.1.0/cli_web/reddit/utils/__init__.py +0 -0
  27. cli_web_reddit-0.1.0/cli_web/reddit/utils/doctor.py +188 -0
  28. cli_web_reddit-0.1.0/cli_web/reddit/utils/helpers.py +91 -0
  29. cli_web_reddit-0.1.0/cli_web/reddit/utils/mcp_server.py +290 -0
  30. cli_web_reddit-0.1.0/cli_web/reddit/utils/output.py +133 -0
  31. cli_web_reddit-0.1.0/cli_web/reddit/utils/repl_skin.py +486 -0
  32. cli_web_reddit-0.1.0/cli_web_reddit.egg-info/PKG-INFO +15 -0
  33. cli_web_reddit-0.1.0/cli_web_reddit.egg-info/SOURCES.txt +37 -0
  34. cli_web_reddit-0.1.0/cli_web_reddit.egg-info/dependency_links.txt +1 -0
  35. cli_web_reddit-0.1.0/cli_web_reddit.egg-info/entry_points.txt +2 -0
  36. cli_web_reddit-0.1.0/cli_web_reddit.egg-info/requires.txt +7 -0
  37. cli_web_reddit-0.1.0/cli_web_reddit.egg-info/top_level.txt +1 -0
  38. cli_web_reddit-0.1.0/setup.cfg +4 -0
  39. cli_web_reddit-0.1.0/setup.py +28 -0
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-web-reddit
3
+ Version: 0.1.0
4
+ Summary: CLI for Reddit browsing, search, and interaction
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: curl_cffi
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: prompt_toolkit>=3.0
10
+ Provides-Extra: browser
11
+ Requires-Dist: playwright>=1.40.0; extra == "browser"
12
+ Dynamic: provides-extra
13
+ Dynamic: requires-dist
14
+ Dynamic: requires-python
15
+ Dynamic: summary
@@ -0,0 +1,68 @@
1
+ # cli-web-reddit
2
+
3
+ CLI for Reddit browsing and search. Browse feeds, subreddits, search posts, view user profiles, and read comments — all from the command line.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ cd reddit/agent-harness
9
+ pip install -e .
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ ```bash
15
+ # Hot posts on the front page
16
+ cli-web-reddit feed hot --limit 10
17
+
18
+ # Top posts this week
19
+ cli-web-reddit feed top --time week --json
20
+
21
+ # Browse a subreddit
22
+ cli-web-reddit sub hot python --limit 10
23
+
24
+ # Subreddit info
25
+ cli-web-reddit sub info programming --json
26
+
27
+ # Search posts
28
+ cli-web-reddit search posts "machine learning" --sort top --json
29
+
30
+ # Search subreddits
31
+ cli-web-reddit search subs "python" --json
32
+
33
+ # User profile
34
+ cli-web-reddit user info spez --json
35
+
36
+ # Post detail with comments (auto-expands deeply nested threads)
37
+ cli-web-reddit post get https://www.reddit.com/r/python/comments/abc123/my_post/ --json
38
+
39
+ # Interactive REPL mode
40
+ cli-web-reddit
41
+ ```
42
+
43
+ ## Commands
44
+
45
+ | Group | Command | Description |
46
+ |-------|---------|-------------|
47
+ | `feed` | `hot`, `new`, `top`, `rising`, `popular` | Global Reddit feeds |
48
+ | `sub` | `hot`, `new`, `top`, `info`, `rules`, `search` | Subreddit operations |
49
+ | `search` | `posts`, `subs` | Search posts and subreddits |
50
+ | `user` | `info`, `posts`, `comments` | User profiles and activity |
51
+ | `post` | `get` | Post detail with comments (auto-expands deep threads) |
52
+ | `vote` | `up`, `down`, `unvote` | Vote on posts/comments (auth required) |
53
+ | `submit` | `text`, `link` | Submit new posts (auth required) |
54
+ | `comment` | `add`, `edit`, `delete` | Comment on posts (auth required) |
55
+ | `saved` | `save`, `unsave` | Save/unsave items (auth required) |
56
+ | `me` | `profile`, `saved`, `upvoted`, `subscriptions`, `inbox` | Authenticated user data (auth required) |
57
+ | `auth` | `login`, `logout`, `status` | OAuth authentication management |
58
+
59
+ All commands support `--json` for structured output and `--limit N` for pagination.
60
+
61
+ ## Notes
62
+
63
+ - **Reading is public** — feeds, subreddits, search, and user profiles work without auth
64
+ - **Writing requires auth** — vote, comment, submit, save, and inbox commands need OAuth login (`cli-web-reddit auth login`)
65
+ - **Token auto-refresh** — the `token_v2` session cookie expires every ~15-30 min, but the CLI silently refreshes it using a headless browser with the saved profile. No manual re-login needed.
66
+ - Uses `curl_cffi` with Chrome impersonation (Reddit blocks plain HTTP clients)
67
+ - Pagination via `--after` cursor (shown in output)
68
+ - Time filters: `hour`, `day`, `week`, `month`, `year`, `all`
@@ -0,0 +1,3 @@
1
+ """cli-web-reddit — CLI for Reddit browsing and search."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Allow running as: python -m cli_web.reddit"""
2
+
3
+ from .reddit_cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,268 @@
1
+ """Action commands for cli-web-reddit — vote, save, hide, comment, submit, edit, delete."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..core.client import RedditClient
8
+ from ..core.exceptions import SubmitError
9
+ from ..utils.helpers import handle_errors, print_json, resolve_json_mode
10
+
11
+ # ── Vote ──────────────────────────────────────────────────────
12
+
13
+
14
+ @click.group("vote")
15
+ def vote():
16
+ """Vote on posts and comments (requires login)."""
17
+
18
+
19
+ @vote.command("up")
20
+ @click.argument("thing_id")
21
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
22
+ def up(thing_id, use_json):
23
+ """Upvote a post or comment. Pass the fullname (t3_xxx or t1_xxx)."""
24
+ use_json = resolve_json_mode(use_json)
25
+ with handle_errors(json_mode=use_json):
26
+ client = RedditClient()
27
+ client.vote(thing_id, 1)
28
+ if use_json:
29
+ print_json({"success": True, "action": "upvote", "id": thing_id})
30
+ else:
31
+ click.echo(f" Upvoted {thing_id}")
32
+
33
+
34
+ @vote.command("down")
35
+ @click.argument("thing_id")
36
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
37
+ def down(thing_id, use_json):
38
+ """Downvote a post or comment."""
39
+ use_json = resolve_json_mode(use_json)
40
+ with handle_errors(json_mode=use_json):
41
+ client = RedditClient()
42
+ client.vote(thing_id, -1)
43
+ if use_json:
44
+ print_json({"success": True, "action": "downvote", "id": thing_id})
45
+ else:
46
+ click.echo(f" Downvoted {thing_id}")
47
+
48
+
49
+ @vote.command("unvote")
50
+ @click.argument("thing_id")
51
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
52
+ def unvote(thing_id, use_json):
53
+ """Remove vote from a post or comment."""
54
+ use_json = resolve_json_mode(use_json)
55
+ with handle_errors(json_mode=use_json):
56
+ client = RedditClient()
57
+ client.vote(thing_id, 0)
58
+ if use_json:
59
+ print_json({"success": True, "action": "unvote", "id": thing_id})
60
+ else:
61
+ click.echo(f" Removed vote on {thing_id}")
62
+
63
+
64
+ # ── Submit ────────────────────────────────────────────────────
65
+
66
+
67
+ @click.group("submit")
68
+ def submit():
69
+ """Submit posts to subreddits (requires login)."""
70
+
71
+
72
+ @submit.command("flairs")
73
+ @click.argument("subreddit")
74
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
75
+ def submit_flairs(subreddit, use_json):
76
+ """List available post flairs for a subreddit.
77
+
78
+ Example: submit flairs ClaudeCode
79
+ """
80
+ use_json = resolve_json_mode(use_json)
81
+ with handle_errors(json_mode=use_json):
82
+ client = RedditClient()
83
+ flairs = client.get_subreddit_flairs(subreddit)
84
+ if use_json:
85
+ print_json({"success": True, "subreddit": subreddit, "flairs": flairs})
86
+ else:
87
+ if not flairs:
88
+ click.echo(f" No flairs available for r/{subreddit}")
89
+ else:
90
+ click.echo(f" Flairs for r/{subreddit}:")
91
+ for f in flairs:
92
+ click.echo(f" {f['id']} {f['text']}")
93
+
94
+
95
+ @submit.command("text")
96
+ @click.argument("subreddit")
97
+ @click.argument("title")
98
+ @click.argument("body")
99
+ @click.option(
100
+ "--flair", "flair_id", default=None, help="Flair ID (use 'submit flairs <sub>' to list)."
101
+ )
102
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
103
+ def submit_text(subreddit, title, body, flair_id, use_json):
104
+ """Submit a text (self) post.
105
+
106
+ Example: submit text python "My Title" "Post body here"
107
+ Example with flair: submit text ClaudeCode "Title" "Body" --flair abc123
108
+ """
109
+ use_json = resolve_json_mode(use_json)
110
+ # Decode escape sequences so \n from CLI becomes actual newlines
111
+ body = body.encode("utf-8").decode("unicode_escape") if "\\n" in body else body
112
+ with handle_errors(json_mode=use_json):
113
+ client = RedditClient()
114
+ result = client.submit_text(subreddit, title, body, flair_id=flair_id)
115
+ data = result.get("json", {}).get("data", {})
116
+ errors = result.get("json", {}).get("errors", [])
117
+ if errors:
118
+ msg = "; ".join(str(e) for e in errors)
119
+ raise SubmitError(f"Submit failed: {msg}")
120
+ if use_json:
121
+ print_json(
122
+ {
123
+ "success": True,
124
+ "id": data.get("name", ""),
125
+ "url": data.get("url", ""),
126
+ }
127
+ )
128
+ else:
129
+ click.echo(f" Posted to r/{subreddit}: {data.get('url', '')}")
130
+
131
+
132
+ @submit.command("link")
133
+ @click.argument("subreddit")
134
+ @click.argument("title")
135
+ @click.argument("url")
136
+ @click.option(
137
+ "--flair", "flair_id", default=None, help="Flair ID (use 'submit flairs <sub>' to list)."
138
+ )
139
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
140
+ def submit_link(subreddit, title, url, flair_id, use_json):
141
+ """Submit a link post.
142
+
143
+ Example: submit link python "Check this out" "https://example.com"
144
+ Example with flair: submit link ClaudeCode "Title" "https://..." --flair abc123
145
+ """
146
+ use_json = resolve_json_mode(use_json)
147
+ with handle_errors(json_mode=use_json):
148
+ client = RedditClient()
149
+ result = client.submit_link(subreddit, title, url, flair_id=flair_id)
150
+ data = result.get("json", {}).get("data", {})
151
+ errors = result.get("json", {}).get("errors", [])
152
+ if errors:
153
+ msg = "; ".join(str(e) for e in errors)
154
+ raise SubmitError(f"Submit failed: {msg}")
155
+ if use_json:
156
+ print_json(
157
+ {
158
+ "success": True,
159
+ "id": data.get("name", ""),
160
+ "url": data.get("url", ""),
161
+ }
162
+ )
163
+ else:
164
+ click.echo(f" Posted to r/{subreddit}: {data.get('url', '')}")
165
+
166
+
167
+ # ── Comment ───────────────────────────────────────────────────
168
+
169
+
170
+ @click.group("comment")
171
+ def comment_group():
172
+ """Comment on posts and reply to comments (requires login)."""
173
+
174
+
175
+ @comment_group.command("add")
176
+ @click.argument("thing_id")
177
+ @click.argument("text")
178
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
179
+ def add_comment(thing_id, text, use_json):
180
+ """Add a comment to a post or reply to a comment.
181
+
182
+ thing_id: fullname of the post (t3_xxx) or comment (t1_xxx) to reply to.
183
+ """
184
+ use_json = resolve_json_mode(use_json)
185
+ # Decode escape sequences so \n from CLI becomes actual newlines
186
+ text = text.encode("utf-8").decode("unicode_escape") if "\\n" in text else text
187
+ with handle_errors(json_mode=use_json):
188
+ client = RedditClient()
189
+ result = client.comment(thing_id, text)
190
+ things = result.get("json", {}).get("data", {}).get("things", [])
191
+ comment_id = things[0].get("data", {}).get("name", "") if things else ""
192
+ errors = result.get("json", {}).get("errors", [])
193
+ if errors:
194
+ msg = "; ".join(str(e) for e in errors)
195
+ raise SubmitError(f"Comment failed: {msg}")
196
+ if use_json:
197
+ print_json({"success": True, "comment_id": comment_id})
198
+ else:
199
+ click.echo(f" Comment posted: {comment_id}")
200
+
201
+
202
+ @comment_group.command("edit")
203
+ @click.argument("thing_id")
204
+ @click.argument("text")
205
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
206
+ def edit_comment(thing_id, text, use_json):
207
+ """Edit your own post or comment text."""
208
+ use_json = resolve_json_mode(use_json)
209
+ with handle_errors(json_mode=use_json):
210
+ client = RedditClient()
211
+ client.edit(thing_id, text)
212
+ if use_json:
213
+ print_json({"success": True, "action": "edit", "id": thing_id})
214
+ else:
215
+ click.echo(f" Edited {thing_id}")
216
+
217
+
218
+ @comment_group.command("delete")
219
+ @click.argument("thing_id")
220
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
221
+ def delete_thing(thing_id, use_json):
222
+ """Delete your own post or comment."""
223
+ use_json = resolve_json_mode(use_json)
224
+ with handle_errors(json_mode=use_json):
225
+ client = RedditClient()
226
+ client.delete(thing_id)
227
+ if use_json:
228
+ print_json({"success": True, "action": "delete", "id": thing_id})
229
+ else:
230
+ click.echo(f" Deleted {thing_id}")
231
+
232
+
233
+ # ── Save ──────────────────────────────────────────────────────
234
+
235
+
236
+ @click.group("saved")
237
+ def saved_group():
238
+ """Save and unsave posts (requires login)."""
239
+
240
+
241
+ @saved_group.command("save")
242
+ @click.argument("thing_id")
243
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
244
+ def save_thing(thing_id, use_json):
245
+ """Save a post or comment."""
246
+ use_json = resolve_json_mode(use_json)
247
+ with handle_errors(json_mode=use_json):
248
+ client = RedditClient()
249
+ client.save(thing_id)
250
+ if use_json:
251
+ print_json({"success": True, "action": "save", "id": thing_id})
252
+ else:
253
+ click.echo(f" Saved {thing_id}")
254
+
255
+
256
+ @saved_group.command("unsave")
257
+ @click.argument("thing_id")
258
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
259
+ def unsave_thing(thing_id, use_json):
260
+ """Unsave a post or comment."""
261
+ use_json = resolve_json_mode(use_json)
262
+ with handle_errors(json_mode=use_json):
263
+ client = RedditClient()
264
+ client.unsave(thing_id)
265
+ if use_json:
266
+ print_json({"success": True, "action": "unsave", "id": thing_id})
267
+ else:
268
+ click.echo(f" Unsaved {thing_id}")
@@ -0,0 +1,73 @@
1
+ """Auth commands for cli-web-reddit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..core.auth import clear_auth, load_auth, login_browser
8
+ from ..core.client import RedditClient
9
+ from ..utils.helpers import handle_errors, print_json, resolve_json_mode
10
+
11
+
12
+ @click.group("auth")
13
+ def auth():
14
+ """Login, logout, and check authentication status."""
15
+
16
+
17
+ @auth.command("login")
18
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
19
+ def login(use_json):
20
+ """Login to Reddit via browser (opens Playwright)."""
21
+ use_json = resolve_json_mode(use_json)
22
+ with handle_errors(json_mode=use_json):
23
+ login_browser()
24
+ if use_json:
25
+ print_json({"success": True, "message": "Logged in successfully"})
26
+ else:
27
+ click.echo(" Logged in successfully. Token saved.")
28
+
29
+
30
+ @auth.command("logout")
31
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
32
+ def logout(use_json):
33
+ """Remove saved authentication."""
34
+ use_json = resolve_json_mode(use_json)
35
+ with handle_errors(json_mode=use_json):
36
+ clear_auth()
37
+ if use_json:
38
+ print_json({"success": True, "message": "Logged out"})
39
+ else:
40
+ click.echo(" Logged out. Auth data removed.")
41
+
42
+
43
+ @auth.command("status")
44
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
45
+ def status(use_json):
46
+ """Check current authentication status."""
47
+ use_json = resolve_json_mode(use_json)
48
+ with handle_errors(json_mode=use_json):
49
+ auth_data = load_auth()
50
+ if not auth_data or not auth_data.get("token"):
51
+ if use_json:
52
+ print_json({"authenticated": False, "message": "Not logged in"})
53
+ else:
54
+ click.echo(" Not logged in. Run: cli-web-reddit auth login")
55
+ return
56
+
57
+ # Verify token works by calling /api/v1/me
58
+ client = RedditClient()
59
+ me = client.me()
60
+ username = me.get("name", "unknown")
61
+ karma = me.get("total_karma", 0)
62
+
63
+ if use_json:
64
+ print_json(
65
+ {
66
+ "authenticated": True,
67
+ "username": username,
68
+ "total_karma": karma,
69
+ }
70
+ )
71
+ else:
72
+ click.echo(f" Logged in as: u/{username}")
73
+ click.echo(f" Total karma: {karma:,}")
@@ -0,0 +1,115 @@
1
+ """Feed commands for cli-web-reddit — global feeds (hot, new, top, rising, popular)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..core.client import RedditClient
8
+ from ..core.models import extract_listing_posts
9
+ from ..utils.helpers import handle_errors, print_json, resolve_json_mode
10
+ from ..utils.output import post_table
11
+
12
+ TIME_CHOICES = ["hour", "day", "week", "month", "year", "all"]
13
+
14
+
15
+ @click.group("feed")
16
+ def feed():
17
+ """Browse global Reddit feeds."""
18
+
19
+
20
+ @feed.command("hot")
21
+ @click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
22
+ @click.option("--after", default=None, help="Pagination cursor.")
23
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
24
+ def hot(limit, after, use_json):
25
+ """Hot posts from the front page."""
26
+ use_json = resolve_json_mode(use_json)
27
+ with handle_errors(json_mode=use_json):
28
+ client = RedditClient()
29
+ data = client.feed_hot(limit=limit, after=after)
30
+ posts, next_after = extract_listing_posts(data)
31
+ if use_json:
32
+ print_json({"posts": posts, "after": next_after})
33
+ else:
34
+ post_table(posts, title="Hot Posts")
35
+ if next_after:
36
+ click.echo(f" Next page: feed hot --after {next_after}")
37
+
38
+
39
+ @feed.command("new")
40
+ @click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
41
+ @click.option("--after", default=None, help="Pagination cursor.")
42
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
43
+ def new(limit, after, use_json):
44
+ """Newest posts from the front page."""
45
+ use_json = resolve_json_mode(use_json)
46
+ with handle_errors(json_mode=use_json):
47
+ client = RedditClient()
48
+ data = client.feed_new(limit=limit, after=after)
49
+ posts, next_after = extract_listing_posts(data)
50
+ if use_json:
51
+ print_json({"posts": posts, "after": next_after})
52
+ else:
53
+ post_table(posts, title="New Posts")
54
+ if next_after:
55
+ click.echo(f" Next page: feed new --after {next_after}")
56
+
57
+
58
+ @feed.command("top")
59
+ @click.option(
60
+ "--time", "time_filter", type=click.Choice(TIME_CHOICES), default="day", help="Time period."
61
+ )
62
+ @click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
63
+ @click.option("--after", default=None, help="Pagination cursor.")
64
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
65
+ def top(time_filter, limit, after, use_json):
66
+ """Top posts by time period."""
67
+ use_json = resolve_json_mode(use_json)
68
+ with handle_errors(json_mode=use_json):
69
+ client = RedditClient()
70
+ data = client.feed_top(limit=limit, after=after, time=time_filter)
71
+ posts, next_after = extract_listing_posts(data)
72
+ if use_json:
73
+ print_json({"posts": posts, "after": next_after})
74
+ else:
75
+ post_table(posts, title=f"Top Posts ({time_filter})")
76
+ if next_after:
77
+ click.echo(f" Next page: feed top --time {time_filter} --after {next_after}")
78
+
79
+
80
+ @feed.command("rising")
81
+ @click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
82
+ @click.option("--after", default=None, help="Pagination cursor.")
83
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
84
+ def rising(limit, after, use_json):
85
+ """Rising posts."""
86
+ use_json = resolve_json_mode(use_json)
87
+ with handle_errors(json_mode=use_json):
88
+ client = RedditClient()
89
+ data = client.feed_rising(limit=limit, after=after)
90
+ posts, next_after = extract_listing_posts(data)
91
+ if use_json:
92
+ print_json({"posts": posts, "after": next_after})
93
+ else:
94
+ post_table(posts, title="Rising Posts")
95
+ if next_after:
96
+ click.echo(f" Next page: feed rising --after {next_after}")
97
+
98
+
99
+ @feed.command("popular")
100
+ @click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
101
+ @click.option("--after", default=None, help="Pagination cursor.")
102
+ @click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
103
+ def popular(limit, after, use_json):
104
+ """Popular posts from r/popular."""
105
+ use_json = resolve_json_mode(use_json)
106
+ with handle_errors(json_mode=use_json):
107
+ client = RedditClient()
108
+ data = client.feed_popular(limit=limit, after=after)
109
+ posts, next_after = extract_listing_posts(data)
110
+ if use_json:
111
+ print_json({"posts": posts, "after": next_after})
112
+ else:
113
+ post_table(posts, title="Popular Posts")
114
+ if next_after:
115
+ click.echo(f" Next page: feed popular --after {next_after}")