reddit-mcp-server 1.5.1 → 1.5.3

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.
package/README.md CHANGED
@@ -213,6 +213,16 @@ export REDDIT_BOT_FOOTER=$'\n\n---\n^(🤖 Custom bot footer text)'
213
213
  }
214
214
  ```
215
215
 
216
+ **Anonymous mode does not work on every network.** Reddit blocks unauthenticated requests from many IP ranges — datacenters, cloud hosts, VPNs, and addresses it has flagged — and answers with an HTTP 403 block page. If you hit this, tools fail with:
217
+
218
+ ```
219
+ Reddit is blocking unauthenticated requests from this network (HTTP 403 with a
220
+ block page). Set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET to authenticate with
221
+ OAuth, which also raises the rate limit from ~10 to 60+ requests/min.
222
+ ```
223
+
224
+ The fix is OAuth credentials — see the next section. This is a property of your network, not of your Reddit account or the subreddit you asked for, so it affects every tool at once. A 403 on a single subreddit while others work is a different thing: that subreddit is private or quarantined.
225
+
216
226
  ### Authenticated Mode (Higher Rate Limits)
217
227
 
218
228
  1. Create a Reddit app at https://www.reddit.com/prefs/apps (select "script" type)
package/dist/bin.d.ts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}
package/dist/bin.js CHANGED
@@ -3,12 +3,12 @@
3
3
  process.env.TRANSPORT_TYPE ??= "stdio";
4
4
  const args = process.argv.slice(2);
5
5
  if (args.includes("--version") || args.includes("-v")) {
6
- console.log("1.5.1");
6
+ console.log("1.5.3");
7
7
  process.exit(0);
8
8
  }
9
9
  if (args.includes("--help") || args.includes("-h")) {
10
10
  console.log(`
11
- Reddit MCP Server v1.5.1
11
+ Reddit MCP Server v1.5.3
12
12
 
13
13
  Usage: reddit-mcp-server [options]
14
14
 
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}
package/dist/index.js CHANGED
@@ -59,6 +59,21 @@ var NotFoundError = class extends RedditErrorBase {
59
59
  this.name = "NotFoundError";
60
60
  }
61
61
  };
62
+ /**
63
+ * Reddit refused the request at the network level rather than for this specific resource.
64
+ *
65
+ * Reddit 403s the unauthenticated JSON API from many IP ranges (datacenters, VPNs, flagged
66
+ * addresses), answering with an HTML block page instead of JSON. A bare `HttpError(403)` reads
67
+ * as "this subreddit is private", which sends people looking in the wrong place — the fix is to
68
+ * supply OAuth credentials, which also raises the rate limit from ~10 to 60+ req/min.
69
+ */
70
+ var NetworkBlockedError = class extends RedditErrorBase {
71
+ _tag = "NetworkBlockedError";
72
+ constructor(message) {
73
+ super(message);
74
+ this.name = "NetworkBlockedError";
75
+ }
76
+ };
62
77
  /** Client-side input or safety-policy rejection (invalid sort, duplicate-content guard). */
63
78
  var ValidationError = class extends RedditErrorBase {
64
79
  _tag = "ValidationError";
@@ -90,6 +105,71 @@ function classifyRedditError(error, context) {
90
105
  return new UnknownError(Option(context).fold(() => error.message, (ctx) => `${ctx}: ${error.message}`), error);
91
106
  }
92
107
  //#endregion
108
+ //#region src/utils/reddit-identifiers.ts
109
+ /**
110
+ * Validation and normalization for Reddit identifiers that get interpolated into API paths.
111
+ *
112
+ * Every identifier reaching the client is model- or user-supplied. Interpolating one raw is a
113
+ * path-injection hole: URL parsing resolves dot segments, so a `subreddit` of `../../api/v1/me`
114
+ * escapes `/r/{sub}/about.json` and steers the OAuth bearer token to a different endpoint, and an
115
+ * embedded `?` or `&` injects query parameters into the request we build.
116
+ *
117
+ * So each identifier is normalized (stripping the `r/`, `/u/`, `t3_` prefixes people naturally
118
+ * type) and then checked against Reddit's own charset. Every value returned from this module
119
+ * matches `[A-Za-z0-9_+-]+`, which is already URL-path-safe — no percent-encoding is applied,
120
+ * because encoding the `+` in a multireddit (`r/science+space`) would break it.
121
+ */
122
+ const SUBREDDIT_PATTERN = /^(u_)?[A-Za-z0-9][A-Za-z0-9_]{1,20}$/;
123
+ const USERNAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{1,19}$/;
124
+ const THING_ID_PATTERN = /^[a-z0-9]{1,13}$/;
125
+ const stripLeading = (value, prefixes) => {
126
+ const lower = value.toLowerCase();
127
+ const matched = prefixes.find((prefix) => lower.startsWith(prefix));
128
+ return matched === void 0 ? value : value.slice(matched.length);
129
+ };
130
+ const bare = (value, prefixes) => stripLeading(stripLeading(value.trim(), ["/"]), prefixes).replace(/\/+$/, "");
131
+ /**
132
+ * Normalize a subreddit name for use in a path segment.
133
+ *
134
+ * Accepts `science`, `r/science`, `/r/science`, and `+`-joined multireddits (`science+space`).
135
+ * The empty string passes through unchanged — callers use it to mean "the home feed".
136
+ */
137
+ function normalizeSubreddit(input) {
138
+ const trimmed = input.trim();
139
+ if (trimmed === "") return "";
140
+ const segments = bare(trimmed, ["r/"]).split("+").map((segment) => segment.trim());
141
+ if (segments.find((segment) => !SUBREDDIT_PATTERN.test(segment)) !== void 0) throw new ValidationError(`Invalid subreddit name "${input}". Expected 2-21 characters of letters, digits, or underscores (e.g. "science").`);
142
+ return segments.join("+");
143
+ }
144
+ /** Normalize a username for use in a path segment. Accepts `spez`, `u/spez`, `/user/spez`. */
145
+ function normalizeUsername(input) {
146
+ const name = bare(input, ["user/", "u/"]);
147
+ if (!USERNAME_PATTERN.test(name)) throw new ValidationError(`Invalid Reddit username "${input}". Expected 2-20 characters of letters, digits, underscores, or hyphens (e.g. "spez").`);
148
+ return name;
149
+ }
150
+ /**
151
+ * Normalize a post or comment ID to its bare base36 form, dropping any `t1_`/`t3_` fullname
152
+ * prefix. Use this wherever the ID lands in a path segment or query value.
153
+ */
154
+ function normalizeThingId(input) {
155
+ const id = bare(input, [
156
+ "t1_",
157
+ "t3_",
158
+ "t4_",
159
+ "t5_"
160
+ ]).toLowerCase();
161
+ if (!THING_ID_PATTERN.test(id)) throw new ValidationError(`Invalid Reddit ID "${input}". Expected a base36 id such as "1abc2de", optionally prefixed with t1_ or t3_.`);
162
+ return id;
163
+ }
164
+ /**
165
+ * Normalize an ID to a Reddit fullname (`t3_1abc2de`), preserving an explicit `t1_`/`t3_` prefix
166
+ * and falling back to `defaultKind` when the caller passed a bare ID.
167
+ */
168
+ function normalizeFullname(input, defaultKind) {
169
+ const trimmed = input.trim().toLowerCase();
170
+ return `${trimmed.startsWith("t1_") ? "t1" : trimmed.startsWith("t3_") ? "t3" : defaultKind}_${normalizeThingId(input)}`;
171
+ }
172
+ //#endregion
93
173
  //#region src/client/response-cache.ts
94
174
  const SECOND = 1e3;
95
175
  var ResponseCache = class {
@@ -160,6 +240,10 @@ function listingCursor(data) {
160
240
  ...before
161
241
  };
162
242
  }
243
+ function headerValue(response, name) {
244
+ const headers = response.headers;
245
+ return (headers === null || headers === void 0 ? void 0 : headers.get(name)) ?? "";
246
+ }
163
247
  function parsePostData(post) {
164
248
  return {
165
249
  id: post.id,
@@ -254,6 +338,7 @@ var RedditClient = class {
254
338
  ...headers,
255
339
  Authorization: await this.reauthorize()
256
340
  }, path, 0) : first;
341
+ if (!requiresAuth && response.status === 403 && !headerValue(response, "content-type").includes("json")) throw new NetworkBlockedError("Reddit is blocking unauthenticated requests from this network (HTTP 403 with a block page). Set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET to authenticate with OAuth, which also raises the rate limit from ~10 to 60+ requests/min. See https://www.reddit.com/prefs/apps to create an app.");
257
342
  if (cacheable && response.ok) {
258
343
  const text = await response.text();
259
344
  this.cache.set(url, text, response.status);
@@ -381,7 +466,7 @@ var RedditClient = class {
381
466
  async getUser(username) {
382
467
  const context = `Failed to get user info for ${username}`;
383
468
  return (await Try.async(async () => {
384
- const response = (await this.makeRequest(`/user/${username}/about.json`)).orThrow();
469
+ const response = (await this.makeRequest(`/user/${normalizeUsername(username)}/about.json`)).orThrow();
385
470
  if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
386
471
  const { data } = await response.json();
387
472
  return {
@@ -431,14 +516,14 @@ var RedditClient = class {
431
516
  const { limit = 25, after } = options;
432
517
  const params = new URLSearchParams({ limit: limit.toString() });
433
518
  if (after !== void 0) params.set("after", after);
434
- return this.getUserContent(`/user/${this.username}/overview.json?${params}`, "Failed to get your overview");
519
+ return this.getUserContent(`/user/${encodeURIComponent(this.username)}/overview.json?${params}`, "Failed to get your overview");
435
520
  }
436
521
  async getMySaved(options = {}) {
437
522
  if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching saved content requires REDDIT_USERNAME"));
438
523
  const { limit = 25, after } = options;
439
524
  const params = new URLSearchParams({ limit: limit.toString() });
440
525
  if (after !== void 0) params.set("after", after);
441
- return this.getUserContent(`/user/${this.username}/saved.json?${params}`, "Failed to get saved content");
526
+ return this.getUserContent(`/user/${encodeURIComponent(this.username)}/saved.json?${params}`, "Failed to get saved content");
442
527
  }
443
528
  async getMe() {
444
529
  if (this.username === void 0) return Left(new NotAuthenticatedError("Fetching your account requires REDDIT_USERNAME"));
@@ -464,7 +549,7 @@ var RedditClient = class {
464
549
  async getSubredditInfo(subredditName) {
465
550
  const context = `Failed to get subreddit info for ${subredditName}`;
466
551
  return (await Try.async(async () => {
467
- const response = (await this.makeRequest(`/r/${subredditName}/about.json`)).orThrow();
552
+ const response = (await this.makeRequest(`/r/${normalizeSubreddit(subredditName)}/about.json`)).orThrow();
468
553
  if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
469
554
  const { data } = await response.json();
470
555
  return {
@@ -484,7 +569,7 @@ var RedditClient = class {
484
569
  async getSubredditRules(subreddit) {
485
570
  const context = `Failed to get rules for r/${subreddit}`;
486
571
  return (await Try.async(async () => {
487
- const response = (await this.makeRequest(`/r/${subreddit}/about/rules.json`)).orThrow();
572
+ const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/about/rules.json`)).orThrow();
488
573
  if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
489
574
  return (await response.json()).rules.map((rule) => ({
490
575
  shortName: rule.short_name,
@@ -499,7 +584,7 @@ var RedditClient = class {
499
584
  async getPostFlairs(subreddit) {
500
585
  const context = `Failed to get post flairs for r/${subreddit}`;
501
586
  return (await Try.async(async () => {
502
- const response = (await this.makeRequest(`/r/${subreddit}/api/link_flair_v2.json`)).orThrow();
587
+ const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/api/link_flair_v2.json`)).orThrow();
503
588
  if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
504
589
  return (await response.json()).map((flair) => ({
505
590
  id: flair.id,
@@ -510,7 +595,6 @@ var RedditClient = class {
510
595
  })).toEither((error) => classifyRedditError(error, context));
511
596
  }
512
597
  async getTopPosts(subreddit, timeFilter = "week", limit = 10, after) {
513
- const endpoint = subreddit !== "" ? `/r/${subreddit}/top.json` : "/top.json";
514
598
  const params = new URLSearchParams({
515
599
  t: timeFilter,
516
600
  limit: limit.toString()
@@ -518,6 +602,8 @@ var RedditClient = class {
518
602
  if (after !== void 0) params.set("after", after);
519
603
  const context = `Failed to get top posts for ${subreddit !== "" ? subreddit : "home"}`;
520
604
  return (await Try.async(async () => {
605
+ const name = normalizeSubreddit(subreddit);
606
+ const endpoint = name !== "" ? `/r/${name}/top.json` : "/top.json";
521
607
  const response = (await this.makeRequest(`${endpoint}?${params}`)).orThrow();
522
608
  if (!response.ok) throw new HttpError(response.status, `Failed to get top posts: HTTP ${response.status}`);
523
609
  const json = await response.json();
@@ -536,13 +622,14 @@ var RedditClient = class {
536
622
  "controversial"
537
623
  ];
538
624
  if (!validSorts.includes(sort)) return Left(new ValidationError(`Invalid sort "${sort}". Valid options are: ${validSorts.join(", ")}`));
539
- const endpoint = subreddit !== "" ? `/r/${subreddit}/${sort}.json` : `/${sort}.json`;
540
625
  const params = new URLSearchParams({ limit: limit.toString() });
541
626
  if (sort === "top" || sort === "controversial") params.set("t", timeFilter);
542
627
  if (after !== void 0) params.set("after", after);
543
628
  const home = subreddit !== "" ? subreddit : "home";
544
629
  const context = `Failed to browse r/${home} (${sort})`;
545
630
  return (await Try.async(async () => {
631
+ const name = normalizeSubreddit(subreddit);
632
+ const endpoint = name !== "" ? `/r/${name}/${sort}.json` : `/${sort}.json`;
546
633
  const response = (await this.makeRequest(`${endpoint}?${params}`)).orThrow();
547
634
  if (!response.ok) throw new HttpError(response.status, `Failed to browse r/${home}: HTTP ${response.status}`);
548
635
  const json = await response.json();
@@ -553,9 +640,10 @@ var RedditClient = class {
553
640
  })).toEither((error) => classifyRedditError(error, context));
554
641
  }
555
642
  async getPost(postId, subreddit) {
556
- const endpoint = Option(subreddit).fold(() => `/api/info.json?id=t3_${postId}`, (sr) => `/r/${sr}/comments/${postId}.json`);
557
643
  const context = `Failed to get post with ID ${postId}`;
558
644
  return (await Try.async(async () => {
645
+ const id = normalizeThingId(postId);
646
+ const endpoint = Option(subreddit).fold(() => `/api/info.json?id=t3_${id}`, (sr) => `/r/${normalizeSubreddit(sr)}/comments/${id}.json`);
559
647
  const response = (await this.makeRequest(endpoint)).orThrow();
560
648
  if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
561
649
  if (subreddit !== void 0) return parsePostData((await response.json())[0].data.children[0].data);
@@ -579,10 +667,12 @@ var RedditClient = class {
579
667
  this.validateWriteAccess();
580
668
  await this.enforceWriteRateLimit();
581
669
  this.checkDuplicateContent(title + content, subreddit);
670
+ const targetSubreddit = normalizeSubreddit(subreddit);
671
+ if (targetSubreddit === "") throw new ValidationError("A subreddit is required to create a post.");
582
672
  const finalContent = isSelf ? this.appendBotDisclosure(content) : content;
583
673
  const kind = isSelf ? "self" : "link";
584
674
  const params = new URLSearchParams();
585
- params.append("sr", subreddit);
675
+ params.append("sr", targetSubreddit);
586
676
  params.append("kind", kind);
587
677
  params.append("title", title);
588
678
  params.append(isSelf ? "text" : "url", finalContent);
@@ -599,12 +689,12 @@ var RedditClient = class {
599
689
  if (json.json.errors !== void 0 && json.json.errors.length > 0) throw new ApiError(`Reddit API errors: ${json.json.errors.map((e) => e[1]).join(", ")}`);
600
690
  const postId = ((_json$json$data = json.json.data) === null || _json$json$data === void 0 ? void 0 : _json$json$data.id) ?? ((_json$json$data2 = json.json.data) === null || _json$json$data2 === void 0 || (_json$json$data2 = _json$json$data2.name) === null || _json$json$data2 === void 0 ? void 0 : _json$json$data2.replace("t3_", ""));
601
691
  if (postId === void 0) throw new ApiError("No post ID returned from Reddit");
602
- return (await this.getPost(postId, subreddit)).orThrow();
692
+ return (await this.getPost(postId, targetSubreddit)).orThrow();
603
693
  })).toEither((error) => classifyRedditError(error));
604
694
  }
605
695
  async checkPostExists(postId) {
606
696
  return (await Try.async(async () => {
607
- const response = (await this.makeRequest(`/api/info.json?id=t3_${postId}`)).orThrow();
697
+ const response = (await this.makeRequest(`/api/info.json?id=t3_${normalizeThingId(postId)}`)).orThrow();
608
698
  if (!response.ok) return false;
609
699
  return (await response.json()).data.children.length > 0;
610
700
  })).orElse(false);
@@ -616,9 +706,9 @@ var RedditClient = class {
616
706
  await this.enforceWriteRateLimit();
617
707
  this.checkDuplicateContent(content);
618
708
  const finalContent = this.appendBotDisclosure(content);
619
- const fullThingId = postId.startsWith("t3_") || postId.startsWith("t1_") ? postId : `t3_${postId}`;
620
- if (!postId.startsWith("t1_")) {
621
- if (!await this.checkPostExists(postId.replace(/^t3_/, ""))) throw new NotFoundError(`Post with ID ${postId} does not exist or is not accessible`);
709
+ const fullThingId = normalizeFullname(postId, "t3");
710
+ if (!fullThingId.startsWith("t1_")) {
711
+ if (!await this.checkPostExists(normalizeThingId(postId))) throw new NotFoundError(`Post with ID ${postId} does not exist or is not accessible`);
622
712
  }
623
713
  const params = new URLSearchParams();
624
714
  params.append("thing_id", fullThingId);
@@ -651,10 +741,10 @@ var RedditClient = class {
651
741
  else throw new ApiError("Failed to parse reply response");
652
742
  })).toEither((error) => classifyRedditError(error));
653
743
  }
654
- async deletePost(thingId) {
744
+ async deleteThing(thingId, defaultKind) {
655
745
  return (await Try.async(async () => {
656
746
  this.validateWriteAccess();
657
- const fullThingId = thingId.startsWith("t3_") || thingId.startsWith("t1_") ? thingId : `t3_${thingId}`;
747
+ const fullThingId = normalizeFullname(thingId, defaultKind);
658
748
  const params = new URLSearchParams();
659
749
  params.append("id", fullThingId);
660
750
  const response = (await this.makeRequest("/api/del", {
@@ -675,17 +765,19 @@ var RedditClient = class {
675
765
  return classifyRedditError(error);
676
766
  });
677
767
  }
768
+ async deletePost(thingId) {
769
+ return this.deleteThing(thingId, "t3");
770
+ }
678
771
  async deleteComment(thingId) {
679
- const fullThingId = thingId.startsWith("t1_") ? thingId : `t1_${thingId}`;
680
- return this.deletePost(fullThingId);
772
+ return this.deleteThing(thingId, "t1");
681
773
  }
682
- async editPost(thingId, newText) {
774
+ async editThing(thingId, newText, defaultKind) {
683
775
  return (await Try.async(async () => {
684
776
  this.validateWriteAccess();
685
777
  await this.enforceWriteRateLimit();
686
778
  this.checkDuplicateContent(newText);
687
779
  const finalText = this.appendBotDisclosure(newText);
688
- const fullThingId = thingId.startsWith("t3_") || thingId.startsWith("t1_") ? thingId : `t3_${thingId}`;
780
+ const fullThingId = normalizeFullname(thingId, defaultKind);
689
781
  const params = new URLSearchParams();
690
782
  params.append("thing_id", fullThingId);
691
783
  params.append("text", finalText);
@@ -701,13 +793,14 @@ var RedditClient = class {
701
793
  return true;
702
794
  })).toEither((error) => classifyRedditError(error));
703
795
  }
796
+ async editPost(thingId, newText) {
797
+ return this.editThing(thingId, newText, "t3");
798
+ }
704
799
  async editComment(thingId, newText) {
705
- const fullThingId = thingId.startsWith("t1_") ? thingId : `t1_${thingId}`;
706
- return this.editPost(fullThingId, newText);
800
+ return this.editThing(thingId, newText, "t1");
707
801
  }
708
802
  async searchReddit(query, options = {}) {
709
803
  const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link", after, before } = options;
710
- const endpoint = Option(subreddit).fold(() => "/search.json", (sr) => `/r/${sr}/search.json`);
711
804
  const params = new URLSearchParams({
712
805
  q: query,
713
806
  sort,
@@ -720,6 +813,7 @@ var RedditClient = class {
720
813
  });
721
814
  const context = `Failed to search Reddit for: ${query}`;
722
815
  return (await Try.async(async () => {
816
+ const endpoint = Option(subreddit).fold(() => "/search.json", (sr) => `/r/${normalizeSubreddit(sr)}/search.json`);
723
817
  const response = (await this.makeRequest(`${endpoint}?${params}`)).orThrow();
724
818
  if (!response.ok) throw new HttpError(response.status, `Failed to search Reddit: HTTP ${response.status}`);
725
819
  const json = await response.json();
@@ -737,7 +831,7 @@ var RedditClient = class {
737
831
  });
738
832
  const context = `Failed to get comments for post ${postId}`;
739
833
  return (await Try.async(async () => {
740
- const response = (await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`)).orThrow();
834
+ const response = (await this.makeRequest(`/r/${normalizeSubreddit(subreddit)}/comments/${normalizeThingId(postId)}.json?${params}`)).orThrow();
741
835
  if (!response.ok) throw new HttpError(response.status, `Failed to get comments: HTTP ${response.status}`);
742
836
  const json = await response.json();
743
837
  const postData = json[0].data.children[0].data;
@@ -769,15 +863,14 @@ var RedditClient = class {
769
863
  })).toEither((error) => classifyRedditError(error, context));
770
864
  }
771
865
  async getMoreComments(linkId, commentIds) {
772
- const fullLinkId = linkId.startsWith("t3_") ? linkId : `t3_${linkId}`;
773
- const context = `Failed to expand comments for ${fullLinkId}`;
774
- const params = new URLSearchParams({
775
- api_type: "json",
776
- link_id: fullLinkId,
777
- children: commentIds.join(",")
778
- });
866
+ const context = `Failed to expand comments for ${linkId}`;
779
867
  return (await Try.async(async () => {
780
868
  var _json$json$data4;
869
+ const params = new URLSearchParams({
870
+ api_type: "json",
871
+ link_id: normalizeFullname(linkId, "t3"),
872
+ children: commentIds.map((id) => normalizeThingId(id)).join(",")
873
+ });
781
874
  const response = (await this.makeRequest(`/api/morechildren?${params}`)).orThrow();
782
875
  if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
783
876
  return (((_json$json$data4 = (await response.json()).json.data) === null || _json$json$data4 === void 0 ? void 0 : _json$json$data4.things) ?? []).filter((thing) => thing.kind === "t1" && thing.data.body !== void 0).map((thing) => {
@@ -809,7 +902,7 @@ var RedditClient = class {
809
902
  if (after !== void 0) params.set("after", after);
810
903
  const context = `Failed to get posts for user ${username}`;
811
904
  return (await Try.async(async () => {
812
- const response = (await this.makeRequest(`/user/${username}/submitted.json?${params}`)).orThrow();
905
+ const response = (await this.makeRequest(`/user/${normalizeUsername(username)}/submitted.json?${params}`)).orThrow();
813
906
  if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
814
907
  const json = await response.json();
815
908
  return {
@@ -828,7 +921,7 @@ var RedditClient = class {
828
921
  if (after !== void 0) params.set("after", after);
829
922
  const context = `Failed to get comments for user ${username}`;
830
923
  return (await Try.async(async () => {
831
- const response = (await this.makeRequest(`/user/${username}/comments.json?${params}`)).orThrow();
924
+ const response = (await this.makeRequest(`/user/${normalizeUsername(username)}/comments.json?${params}`)).orThrow();
832
925
  if (!response.ok) throw new HttpError(response.status, `${context}: HTTP ${response.status}`);
833
926
  const json = await response.json();
834
927
  return {
@@ -872,7 +965,7 @@ function formatTimestamp(timestamp) {
872
965
  function analyzeUserActivity(karmaRatio, isMod, accountAgeDays) {
873
966
  return [
874
967
  ...karmaRatio > 5 ? ["Primarily a commenter, highly engaged in discussions"] : karmaRatio < .2 ? ["Content creator, focuses on sharing posts"] : ["Balanced participation in both posting and commenting"],
875
- ...accountAgeDays < 30 ? ["New user, still exploring Reddit"] : accountAgeDays > 365 * 5 ? ["Long-time Redditor with extensive platform experience"] : [],
968
+ ...accountAgeDays < 30 ? ["New user, still exploring Reddit"] : accountAgeDays > 1825 ? ["Long-time Redditor with extensive platform experience"] : [],
876
969
  ...isMod ? ["Community leader who helps maintain subreddit quality"] : []
877
970
  ].join("\n - ");
878
971
  }
@@ -882,7 +975,7 @@ function analyzePostEngagement(score, ratio, numComments) {
882
975
  function analyzeSubredditHealth(subscribers, activeUsers, ageDays) {
883
976
  const sizeInsights = subscribers > 1e6 ? ["Major subreddit with massive following"] : subscribers > 1e5 ? ["Well-established community"] : subscribers < 1e3 ? ["Niche community, potential for growth"] : [];
884
977
  const activityInsights = activeUsers.map((active) => active / subscribers).fold(() => [], (activityRatio) => activityRatio > .1 ? ["Highly active community with strong engagement"] : activityRatio < .01 ? ["Could benefit from more community engagement initiatives"] : []);
885
- const ageInsights = ageDays > 365 * 5 ? ["Mature subreddit with established culture"] : ageDays < 90 ? ["New subreddit still forming its community"] : [];
978
+ const ageInsights = ageDays > 1825 ? ["Mature subreddit with established culture"] : ageDays < 90 ? ["New subreddit still forming its community"] : [];
886
979
  return [
887
980
  ...sizeInsights,
888
981
  ...activityInsights,
@@ -910,7 +1003,7 @@ function getSubredditEngagementTips(subreddit) {
910
1003
  return allTips.length > 0 ? allTips.join("\n - ") : "Regular engagement recommended to maintain community presence";
911
1004
  }
912
1005
  function formatUserInfo(user) {
913
- const accountAgeDays = (Date.now() / 1e3 - user.createdUtc) / (24 * 3600);
1006
+ const accountAgeDays = (Date.now() / 1e3 - user.createdUtc) / 86400;
914
1007
  const karmaRatio = user.commentKarma / (user.linkKarma === 0 ? 1 : user.linkKarma);
915
1008
  const status = [
916
1009
  ...user.isMod ? ["Moderator"] : [],
@@ -965,7 +1058,7 @@ function formatPostInfo(post) {
965
1058
  }
966
1059
  function formatSubredditInfo(subreddit) {
967
1060
  const flags = [...subreddit.over18 ? ["NSFW"] : [], ...Option(subreddit.subredditType).fold(() => [], (type) => [`Type: ${type}`])];
968
- const ageDays = (Date.now() / 1e3 - subreddit.createdUtc) / (24 * 3600);
1061
+ const ageDays = (Date.now() / 1e3 - subreddit.createdUtc) / 86400;
969
1062
  return {
970
1063
  name: subreddit.displayName,
971
1064
  title: subreddit.title,
@@ -992,7 +1085,7 @@ function formatSubredditInfo(subreddit) {
992
1085
  //#endregion
993
1086
  //#region src/index.ts
994
1087
  dotenv.config({ quiet: true });
995
- const VERSION = "1.5.1";
1088
+ const VERSION = "1.5.3";
996
1089
  function validateUserAgent(userAgent, username) {
997
1090
  if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
998
1091
  console.error("[Warning] User-Agent does not follow Reddit's recommended format");