reddit-mcp-server 1.3.2 → 1.4.1

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/dist/index.js CHANGED
@@ -2,39 +2,68 @@ import crypto from "crypto";
2
2
  import dotenv from "dotenv";
3
3
  import { FastMCP } from "fastmcp";
4
4
  import { z } from "zod";
5
+ import { Left, Option, Right, Try } from "functype";
5
6
 
6
7
  //#region src/client/reddit-client.ts
8
+ function toError(error) {
9
+ return error instanceof Error ? error : new Error(String(error));
10
+ }
11
+ function parsePostData(post) {
12
+ return {
13
+ id: post.id,
14
+ title: post.title,
15
+ author: post.author,
16
+ subreddit: post.subreddit,
17
+ selftext: post.selftext,
18
+ url: post.url,
19
+ score: post.score,
20
+ upvoteRatio: post.upvote_ratio,
21
+ numComments: post.num_comments,
22
+ createdUtc: post.created_utc,
23
+ over18: post.over_18,
24
+ spoiler: post.spoiler,
25
+ edited: Boolean(post.edited),
26
+ isSelf: post.is_self,
27
+ linkFlairText: post.link_flair_text ?? void 0,
28
+ permalink: post.permalink
29
+ };
30
+ }
7
31
  var RedditClient = class {
8
32
  clientId;
9
33
  clientSecret;
10
34
  userAgent;
11
35
  username;
12
36
  password;
13
- accessToken;
14
- tokenExpiry = 0;
15
37
  baseUrl;
16
- authenticated = false;
17
38
  authMode;
18
39
  hasCredentials;
19
40
  safeMode;
41
+ botDisclosure;
42
+ accessToken;
43
+ tokenExpiry = 0;
44
+ authenticated = false;
20
45
  lastWriteTime = 0;
21
- recentContentHashes = /* @__PURE__ */ new Set();
46
+ recentContentRecords = [];
22
47
  constructor(config) {
23
48
  this.clientId = config.clientId;
24
49
  this.clientSecret = config.clientSecret;
25
50
  this.userAgent = config.userAgent;
26
51
  this.username = config.username;
27
52
  this.password = config.password;
28
- this.authMode = config.authMode || "auto";
29
- this.hasCredentials = !!(this.clientId && this.clientSecret);
53
+ this.authMode = config.authMode ?? "auto";
54
+ this.hasCredentials = Boolean(this.clientId && this.clientSecret);
30
55
  this.baseUrl = this.determineBaseUrl();
31
- this.safeMode = config.safeMode || {
56
+ this.safeMode = config.safeMode ?? {
32
57
  enabled: false,
33
58
  mode: "off",
34
59
  writeDelayMs: 0,
35
60
  duplicateCheck: false,
36
61
  maxRecentHashes: 10
37
62
  };
63
+ this.botDisclosure = config.botDisclosure ?? {
64
+ enabled: false,
65
+ footer: ""
66
+ };
38
67
  }
39
68
  determineBaseUrl() {
40
69
  switch (this.authMode) {
@@ -44,49 +73,53 @@ var RedditClient = class {
44
73
  }
45
74
  }
46
75
  async makeRequest(path, options = {}) {
47
- const requiresAuth = this.authMode === "authenticated" || this.authMode === "auto" && this.hasCredentials;
48
- if (requiresAuth && (Date.now() >= this.tokenExpiry || !this.authenticated)) await this.authenticate();
49
- const url = `${this.baseUrl}${path}`;
50
- const headers = {
51
- "User-Agent": this.userAgent,
52
- ...options.headers
53
- };
54
- if (requiresAuth && this.accessToken) headers.Authorization = `Bearer ${this.accessToken}`;
55
- const response = await fetch(url, {
56
- ...options,
57
- headers
58
- });
59
- if (response.status === 401 && this.authenticated) {
60
- await this.authenticate();
61
- const retryHeaders = {
62
- ...headers,
63
- Authorization: `Bearer ${this.accessToken}`
76
+ try {
77
+ const requiresAuth = this.authMode === "authenticated" || this.authMode === "auto" && this.hasCredentials;
78
+ if (requiresAuth && (Date.now() >= this.tokenExpiry || !this.authenticated)) (await this.authenticate()).orThrow();
79
+ const url = `${this.baseUrl}${path}`;
80
+ const headers = {
81
+ "User-Agent": this.userAgent,
82
+ ...options.headers
64
83
  };
65
- return fetch(url, {
84
+ if (requiresAuth && this.accessToken !== void 0) headers["Authorization"] = `Bearer ${this.accessToken}`;
85
+ const response = await fetch(url, {
66
86
  ...options,
67
- headers: retryHeaders
87
+ headers
68
88
  });
89
+ if (response.status === 401 && this.authenticated) {
90
+ (await this.authenticate()).orThrow();
91
+ const retryHeaders = {
92
+ ...headers,
93
+ Authorization: `Bearer ${this.accessToken}`
94
+ };
95
+ return Right(await fetch(url, {
96
+ ...options,
97
+ headers: retryHeaders
98
+ }));
99
+ }
100
+ return Right(response);
101
+ } catch (error) {
102
+ return Left(toError(error));
69
103
  }
70
- return response;
71
104
  }
72
105
  async authenticate() {
73
106
  if (this.authMode === "anonymous") {
74
107
  this.authenticated = false;
75
- return;
108
+ return Right(void 0);
76
109
  }
77
- if (this.authMode === "authenticated" && !this.hasCredentials) throw new Error("Authenticated mode requires REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET");
110
+ if (this.authMode === "authenticated" && !this.hasCredentials) return Left(/* @__PURE__ */ new Error("Authenticated mode requires REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET"));
78
111
  if (this.authMode === "auto" && !this.hasCredentials) {
79
112
  this.authenticated = false;
80
- return;
113
+ return Right(void 0);
81
114
  }
82
115
  try {
83
116
  const now = Date.now();
84
- if (this.accessToken && now < this.tokenExpiry) return;
117
+ if (this.accessToken !== void 0 && now < this.tokenExpiry) return Right(void 0);
85
118
  const authUrl = "https://www.reddit.com/api/v1/access_token";
86
119
  const authData = new URLSearchParams();
87
120
  const { username } = this;
88
121
  const { password } = this;
89
- if (!!(username && password)) {
122
+ if (Boolean(username && password) && username !== void 0 && password !== void 0) {
90
123
  authData.append("grant_type", "password");
91
124
  authData.append("username", username);
92
125
  authData.append("password", password);
@@ -102,29 +135,24 @@ var RedditClient = class {
102
135
  body: authData.toString()
103
136
  });
104
137
  if (!response.ok) {
105
- const statusText = response.statusText || "Unknown Error";
106
- throw new Error(`Authentication failed: ${response.status} ${statusText}`);
138
+ const statusText = response.statusText !== "" ? response.statusText : "Unknown Error";
139
+ return Left(/* @__PURE__ */ new Error(`Authentication failed: ${response.status} ${statusText}`));
107
140
  }
108
141
  const data = await response.json();
109
142
  this.accessToken = data.access_token;
110
143
  this.tokenExpiry = now + data.expires_in * 1e3;
111
144
  this.authenticated = true;
145
+ return Right(void 0);
112
146
  } catch (error) {
113
- if (error instanceof Error) throw error;
114
- throw new Error("Failed to authenticate with Reddit API", { cause: error });
147
+ return Left(toError(error));
115
148
  }
116
149
  }
117
150
  async checkAuthentication() {
118
- if (!this.authenticated) try {
119
- await this.authenticate();
120
- return true;
121
- } catch {
122
- return false;
123
- }
151
+ if (!this.authenticated) return (await this.authenticate()).isRight();
124
152
  return true;
125
153
  }
126
154
  validateWriteAccess() {
127
- if (!this.username || !this.password) {
155
+ if (this.username === void 0 || this.password === void 0) {
128
156
  if (this.authMode === "anonymous") throw new Error("Write operations not available in anonymous mode. Set REDDIT_USERNAME, REDDIT_PASSWORD and use 'auto' or 'authenticated' mode.");
129
157
  throw new Error("Write operations require REDDIT_USERNAME and REDDIT_PASSWORD");
130
158
  }
@@ -142,171 +170,143 @@ var RedditClient = class {
142
170
  hashContent(content) {
143
171
  return crypto.createHash("sha256").update(content.trim().toLowerCase()).digest("hex");
144
172
  }
145
- checkDuplicateContent(content) {
173
+ checkDuplicateContent(content, subreddit) {
146
174
  if (!this.safeMode.enabled || !this.safeMode.duplicateCheck) return;
147
175
  const hash = this.hashContent(content);
148
- if (this.recentContentHashes.has(hash)) throw new Error("Duplicate content detected. Reddit's spam filter may ban your account for posting identical content. Please modify your content and try again.");
149
- this.recentContentHashes.add(hash);
150
- if (this.recentContentHashes.size > this.safeMode.maxRecentHashes) {
151
- const first = this.recentContentHashes.values().next().value;
152
- if (first) this.recentContentHashes.delete(first);
176
+ const duplicate = this.recentContentRecords.find((record) => record.hash === hash);
177
+ if (duplicate !== void 0) {
178
+ if (subreddit !== void 0 && duplicate.subreddit !== "" && subreddit !== duplicate.subreddit) throw new Error("Cross-subreddit duplicate detected. Reddit's Responsible Builder Policy prohibits posting identical or substantially similar content across multiple subreddits. Please create unique content for each subreddit.");
179
+ throw new Error("Duplicate content detected. Reddit's spam filter may ban your account for posting identical content. Please modify your content and try again.");
153
180
  }
181
+ this.recentContentRecords.push({
182
+ hash,
183
+ subreddit: subreddit ?? "",
184
+ timestamp: Date.now()
185
+ });
186
+ this.recentContentRecords = this.recentContentRecords.slice(-this.safeMode.maxRecentHashes);
187
+ }
188
+ appendBotDisclosure(content) {
189
+ if (!this.botDisclosure.enabled || this.botDisclosure.footer === "") return content;
190
+ return `${content}${this.botDisclosure.footer}`;
154
191
  }
155
192
  async getUser(username) {
156
193
  try {
157
- const response = await this.makeRequest(`/user/${username}/about.json`);
158
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
194
+ const response = (await this.makeRequest(`/user/${username}/about.json`)).orThrow();
195
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to get user info for ${username}: HTTP ${response.status}`));
159
196
  const { data } = await response.json();
160
- return {
197
+ return Right({
161
198
  name: data.name,
162
199
  id: data.id,
163
200
  commentKarma: data.comment_karma,
164
201
  linkKarma: data.link_karma,
165
- totalKarma: data.total_karma || data.comment_karma + data.link_karma,
202
+ totalKarma: data.total_karma ?? data.comment_karma + data.link_karma,
166
203
  isMod: data.is_mod,
167
204
  isGold: data.is_gold,
168
205
  isEmployee: data.is_employee,
169
206
  createdUtc: data.created_utc,
170
207
  profileUrl: `https://reddit.com/user/${data.name}`
171
- };
172
- } catch {
173
- throw new Error(`Failed to get user info for ${username}`);
208
+ });
209
+ } catch (error) {
210
+ return Left(/* @__PURE__ */ new Error(`Failed to get user info for ${username}: ${toError(error).message}`));
174
211
  }
175
212
  }
176
213
  async getSubredditInfo(subredditName) {
177
214
  try {
178
- const response = await this.makeRequest(`/r/${subredditName}/about.json`);
179
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
215
+ const response = (await this.makeRequest(`/r/${subredditName}/about.json`)).orThrow();
216
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to get subreddit info for ${subredditName}: HTTP ${response.status}`));
180
217
  const { data } = await response.json();
181
- return {
218
+ return Right({
182
219
  displayName: data.display_name,
183
220
  title: data.title,
184
- description: data.description || "",
185
- publicDescription: data.public_description || "",
221
+ description: data.description ?? "",
222
+ publicDescription: data.public_description ?? "",
186
223
  subscribers: data.subscribers,
187
224
  activeUserCount: data.active_user_count ?? void 0,
188
225
  createdUtc: data.created_utc,
189
226
  over18: data.over18,
190
227
  subredditType: data.subreddit_type,
191
228
  url: data.url
192
- };
193
- } catch {
194
- throw new Error(`Failed to get subreddit info for ${subredditName}`);
229
+ });
230
+ } catch (error) {
231
+ return Left(/* @__PURE__ */ new Error(`Failed to get subreddit info for ${subredditName}: ${toError(error).message}`));
195
232
  }
196
233
  }
197
234
  async getTopPosts(subreddit, timeFilter = "week", limit = 10) {
235
+ const endpoint = subreddit !== "" ? `/r/${subreddit}/top.json` : "/top.json";
236
+ const params = new URLSearchParams({
237
+ t: timeFilter,
238
+ limit: limit.toString()
239
+ });
198
240
  try {
199
- const endpoint = subreddit ? `/r/${subreddit}/top.json` : "/top.json";
200
- const params = new URLSearchParams({
201
- t: timeFilter,
202
- limit: limit.toString()
203
- });
204
- const response = await this.makeRequest(`${endpoint}?${params}`);
205
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
206
- return (await response.json()).data.children.map((child) => {
207
- const post = child.data;
208
- return {
209
- id: post.id,
210
- title: post.title,
211
- author: post.author,
212
- subreddit: post.subreddit,
213
- selftext: post.selftext,
214
- url: post.url,
215
- score: post.score,
216
- upvoteRatio: post.upvote_ratio,
217
- numComments: post.num_comments,
218
- createdUtc: post.created_utc,
219
- over18: post.over_18,
220
- spoiler: post.spoiler,
221
- edited: !!post.edited,
222
- isSelf: post.is_self,
223
- linkFlairText: post.link_flair_text ?? void 0,
224
- permalink: post.permalink
225
- };
226
- });
227
- } catch {
228
- throw new Error(`Failed to get top posts for ${subreddit || "home"}`);
241
+ const response = (await this.makeRequest(`${endpoint}?${params}`)).orThrow();
242
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to get top posts: HTTP ${response.status}`));
243
+ return Right((await response.json()).data.children.map((child) => parsePostData(child.data)));
244
+ } catch (error) {
245
+ return Left(/* @__PURE__ */ new Error(`Failed to get top posts for ${subreddit !== "" ? subreddit : "home"}: ${toError(error).message}`));
229
246
  }
230
247
  }
231
248
  async getPost(postId, subreddit) {
249
+ const endpoint = subreddit !== void 0 ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`;
232
250
  try {
233
- const endpoint = subreddit ? `/r/${subreddit}/comments/${postId}.json` : `/api/info.json?id=t3_${postId}`;
234
- const response = await this.makeRequest(endpoint);
235
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
251
+ const response = (await this.makeRequest(endpoint)).orThrow();
252
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to get post with ID ${postId}: HTTP ${response.status}`));
236
253
  let post;
237
- if (subreddit) post = (await response.json())[0].data.children[0].data;
254
+ if (subreddit !== void 0) post = (await response.json())[0].data.children[0].data;
238
255
  else {
239
256
  const json = await response.json();
240
- if (!json.data.children.length) throw new Error(`Post with ID ${postId} not found`);
257
+ if (json.data.children.length === 0) return Left(/* @__PURE__ */ new Error(`Post with ID ${postId} not found`));
241
258
  post = json.data.children[0].data;
242
259
  }
243
- return {
244
- id: post.id,
245
- title: post.title,
246
- author: post.author,
247
- subreddit: post.subreddit,
248
- selftext: post.selftext,
249
- url: post.url,
250
- score: post.score,
251
- upvoteRatio: post.upvote_ratio,
252
- numComments: post.num_comments,
253
- createdUtc: post.created_utc,
254
- over18: post.over_18,
255
- spoiler: post.spoiler,
256
- edited: !!post.edited,
257
- isSelf: post.is_self,
258
- linkFlairText: post.link_flair_text ?? void 0,
259
- permalink: post.permalink
260
- };
261
- } catch {
262
- throw new Error(`Failed to get post with ID ${postId}`);
260
+ return Right(parsePostData(post));
261
+ } catch (error) {
262
+ return Left(/* @__PURE__ */ new Error(`Failed to get post with ID ${postId}: ${toError(error).message}`));
263
263
  }
264
264
  }
265
265
  async getTrendingSubreddits(limit = 5) {
266
+ const params = new URLSearchParams({ limit: limit.toString() });
266
267
  try {
267
- const params = new URLSearchParams({ limit: limit.toString() });
268
- const response = await this.makeRequest(`/subreddits/popular.json?${params}`);
269
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
270
- return (await response.json()).data.children.map((child) => child.data.display_name);
271
- } catch {
272
- throw new Error("Failed to get trending subreddits");
268
+ const response = (await this.makeRequest(`/subreddits/popular.json?${params}`)).orThrow();
269
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to get trending subreddits: HTTP ${response.status}`));
270
+ return Right((await response.json()).data.children.map((child) => child.data.display_name));
271
+ } catch (error) {
272
+ return Left(/* @__PURE__ */ new Error(`Failed to get trending subreddits: ${toError(error).message}`));
273
273
  }
274
274
  }
275
275
  async createPost(subreddit, title, content, isSelf = true) {
276
- this.validateWriteAccess();
277
- await this.enforceWriteRateLimit();
278
- this.checkDuplicateContent(title + content);
279
276
  try {
280
277
  var _json$json, _json$json2, _json$json3;
278
+ this.validateWriteAccess();
279
+ await this.enforceWriteRateLimit();
280
+ this.checkDuplicateContent(title + content, subreddit);
281
+ const finalContent = isSelf ? this.appendBotDisclosure(content) : content;
281
282
  const kind = isSelf ? "self" : "link";
282
283
  const params = new URLSearchParams();
283
284
  params.append("sr", subreddit);
284
285
  params.append("kind", kind);
285
286
  params.append("title", title);
286
- params.append(isSelf ? "text" : "url", content);
287
+ params.append(isSelf ? "text" : "url", finalContent);
287
288
  params.append("api_type", "json");
288
- const response = await this.makeRequest("/api/submit", {
289
+ const response = (await this.makeRequest("/api/submit", {
289
290
  method: "POST",
290
291
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
291
292
  body: params.toString()
292
- });
293
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
293
+ })).orThrow();
294
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to create post: HTTP ${response.status}`));
294
295
  const json = await response.json();
295
- if (((_json$json = json.json) === null || _json$json === void 0 ? void 0 : _json$json.errors) && json.json.errors.length > 0) {
296
- const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
297
- throw new Error(`Reddit API errors: ${errors}`);
296
+ if (((_json$json = json.json) === null || _json$json === void 0 ? void 0 : _json$json.errors) !== void 0 && json.json.errors.length > 0) {
297
+ const errors = json.json.errors.map((e) => e[1] ?? e[0]).join(", ");
298
+ return Left(/* @__PURE__ */ new Error(`Reddit API errors: ${errors}`));
298
299
  }
299
- const postId = ((_json$json2 = json.json) === null || _json$json2 === void 0 || (_json$json2 = _json$json2.data) === null || _json$json2 === void 0 ? void 0 : _json$json2.id) || ((_json$json3 = json.json) === null || _json$json3 === void 0 || (_json$json3 = _json$json3.data) === null || _json$json3 === void 0 || (_json$json3 = _json$json3.name) === null || _json$json3 === void 0 ? void 0 : _json$json3.replace("t3_", ""));
300
- if (!postId) throw new Error("No post ID returned from Reddit");
301
- return await this.getPost(postId, subreddit);
300
+ const postId = ((_json$json2 = json.json) === null || _json$json2 === void 0 || (_json$json2 = _json$json2.data) === null || _json$json2 === void 0 ? void 0 : _json$json2.id) ?? ((_json$json3 = json.json) === null || _json$json3 === void 0 || (_json$json3 = _json$json3.data) === null || _json$json3 === void 0 || (_json$json3 = _json$json3.name) === null || _json$json3 === void 0 ? void 0 : _json$json3.replace("t3_", ""));
301
+ if (postId === void 0) return Left(/* @__PURE__ */ new Error("No post ID returned from Reddit"));
302
+ return this.getPost(postId, subreddit);
302
303
  } catch (error) {
303
- if (error instanceof Error) throw error;
304
- throw new Error(`Failed to create post in ${subreddit}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
304
+ return Left(toError(error));
305
305
  }
306
306
  }
307
307
  async checkPostExists(postId) {
308
308
  try {
309
- const response = await this.makeRequest(`/api/info.json?id=t3_${postId}`);
309
+ const response = (await this.makeRequest(`/api/info.json?id=t3_${postId}`)).orThrow();
310
310
  if (!response.ok) return false;
311
311
  return (await response.json()).data.children.length > 0;
312
312
  } catch {
@@ -314,28 +314,31 @@ var RedditClient = class {
314
314
  }
315
315
  }
316
316
  async replyToPost(postId, content) {
317
- this.validateWriteAccess();
318
- await this.enforceWriteRateLimit();
319
- this.checkDuplicateContent(content);
320
317
  try {
321
318
  var _json$json4, _json$json5;
319
+ this.validateWriteAccess();
320
+ await this.enforceWriteRateLimit();
321
+ this.checkDuplicateContent(content);
322
+ const finalContent = this.appendBotDisclosure(content);
322
323
  const fullThingId = postId.startsWith("t3_") || postId.startsWith("t1_") ? postId : `t3_${postId}`;
323
- if (!postId.startsWith("t1_") && !await this.checkPostExists(postId.replace(/^t3_/, ""))) throw new Error(`Post with ID ${postId} does not exist or is not accessible`);
324
+ if (!postId.startsWith("t1_")) {
325
+ if (!await this.checkPostExists(postId.replace(/^t3_/, ""))) return Left(/* @__PURE__ */ new Error(`Post with ID ${postId} does not exist or is not accessible`));
326
+ }
324
327
  const params = new URLSearchParams();
325
328
  params.append("thing_id", fullThingId);
326
- params.append("text", content);
329
+ params.append("text", finalContent);
327
330
  params.append("api_type", "json");
328
- const response = await this.makeRequest("/api/comment", {
331
+ const response = (await this.makeRequest("/api/comment", {
329
332
  method: "POST",
330
333
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
331
334
  body: params.toString()
332
- });
333
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
335
+ })).orThrow();
336
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to reply: HTTP ${response.status}`));
334
337
  const json = await response.json();
335
- if (((_json$json4 = json.json) === null || _json$json4 === void 0 || (_json$json4 = _json$json4.data) === null || _json$json4 === void 0 ? void 0 : _json$json4.things) && json.json.data.things.length > 0) {
338
+ if (((_json$json4 = json.json) === null || _json$json4 === void 0 || (_json$json4 = _json$json4.data) === null || _json$json4 === void 0 ? void 0 : _json$json4.things) !== void 0 && json.json.data.things.length > 0) {
336
339
  const commentData = json.json.data.things[0].data;
337
340
  const author = this.username ?? "[unknown]";
338
- return {
341
+ return Right({
339
342
  id: commentData.id,
340
343
  author,
341
344
  body: content,
@@ -347,39 +350,37 @@ var RedditClient = class {
347
350
  edited: false,
348
351
  isSubmitter: false,
349
352
  permalink: commentData.permalink
350
- };
351
- } else if (((_json$json5 = json.json) === null || _json$json5 === void 0 ? void 0 : _json$json5.errors) && json.json.errors.length > 0) {
352
- const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
353
- throw new Error(`Reddit API errors: ${errors}`);
354
- } else throw new Error("Failed to parse reply response");
353
+ });
354
+ } else if (((_json$json5 = json.json) === null || _json$json5 === void 0 ? void 0 : _json$json5.errors) !== void 0 && json.json.errors.length > 0) {
355
+ const errors = json.json.errors.map((e) => e[1] ?? e[0]).join(", ");
356
+ return Left(/* @__PURE__ */ new Error(`Reddit API errors: ${errors}`));
357
+ } else return Left(/* @__PURE__ */ new Error("Failed to parse reply response"));
355
358
  } catch (error) {
356
- if (error instanceof Error) throw error;
357
- throw new Error(`Failed to reply to post ${postId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
359
+ return Left(toError(error));
358
360
  }
359
361
  }
360
362
  async deletePost(thingId) {
361
- this.validateWriteAccess();
362
363
  try {
364
+ this.validateWriteAccess();
363
365
  const fullThingId = thingId.startsWith("t3_") || thingId.startsWith("t1_") ? thingId : `t3_${thingId}`;
364
366
  const params = new URLSearchParams();
365
367
  params.append("id", fullThingId);
366
- const response = await this.makeRequest("/api/del", {
368
+ const response = (await this.makeRequest("/api/del", {
367
369
  method: "POST",
368
370
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
369
371
  body: params.toString()
370
- });
372
+ })).orThrow();
371
373
  if (!response.ok) {
372
374
  const errorText = await response.text();
373
375
  console.error(`[Reddit API] Delete failed: ${response.status} ${response.statusText}`);
374
376
  console.error(`[Reddit API] Error response: ${errorText}`);
375
- throw new Error(`HTTP ${response.status}: ${errorText}`);
377
+ return Left(/* @__PURE__ */ new Error(`HTTP ${response.status}: ${errorText}`));
376
378
  }
377
379
  console.error(`[Reddit API] Successfully deleted ${fullThingId}`);
378
- return true;
380
+ return Right(true);
379
381
  } catch (error) {
380
382
  console.error(`[Reddit API] Delete exception:`, error);
381
- if (error instanceof Error && error.message.includes("HTTP")) throw error;
382
- throw new Error(`Failed to delete content ${thingId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
383
+ return Left(toError(error));
383
384
  }
384
385
  }
385
386
  async deleteComment(thingId) {
@@ -387,31 +388,31 @@ var RedditClient = class {
387
388
  return this.deletePost(fullThingId);
388
389
  }
389
390
  async editPost(thingId, newText) {
390
- this.validateWriteAccess();
391
- await this.enforceWriteRateLimit();
392
- this.checkDuplicateContent(newText);
393
391
  try {
394
392
  var _json$json6;
393
+ this.validateWriteAccess();
394
+ await this.enforceWriteRateLimit();
395
+ this.checkDuplicateContent(newText);
396
+ const finalText = this.appendBotDisclosure(newText);
395
397
  const fullThingId = thingId.startsWith("t3_") || thingId.startsWith("t1_") ? thingId : `t3_${thingId}`;
396
398
  const params = new URLSearchParams();
397
399
  params.append("thing_id", fullThingId);
398
- params.append("text", newText);
400
+ params.append("text", finalText);
399
401
  params.append("api_type", "json");
400
- const response = await this.makeRequest("/api/editusertext", {
402
+ const response = (await this.makeRequest("/api/editusertext", {
401
403
  method: "POST",
402
404
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
403
405
  body: params.toString()
404
- });
405
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
406
+ })).orThrow();
407
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to edit: HTTP ${response.status}`));
406
408
  const json = await response.json();
407
- if (((_json$json6 = json.json) === null || _json$json6 === void 0 ? void 0 : _json$json6.errors) && json.json.errors.length > 0) {
408
- const errors = json.json.errors.map((e) => e[1] || e[0]).join(", ");
409
- throw new Error(`Reddit API errors: ${errors}`);
409
+ if (((_json$json6 = json.json) === null || _json$json6 === void 0 ? void 0 : _json$json6.errors) !== void 0 && json.json.errors.length > 0) {
410
+ const errors = json.json.errors.map((e) => e[1] ?? e[0]).join(", ");
411
+ return Left(/* @__PURE__ */ new Error(`Reddit API errors: ${errors}`));
410
412
  }
411
- return true;
413
+ return Right(true);
412
414
  } catch (error) {
413
- if (error instanceof Error) throw error;
414
- throw new Error(`Failed to edit content ${thingId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
415
+ return Left(toError(error));
415
416
  }
416
417
  }
417
418
  async editComment(thingId, newText) {
@@ -419,152 +420,92 @@ var RedditClient = class {
419
420
  return this.editPost(fullThingId, newText);
420
421
  }
421
422
  async searchReddit(query, options = {}) {
423
+ const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options;
424
+ const endpoint = subreddit !== void 0 ? `/r/${subreddit}/search.json` : "/search.json";
425
+ const params = new URLSearchParams({
426
+ q: query,
427
+ sort,
428
+ t: timeFilter,
429
+ limit: limit.toString(),
430
+ type,
431
+ ...subreddit !== void 0 ? { restrict_sr: "true" } : {}
432
+ });
422
433
  try {
423
- const { subreddit, sort = "relevance", timeFilter = "all", limit = 25, type = "link" } = options;
424
- const endpoint = subreddit ? `/r/${subreddit}/search.json` : "/search.json";
425
- const params = new URLSearchParams({
426
- q: query,
427
- sort,
428
- t: timeFilter,
429
- limit: limit.toString(),
430
- type,
431
- ...subreddit && { restrict_sr: "true" }
432
- });
433
- const response = await this.makeRequest(`${endpoint}?${params}`);
434
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
435
- return (await response.json()).data.children.filter((child) => child.kind === "t3").map((child) => {
436
- const post = child.data;
437
- return {
438
- id: post.id,
439
- title: post.title,
440
- author: post.author,
441
- subreddit: post.subreddit,
442
- selftext: post.selftext || "",
443
- url: post.url,
444
- score: post.score,
445
- upvoteRatio: post.upvote_ratio,
446
- numComments: post.num_comments,
447
- createdUtc: post.created_utc,
448
- over18: post.over_18,
449
- spoiler: post.spoiler,
450
- edited: !!post.edited,
451
- isSelf: post.is_self,
452
- linkFlairText: post.link_flair_text ?? void 0,
453
- permalink: post.permalink
454
- };
455
- });
456
- } catch {
457
- throw new Error(`Failed to search Reddit for: ${query}`);
434
+ const response = (await this.makeRequest(`${endpoint}?${params}`)).orThrow();
435
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to search Reddit: HTTP ${response.status}`));
436
+ return Right((await response.json()).data.children.filter((child) => child.kind === "t3").map((child) => parsePostData(child.data)));
437
+ } catch (error) {
438
+ return Left(/* @__PURE__ */ new Error(`Failed to search Reddit for: ${query}: ${toError(error).message}`));
458
439
  }
459
440
  }
460
441
  async getPostComments(postId, subreddit, options = {}) {
442
+ const { sort = "best", limit = 100 } = options;
443
+ const params = new URLSearchParams({
444
+ sort,
445
+ limit: limit.toString()
446
+ });
461
447
  try {
462
448
  var _json$;
463
- const { sort = "best", limit = 100 } = options;
464
- const params = new URLSearchParams({
465
- sort,
466
- limit: limit.toString()
467
- });
468
- const response = await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`);
469
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
449
+ const response = (await this.makeRequest(`/r/${subreddit}/comments/${postId}.json?${params}`)).orThrow();
450
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to get comments: HTTP ${response.status}`));
470
451
  const json = await response.json();
471
452
  const postData = json[0].data.children[0].data;
472
- const post = {
473
- id: postData.id,
474
- title: postData.title,
475
- author: postData.author,
476
- subreddit: postData.subreddit,
477
- selftext: postData.selftext || "",
478
- url: postData.url,
479
- score: postData.score,
480
- upvoteRatio: postData.upvote_ratio,
481
- numComments: postData.num_comments,
482
- createdUtc: postData.created_utc,
483
- over18: postData.over_18,
484
- spoiler: postData.spoiler,
485
- edited: !!postData.edited,
486
- isSelf: postData.is_self,
487
- linkFlairText: postData.link_flair_text ?? void 0,
488
- permalink: postData.permalink
489
- };
490
- const comments = [];
491
- const parseComments = (commentData, depth = 0) => {
492
- for (const item of commentData) if (item.kind === "t1" && item.data.body) {
493
- var _replies$data;
494
- comments.push({
495
- id: item.data.id,
496
- author: item.data.author,
497
- body: item.data.body,
498
- score: item.data.score,
499
- controversiality: item.data.controversiality,
500
- subreddit: item.data.subreddit,
501
- submissionTitle: post.title,
502
- createdUtc: item.data.created_utc,
503
- edited: !!item.data.edited,
504
- isSubmitter: item.data.is_submitter,
505
- permalink: item.data.permalink,
506
- depth,
507
- parentId: item.data.parent_id
508
- });
509
- const { replies } = item.data;
510
- if (replies && typeof replies !== "string" && ((_replies$data = replies.data) === null || _replies$data === void 0 ? void 0 : _replies$data.children)) parseComments(replies.data.children, depth + 1);
511
- }
512
- };
513
- if ((_json$ = json[1]) === null || _json$ === void 0 || (_json$ = _json$.data) === null || _json$ === void 0 ? void 0 : _json$.children) parseComments(json[1].data.children);
514
- return {
453
+ const post = parsePostData(postData);
454
+ const parseComments = (commentData, depth = 0) => commentData.flatMap((item) => {
455
+ var _replies$data;
456
+ if (item.kind !== "t1" || item.data.body === void 0) return [];
457
+ const comment = {
458
+ id: item.data.id,
459
+ author: item.data.author,
460
+ body: item.data.body,
461
+ score: item.data.score,
462
+ controversiality: item.data.controversiality,
463
+ subreddit: item.data.subreddit,
464
+ submissionTitle: post.title,
465
+ createdUtc: item.data.created_utc,
466
+ edited: Boolean(item.data.edited),
467
+ isSubmitter: item.data.is_submitter,
468
+ permalink: item.data.permalink,
469
+ depth,
470
+ parentId: item.data.parent_id
471
+ };
472
+ const { replies } = item.data;
473
+ return [comment, ...replies !== void 0 && typeof replies !== "string" && ((_replies$data = replies.data) === null || _replies$data === void 0 ? void 0 : _replies$data.children) !== void 0 ? parseComments(replies.data.children, depth + 1) : []];
474
+ });
475
+ return Right({
515
476
  post,
516
- comments
517
- };
518
- } catch {
519
- throw new Error(`Failed to get comments for post ${postId}`);
477
+ comments: ((_json$ = json[1]) === null || _json$ === void 0 || (_json$ = _json$.data) === null || _json$ === void 0 ? void 0 : _json$.children) !== void 0 ? parseComments(json[1].data.children) : []
478
+ });
479
+ } catch (error) {
480
+ return Left(/* @__PURE__ */ new Error(`Failed to get comments for post ${postId}: ${toError(error).message}`));
520
481
  }
521
482
  }
522
483
  async getUserPosts(username, options = {}) {
484
+ const { sort = "new", timeFilter = "all", limit = 25 } = options;
485
+ const params = new URLSearchParams({
486
+ sort,
487
+ t: timeFilter,
488
+ limit: limit.toString()
489
+ });
523
490
  try {
524
- const { sort = "new", timeFilter = "all", limit = 25 } = options;
525
- const params = new URLSearchParams({
526
- sort,
527
- t: timeFilter,
528
- limit: limit.toString()
529
- });
530
- const response = await this.makeRequest(`/user/${username}/submitted.json?${params}`);
531
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
532
- return (await response.json()).data.children.filter((child) => child.kind === "t3").map((child) => {
533
- const post = child.data;
534
- return {
535
- id: post.id,
536
- title: post.title,
537
- author: post.author,
538
- subreddit: post.subreddit,
539
- selftext: post.selftext || "",
540
- url: post.url,
541
- score: post.score,
542
- upvoteRatio: post.upvote_ratio,
543
- numComments: post.num_comments,
544
- createdUtc: post.created_utc,
545
- over18: post.over_18,
546
- spoiler: post.spoiler,
547
- edited: !!post.edited,
548
- isSelf: post.is_self,
549
- linkFlairText: post.link_flair_text ?? void 0,
550
- permalink: post.permalink
551
- };
552
- });
553
- } catch {
554
- throw new Error(`Failed to get posts for user ${username}`);
491
+ const response = (await this.makeRequest(`/user/${username}/submitted.json?${params}`)).orThrow();
492
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to get posts for user ${username}: HTTP ${response.status}`));
493
+ return Right((await response.json()).data.children.filter((child) => child.kind === "t3").map((child) => parsePostData(child.data)));
494
+ } catch (error) {
495
+ return Left(/* @__PURE__ */ new Error(`Failed to get posts for user ${username}: ${toError(error).message}`));
555
496
  }
556
497
  }
557
498
  async getUserComments(username, options = {}) {
499
+ const { sort = "new", timeFilter = "all", limit = 25 } = options;
500
+ const params = new URLSearchParams({
501
+ sort,
502
+ t: timeFilter,
503
+ limit: limit.toString()
504
+ });
558
505
  try {
559
- const { sort = "new", timeFilter = "all", limit = 25 } = options;
560
- const params = new URLSearchParams({
561
- sort,
562
- t: timeFilter,
563
- limit: limit.toString()
564
- });
565
- const response = await this.makeRequest(`/user/${username}/comments.json?${params}`);
566
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
567
- return (await response.json()).data.children.filter((child) => child.kind === "t1").map((child) => {
506
+ const response = (await this.makeRequest(`/user/${username}/comments.json?${params}`)).orThrow();
507
+ if (!response.ok) return Left(/* @__PURE__ */ new Error(`Failed to get comments for user ${username}: HTTP ${response.status}`));
508
+ return Right((await response.json()).data.children.filter((child) => child.kind === "t1").map((child) => {
568
509
  const comment = child.data;
569
510
  return {
570
511
  id: comment.id,
@@ -575,79 +516,60 @@ var RedditClient = class {
575
516
  subreddit: comment.subreddit,
576
517
  submissionTitle: comment.link_title ?? "",
577
518
  createdUtc: comment.created_utc,
578
- edited: !!comment.edited,
519
+ edited: Boolean(comment.edited),
579
520
  isSubmitter: comment.is_submitter,
580
521
  permalink: comment.permalink
581
522
  };
582
- });
583
- } catch {
584
- throw new Error(`Failed to get comments for user ${username}`);
523
+ }));
524
+ } catch (error) {
525
+ return Left(/* @__PURE__ */ new Error(`Failed to get comments for user ${username}: ${toError(error).message}`));
585
526
  }
586
527
  }
587
528
  };
588
- let redditClient = null;
529
+ let clientInstance = Option.none();
589
530
  function initializeRedditClient(config) {
590
- redditClient = new RedditClient(config);
591
- return redditClient;
531
+ const client = new RedditClient(config);
532
+ clientInstance = Option(client);
533
+ return client;
592
534
  }
593
535
  function getRedditClient() {
594
- return redditClient;
536
+ return clientInstance;
595
537
  }
596
538
 
597
539
  //#endregion
598
540
  //#region src/utils/formatters.ts
599
541
  function formatTimestamp(timestamp) {
600
- try {
542
+ return Try(() => {
601
543
  return (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
602
- } catch {
603
- return String(timestamp);
604
- }
544
+ }).orElse(String(timestamp));
605
545
  }
606
546
  function analyzeUserActivity(karmaRatio, isMod, accountAgeDays) {
607
- const insights = [];
608
- if (karmaRatio > 5) insights.push("Primarily a commenter, highly engaged in discussions");
609
- else if (karmaRatio < .2) insights.push("Content creator, focuses on sharing posts");
610
- else insights.push("Balanced participation in both posting and commenting");
611
- if (accountAgeDays < 30) insights.push("New user, still exploring Reddit");
612
- else if (accountAgeDays > 365 * 5) insights.push("Long-time Redditor with extensive platform experience");
613
- if (isMod) insights.push("Community leader who helps maintain subreddit quality");
614
- return insights.join("\n - ");
547
+ return [
548
+ ...karmaRatio > 5 ? ["Primarily a commenter, highly engaged in discussions"] : karmaRatio < .2 ? ["Content creator, focuses on sharing posts"] : ["Balanced participation in both posting and commenting"],
549
+ ...accountAgeDays < 30 ? ["New user, still exploring Reddit"] : accountAgeDays > 365 * 5 ? ["Long-time Redditor with extensive platform experience"] : [],
550
+ ...isMod ? ["Community leader who helps maintain subreddit quality"] : []
551
+ ].join("\n - ");
615
552
  }
616
553
  function analyzePostEngagement(score, ratio, numComments) {
617
- const insights = [];
618
- if (score > 1e3 && ratio > .95) insights.push("Highly successful post with strong community approval");
619
- else if (score > 100 && ratio > .8) insights.push("Well-received post with good engagement");
620
- else if (ratio < .5) insights.push("Controversial post that sparked debate");
621
- if (numComments > 100) insights.push("Generated significant discussion");
622
- else if (numComments > score * .5) insights.push("Highly discussable content with active comment section");
623
- else if (numComments === 0) insights.push("Yet to receive community interaction");
624
- return insights.join("\n - ");
554
+ return [...score > 1e3 && ratio > .95 ? ["Highly successful post with strong community approval"] : score > 100 && ratio > .8 ? ["Well-received post with good engagement"] : ratio < .5 ? ["Controversial post that sparked debate"] : [], ...numComments > 100 ? ["Generated significant discussion"] : numComments > score * .5 ? ["Highly discussable content with active comment section"] : numComments === 0 ? ["Yet to receive community interaction"] : []].join("\n - ");
625
555
  }
626
556
  function analyzeSubredditHealth(subscribers, activeUsers, ageDays) {
627
- const insights = [];
628
- if (subscribers > 1e6) insights.push("Major subreddit with massive following");
629
- else if (subscribers > 1e5) insights.push("Well-established community");
630
- else if (subscribers < 1e3) insights.push("Niche community, potential for growth");
631
- if (activeUsers) {
632
- const activityRatio = activeUsers / subscribers;
633
- if (activityRatio > .1) insights.push("Highly active community with strong engagement");
634
- else if (activityRatio < .01) insights.push("Could benefit from more community engagement initiatives");
635
- }
636
- if (ageDays > 365 * 5) insights.push("Mature subreddit with established culture");
637
- else if (ageDays < 90) insights.push("New subreddit still forming its community");
638
- return insights.join("\n - ");
557
+ const sizeInsights = subscribers > 1e6 ? ["Major subreddit with massive following"] : subscribers > 1e5 ? ["Well-established community"] : subscribers < 1e3 ? ["Niche community, potential for growth"] : [];
558
+ const activityInsights = Option(activeUsers).map((active) => active / subscribers).fold(() => [], (activityRatio) => activityRatio > .1 ? ["Highly active community with strong engagement"] : activityRatio < .01 ? ["Could benefit from more community engagement initiatives"] : []);
559
+ const ageInsights = ageDays > 365 * 5 ? ["Mature subreddit with established culture"] : ageDays < 90 ? ["New subreddit still forming its community"] : [];
560
+ return [
561
+ ...sizeInsights,
562
+ ...activityInsights,
563
+ ...ageInsights
564
+ ].join("\n - ");
639
565
  }
640
566
  function getUserRecommendations(karmaRatio, isMod, accountAgeDays) {
641
- const recommendations = [];
642
- if (karmaRatio > 5) recommendations.push("Consider creating more posts to share your expertise");
643
- else if (karmaRatio < .2) recommendations.push("Engage more in discussions to build community connections");
644
- if (accountAgeDays < 30) {
645
- recommendations.push("Explore popular subreddits in your areas of interest");
646
- recommendations.push("Read community guidelines before posting");
647
- }
648
- if (isMod) recommendations.push("Share moderation insights with other community leaders");
649
- if (!recommendations.length) recommendations.push("Maintain your balanced engagement across Reddit");
650
- return recommendations.join("\n - ");
567
+ const recommendations = [
568
+ ...karmaRatio > 5 ? ["Consider creating more posts to share your expertise"] : karmaRatio < .2 ? ["Engage more in discussions to build community connections"] : [],
569
+ ...accountAgeDays < 30 ? ["Explore popular subreddits in your areas of interest", "Read community guidelines before posting"] : [],
570
+ ...isMod ? ["Share moderation insights with other community leaders"] : []
571
+ ];
572
+ return recommendations.length > 0 ? recommendations.join("\n - ") : "Maintain your balanced engagement across Reddit";
651
573
  }
652
574
  function getBestEngagementTime(createdUtc) {
653
575
  const postHour = (/* @__PURE__ */ new Date(createdUtc * 1e3)).getHours();
@@ -656,26 +578,19 @@ function getBestEngagementTime(createdUtc) {
656
578
  else return "Posted during moderate activity hours, timing could be optimized";
657
579
  }
658
580
  function getSubredditEngagementTips(subreddit) {
659
- const tips = [];
660
- if (subreddit.subscribers > 1e6) {
661
- tips.push("Post during peak hours for maximum visibility");
662
- tips.push("Ensure content is highly polished due to high competition");
663
- } else if (subreddit.subscribers < 1e3) {
664
- tips.push("Engage actively to help grow the community");
665
- tips.push("Consider cross-posting to related larger subreddits");
666
- }
667
- if (subreddit.activeUserCount) {
668
- if (subreddit.activeUserCount / subreddit.subscribers > .1) tips.push("Quick responses recommended due to high activity");
669
- }
670
- return tips.length ? tips.join("\n - ") : "Regular engagement recommended to maintain community presence";
581
+ const sizeTips = subreddit.subscribers > 1e6 ? ["Post during peak hours for maximum visibility", "Ensure content is highly polished due to high competition"] : subreddit.subscribers < 1e3 ? ["Engage actively to help grow the community", "Consider cross-posting to related larger subreddits"] : [];
582
+ const activityTips = Option(subreddit.activeUserCount).map((active) => active / subreddit.subscribers).fold(() => [], (activityRatio) => activityRatio > .1 ? ["Quick responses recommended due to high activity"] : []);
583
+ const allTips = [...sizeTips, ...activityTips];
584
+ return allTips.length > 0 ? allTips.join("\n - ") : "Regular engagement recommended to maintain community presence";
671
585
  }
672
586
  function formatUserInfo(user) {
673
- const status = [];
674
- if (user.isMod) status.push("Moderator");
675
- if (user.isGold) status.push("Reddit Gold Member");
676
- if (user.isEmployee) status.push("Reddit Employee");
677
587
  const accountAgeDays = (Date.now() / 1e3 - user.createdUtc) / (24 * 3600);
678
- const karmaRatio = user.commentKarma / (user.linkKarma || 1);
588
+ const karmaRatio = user.commentKarma / (user.linkKarma === 0 ? 1 : user.linkKarma);
589
+ const status = [
590
+ ...user.isMod ? ["Moderator"] : [],
591
+ ...user.isGold ? ["Reddit Gold Member"] : [],
592
+ ...user.isEmployee ? ["Reddit Employee"] : []
593
+ ];
679
594
  return {
680
595
  username: user.name,
681
596
  karma: {
@@ -683,7 +598,7 @@ function formatUserInfo(user) {
683
598
  postKarma: user.linkKarma,
684
599
  totalKarma: user.totalKarma
685
600
  },
686
- accountStatus: status.length ? status : ["Regular User"],
601
+ accountStatus: status.length > 0 ? status : ["Regular User"],
687
602
  accountCreated: formatTimestamp(user.createdUtc),
688
603
  profileUrl: user.profileUrl,
689
604
  activityAnalysis: analyzeUserActivity(karmaRatio, user.isMod, accountAgeDays),
@@ -692,11 +607,12 @@ function formatUserInfo(user) {
692
607
  }
693
608
  function formatPostInfo(post) {
694
609
  const contentType = post.isSelf ? "Text Post" : "Link Post";
695
- const content = post.isSelf ? post.selftext || "" : post.url || "";
696
- const flags = [];
697
- if (post.over18) flags.push("NSFW");
698
- if (post.spoiler) flags.push("Spoiler");
699
- if (post.edited) flags.push("Edited");
610
+ const content = post.isSelf ? post.selftext ?? "" : post.url ?? "";
611
+ const flags = [
612
+ ...post.over18 ? ["NSFW"] : [],
613
+ ...post.spoiler === true ? ["Spoiler"] : [],
614
+ ...post.edited ? ["Edited"] : []
615
+ ];
700
616
  return {
701
617
  title: post.title,
702
618
  type: contentType,
@@ -711,7 +627,7 @@ function formatPostInfo(post) {
711
627
  metadata: {
712
628
  posted: formatTimestamp(post.createdUtc),
713
629
  flags,
714
- flair: post.linkFlairText || "None"
630
+ flair: post.linkFlairText ?? "None"
715
631
  },
716
632
  links: {
717
633
  fullPost: `https://reddit.com${post.permalink}`,
@@ -722,16 +638,14 @@ function formatPostInfo(post) {
722
638
  };
723
639
  }
724
640
  function formatSubredditInfo(subreddit) {
725
- const flags = [];
726
- if (subreddit.over18) flags.push("NSFW");
727
- if (subreddit.subredditType) flags.push(`Type: ${subreddit.subredditType}`);
641
+ const flags = [...subreddit.over18 ? ["NSFW"] : [], ...subreddit.subredditType !== void 0 ? [`Type: ${subreddit.subredditType}`] : []];
728
642
  const ageDays = (Date.now() / 1e3 - subreddit.createdUtc) / (24 * 3600);
729
643
  return {
730
644
  name: subreddit.displayName,
731
645
  title: subreddit.title,
732
646
  stats: {
733
647
  subscribers: subreddit.subscribers,
734
- activeUsers: subreddit.activeUserCount !== void 0 ? subreddit.activeUserCount : "Unknown"
648
+ activeUsers: Option(subreddit.activeUserCount).fold(() => "Unknown", (count) => count)
735
649
  },
736
650
  description: {
737
651
  short: subreddit.publicDescription,
@@ -739,7 +653,7 @@ function formatSubredditInfo(subreddit) {
739
653
  },
740
654
  metadata: {
741
655
  created: formatTimestamp(subreddit.createdUtc),
742
- flags: flags.length ? flags : ["None"]
656
+ flags: flags.length > 0 ? flags : ["None"]
743
657
  },
744
658
  links: {
745
659
  subreddit: `https://reddit.com${subreddit.url}`,
@@ -753,21 +667,21 @@ function formatSubredditInfo(subreddit) {
753
667
  //#endregion
754
668
  //#region src/index.ts
755
669
  dotenv.config();
756
- const VERSION = "1.3.2";
670
+ const VERSION = "1.4.1";
757
671
  function validateUserAgent(userAgent, username) {
758
672
  if (!/^[\w-]+:[\w-]+:[\d.]+ \(by \/u\/\w+\)$/.test(userAgent)) {
759
673
  console.error("[Warning] User-Agent does not follow Reddit's recommended format");
760
674
  console.error("[Warning] Recommended: 'platform:app_id:version (by /u/username)'");
761
675
  console.error("[Warning] Non-standard User-Agents may increase ban risk");
762
- if (username) console.error(`[Warning] Consider using: 'typescript:reddit-mcp-server:${VERSION} (by /u/${username})'`);
676
+ if (username !== void 0) console.error(`[Warning] Consider using: 'typescript:reddit-mcp-server:${VERSION} (by /u/${username})'`);
763
677
  }
764
678
  }
765
679
  function buildUserAgent(customAgent, username) {
766
- if (customAgent) {
680
+ if (customAgent !== void 0) {
767
681
  validateUserAgent(customAgent, username);
768
682
  return customAgent;
769
683
  }
770
- if (username) {
684
+ if (username !== void 0) {
771
685
  const autoAgent = `typescript:reddit-mcp-server:${VERSION} (by /u/${username})`;
772
686
  console.error(`[Setup] Auto-generated User-Agent: ${autoAgent}`);
773
687
  return autoAgent;
@@ -801,14 +715,17 @@ function buildSafeModeConfig(safeMode) {
801
715
  };
802
716
  }
803
717
  }
718
+ function unwrapClient() {
719
+ return getRedditClient().orThrow(/* @__PURE__ */ new Error("Reddit client not initialized"));
720
+ }
804
721
  async function setupRedditClient() {
805
722
  const clientId = process.env.REDDIT_CLIENT_ID;
806
723
  const clientSecret = process.env.REDDIT_CLIENT_SECRET;
807
724
  const customUserAgent = process.env.REDDIT_USER_AGENT;
808
725
  const username = process.env.REDDIT_USERNAME;
809
726
  const password = process.env.REDDIT_PASSWORD;
810
- const authMode = process.env.REDDIT_AUTH_MODE || "auto";
811
- const safeMode = process.env.REDDIT_SAFE_MODE || "off";
727
+ const authMode = process.env.REDDIT_AUTH_MODE ?? "auto";
728
+ const safeMode = process.env.REDDIT_SAFE_MODE ?? "standard";
812
729
  if (![
813
730
  "auto",
814
731
  "authenticated",
@@ -827,61 +744,63 @@ async function setupRedditClient() {
827
744
  console.error("[Error] Valid options are: off, standard, strict");
828
745
  process.exit(1);
829
746
  }
830
- if (authMode === "authenticated" && (!clientId || !clientSecret)) {
747
+ if (authMode === "authenticated" && (clientId === void 0 || clientSecret === void 0)) {
831
748
  console.error("[Error] Authenticated mode requires REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET");
832
749
  process.exit(1);
833
750
  }
834
- const hasCredentials = !!(clientId && clientSecret);
751
+ const hasCredentials = Boolean(clientId && clientSecret);
835
752
  const userAgent = buildUserAgent(customUserAgent, username);
836
753
  const safeModeConfig = buildSafeModeConfig(safeMode);
837
- try {
838
- const client = initializeRedditClient({
839
- clientId: clientId || "",
840
- clientSecret: clientSecret || "",
841
- userAgent,
842
- username,
843
- password,
844
- authMode,
845
- safeMode: safeModeConfig
846
- });
847
- console.error("[Setup] Reddit client initialized");
848
- console.error(`[Setup] Authentication mode: ${authMode}`);
849
- if (authMode === "anonymous" || !hasCredentials) {
850
- console.error("[Setup] Using anonymous Reddit API (~10 req/min)");
851
- console.error("[Setup] No authentication required - ready to use!");
852
- } else {
853
- console.error("[Setup] Testing Reddit API connection...");
854
- if (!await client.checkAuthentication()) {
855
- console.error("[Error] Failed to connect to Reddit API");
856
- console.error("[Error] Please check your REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET");
857
- process.exit(1);
858
- }
859
- console.error("[Setup] ✓ Reddit API connection successful");
860
- console.error("[Setup] Using OAuth Reddit API (60-100 req/min)");
861
- }
862
- if (username && password) {
863
- console.error(`[Setup] ✓ User authenticated as: ${username}`);
864
- console.error("[Setup] Write operations enabled (posting, replying, editing, deleting)");
865
- } else {
866
- console.error("[Setup] Read-only mode (no user credentials)");
867
- console.error("[Setup] For write operations, set REDDIT_USERNAME and REDDIT_PASSWORD");
868
- }
869
- if (safeModeConfig.enabled) {
870
- console.error(`[Setup] ✓ Safe mode enabled: ${safeModeConfig.mode}`);
871
- console.error(`[Setup] - Write delay: ${safeModeConfig.writeDelayMs}ms between operations`);
872
- console.error(`[Setup] - Duplicate detection: enabled (tracking last ${safeModeConfig.maxRecentHashes} items)`);
873
- } else {
874
- console.error("[Setup] Safe mode: off (no write operation safeguards)");
875
- console.error("[Setup] Consider enabling REDDIT_SAFE_MODE=standard for spam protection");
754
+ const botDisclosureMode = process.env.REDDIT_BOT_DISCLOSURE ?? "off";
755
+ const botDisclosureConfig = {
756
+ enabled: botDisclosureMode === "auto",
757
+ footer: botDisclosureMode === "auto" ? process.env.REDDIT_BOT_FOOTER ?? "\n\n---\n^(🤖 I am a bot | Built with) [^reddit-mcp-server](https://github.com/jordanburke/reddit-mcp-server)" : ""
758
+ };
759
+ const client = initializeRedditClient({
760
+ clientId: clientId ?? "",
761
+ clientSecret: clientSecret ?? "",
762
+ userAgent,
763
+ username,
764
+ password,
765
+ authMode,
766
+ safeMode: safeModeConfig,
767
+ botDisclosure: botDisclosureConfig
768
+ });
769
+ console.error("[Setup] Reddit client initialized");
770
+ console.error(`[Setup] Authentication mode: ${authMode}`);
771
+ if (authMode === "anonymous" || !hasCredentials) {
772
+ console.error("[Setup] Using anonymous Reddit API (~10 req/min)");
773
+ console.error("[Setup] No authentication required - ready to use!");
774
+ } else {
775
+ console.error("[Setup] Testing Reddit API connection...");
776
+ if (!await client.checkAuthentication()) {
777
+ console.error("[Error] Failed to connect to Reddit API");
778
+ console.error("[Error] Please check your REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET");
779
+ process.exit(1);
876
780
  }
877
- } catch (error) {
878
- console.error("[Error] Reddit API connection failed:", error instanceof Error ? error.message : error);
879
- console.error("[Error] Please verify your Reddit API credentials");
880
- process.exit(1);
781
+ console.error("[Setup] Reddit API connection successful");
782
+ console.error("[Setup] Using OAuth Reddit API (60-100 req/min)");
783
+ }
784
+ if (username !== void 0 && password !== void 0) {
785
+ console.error(`[Setup] ✓ User authenticated as: ${username}`);
786
+ console.error("[Setup] Write operations enabled (posting, replying, editing, deleting)");
787
+ } else {
788
+ console.error("[Setup] Read-only mode (no user credentials)");
789
+ console.error("[Setup] For write operations, set REDDIT_USERNAME and REDDIT_PASSWORD");
790
+ }
791
+ if (safeModeConfig.enabled) {
792
+ console.error(`[Setup] ✓ Safe mode enabled: ${safeModeConfig.mode}`);
793
+ console.error(`[Setup] - Write delay: ${safeModeConfig.writeDelayMs}ms between operations`);
794
+ console.error(`[Setup] - Duplicate detection: enabled (tracking last ${safeModeConfig.maxRecentHashes} items)`);
795
+ } else console.error("[Setup] Safe mode: off (explicitly disabled — ensure compliance with Reddit's Responsible Builder Policy)");
796
+ if (botDisclosureConfig.enabled) console.error("[Setup] ✓ Bot disclosure: enabled (automated content will include bot footer)");
797
+ else {
798
+ console.error("[Setup] Bot disclosure: off");
799
+ console.error("[Setup] For Reddit policy compliance, consider REDDIT_BOT_DISCLOSURE=auto");
881
800
  }
882
801
  }
883
- const oauthToken = process.env.OAUTH_TOKEN || crypto.randomBytes(32).toString("hex");
884
- if (process.env.OAUTH_ENABLED === "true" && !process.env.OAUTH_TOKEN) console.error(`[Auth] Generated OAuth token: ${oauthToken}`);
802
+ const oauthToken = process.env.OAUTH_TOKEN ?? crypto.randomBytes(32).toString("hex");
803
+ if (process.env.OAUTH_ENABLED === "true" && process.env.OAUTH_TOKEN === void 0) console.error(`[Auth] Generated OAuth token: ${oauthToken}`);
885
804
  const server = new FastMCP({
886
805
  name: "reddit-mcp-server",
887
806
  version: VERSION,
@@ -896,8 +815,18 @@ Available capabilities:
896
815
  - Delete your own posts and comments (with authentication)
897
816
  - Analyze engagement metrics and community insights
898
817
 
899
- For write operations (posting, replying, editing, deleting), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured.`,
900
- ...process.env.OAUTH_ENABLED === "true" && { authenticate: async (request) => {
818
+ For write operations (posting, replying, editing, deleting), ensure REDDIT_USERNAME and REDDIT_PASSWORD are configured.
819
+
820
+ IMPORTANT - Reddit Responsible Builder Policy compliance:
821
+ - Data retrieved via these tools must NOT be used for AI model training without Reddit's written approval
822
+ - Data must NOT be sold, licensed, or commercially redistributed
823
+ - Do NOT attempt to de-anonymize or re-identify Reddit users
824
+ - Do NOT post identical or substantially similar content across multiple subreddits
825
+ - Do NOT use these tools to manipulate votes, karma, or circumvent Reddit safety mechanisms
826
+ - All bot-generated content must clearly disclose its automated nature
827
+ - Bots must NOT send private/direct messages without explicit user consent
828
+ For details: https://support.reddithelp.com/hc/en-us/articles/42728983564564-Responsible-Builder-Policy`,
829
+ ...process.env.OAUTH_ENABLED === "true" && { authenticate: (request) => {
901
830
  const authHeader = request.headers.authorization;
902
831
  if (!(authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith("Bearer "))) throw new Response(null, {
903
832
  status: 401,
@@ -912,24 +841,24 @@ For write operations (posting, replying, editing, deleting), ensure REDDIT_USERN
912
841
  status: 403,
913
842
  statusText: "Invalid token"
914
843
  });
915
- return { authenticated: true };
844
+ return Promise.resolve({ authenticated: true });
916
845
  } }
917
846
  });
918
847
  server.addTool({
919
848
  name: "test_reddit_mcp_server",
920
849
  description: "Test the Reddit MCP Server connection and configuration",
921
850
  parameters: z.object({}),
922
- execute: async () => {
851
+ execute: () => {
923
852
  const client = getRedditClient();
924
- const hasAuth = client ? "" : "";
925
- const hasWriteAccess = process.env.REDDIT_USERNAME && process.env.REDDIT_PASSWORD ? "✓" : "✗";
926
- return `Reddit MCP Server Status:
853
+ const hasAuth = client.fold(() => "", () => "");
854
+ const hasWriteAccess = process.env.REDDIT_USERNAME !== void 0 && process.env.REDDIT_PASSWORD !== void 0 ? "✓" : "✗";
855
+ return Promise.resolve(`Reddit MCP Server Status:
927
856
  - Server: ✓ Running
928
- - Reddit Client: ${hasAuth} ${client ? "Initialized" : "Not initialized"}
857
+ - Reddit Client: ${hasAuth} ${client.fold(() => "Not initialized", () => "Initialized")}
929
858
  - Write Access: ${hasWriteAccess} ${hasWriteAccess === "✓" ? "Available" : "Read-only mode"}
930
859
  - Version: ${VERSION}
931
860
 
932
- Ready to handle Reddit API requests!`;
861
+ Ready to handle Reddit API requests!`);
933
862
  }
934
863
  });
935
864
  server.addTool({
@@ -937,10 +866,11 @@ server.addTool({
937
866
  description: "Get detailed information about a Reddit user including karma, account status, and activity analysis",
938
867
  parameters: z.object({ username: z.string().describe("The Reddit username (without u/ prefix)") }),
939
868
  execute: async (args) => {
940
- const client = getRedditClient();
941
- if (!client) throw new Error("Reddit client not initialized");
942
- const formattedUser = formatUserInfo(await client.getUser(args.username));
943
- return `# User Information: u/${formattedUser.username}
869
+ return (await unwrapClient().getUser(args.username)).fold((err) => {
870
+ throw new Error(`Failed to get user info: ${err.message}`);
871
+ }, (user) => {
872
+ const formattedUser = formatUserInfo(user);
873
+ return `# User Information: u/${formattedUser.username}
944
874
 
945
875
  ## Profile Overview
946
876
  - Username: u/${formattedUser.username}
@@ -957,6 +887,7 @@ server.addTool({
957
887
 
958
888
  ## Recommendations
959
889
  - ${formattedUser.recommendations.replace(/\n {2}- /g, "\n- ")}`;
890
+ });
960
891
  }
961
892
  });
962
893
  server.addTool({
@@ -980,26 +911,27 @@ server.addTool({
980
911
  limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
981
912
  }),
982
913
  execute: async (args) => {
983
- const client = getRedditClient();
984
- if (!client) throw new Error("Reddit client not initialized");
985
- const posts = await client.getUserPosts(args.username, {
914
+ return (await unwrapClient().getUserPosts(args.username, {
986
915
  sort: args.sort,
987
916
  timeFilter: args.time_filter,
988
917
  limit: args.limit
989
- });
990
- if (posts.length === 0) return `No posts found for u/${args.username} with the specified filters.`;
991
- const postSummaries = posts.map((post, index) => {
992
- const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler ? ["**Spoiler**"] : []];
993
- return `### ${index + 1}. ${post.title} ${flags.join(" ")}
918
+ })).fold((err) => {
919
+ throw new Error(`Failed to get user posts: ${err.message}`);
920
+ }, (posts) => {
921
+ if (posts.length === 0) return `No posts found for u/${args.username} with the specified filters.`;
922
+ const postSummaries = posts.map((post, index) => {
923
+ const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler === true ? ["**Spoiler**"] : []];
924
+ return `### ${index + 1}. ${post.title} ${flags.join(" ")}
994
925
  - Subreddit: r/${post.subreddit}
995
926
  - Score: ${post.score.toLocaleString()} (${(post.upvoteRatio * 100).toFixed(1)}% upvoted)
996
927
  - Comments: ${post.numComments.toLocaleString()}
997
928
  - Posted: ${(/* @__PURE__ */ new Date(post.createdUtc * 1e3)).toLocaleString()}
998
929
  - Link: https://reddit.com${post.permalink}`;
999
- }).join("\n\n");
1000
- return `# Posts by u/${args.username} (${args.sort} - ${args.time_filter})
930
+ }).join("\n\n");
931
+ return `# Posts by u/${args.username} (${args.sort} - ${args.time_filter})
1001
932
 
1002
933
  ${postSummaries}`;
934
+ });
1003
935
  }
1004
936
  });
1005
937
  server.addTool({
@@ -1023,18 +955,18 @@ server.addTool({
1023
955
  limit: z.number().min(1).max(100).default(10).describe("Number of comments to retrieve")
1024
956
  }),
1025
957
  execute: async (args) => {
1026
- const client = getRedditClient();
1027
- if (!client) throw new Error("Reddit client not initialized");
1028
- const comments = await client.getUserComments(args.username, {
958
+ return (await unwrapClient().getUserComments(args.username, {
1029
959
  sort: args.sort,
1030
960
  timeFilter: args.time_filter,
1031
961
  limit: args.limit
1032
- });
1033
- if (comments.length === 0) return `No comments found for u/${args.username} with the specified filters.`;
1034
- const commentSummaries = comments.map((comment, index) => {
1035
- const truncatedBody = comment.body.length > 300 ? `${comment.body.substring(0, 300)}...` : comment.body;
1036
- const flags = [...comment.edited ? ["*(edited)*"] : [], ...comment.isSubmitter ? ["**OP**"] : []];
1037
- return `### ${index + 1}. Comment ${flags.join(" ")}
962
+ })).fold((err) => {
963
+ throw new Error(`Failed to get user comments: ${err.message}`);
964
+ }, (comments) => {
965
+ if (comments.length === 0) return `No comments found for u/${args.username} with the specified filters.`;
966
+ const commentSummaries = comments.map((comment, index) => {
967
+ const truncatedBody = comment.body.length > 300 ? `${comment.body.substring(0, 300)}...` : comment.body;
968
+ const flags = [...comment.edited ? ["*(edited)*"] : [], ...comment.isSubmitter ? ["**OP**"] : []];
969
+ return `### ${index + 1}. Comment ${flags.join(" ")}
1038
970
  In r/${comment.subreddit} on "${comment.submissionTitle}"
1039
971
 
1040
972
  > ${truncatedBody}
@@ -1042,10 +974,11 @@ In r/${comment.subreddit} on "${comment.submissionTitle}"
1042
974
  - Score: ${comment.score.toLocaleString()}
1043
975
  - Posted: ${(/* @__PURE__ */ new Date(comment.createdUtc * 1e3)).toLocaleString()}
1044
976
  - Link: https://reddit.com${comment.permalink}`;
1045
- }).join("\n\n");
1046
- return `# Comments by u/${args.username} (${args.sort} - ${args.time_filter})
977
+ }).join("\n\n");
978
+ return `# Comments by u/${args.username} (${args.sort} - ${args.time_filter})
1047
979
 
1048
980
  ${commentSummaries}`;
981
+ });
1049
982
  }
1050
983
  });
1051
984
  server.addTool({
@@ -1056,10 +989,11 @@ server.addTool({
1056
989
  post_id: z.string().describe("The Reddit post ID")
1057
990
  }),
1058
991
  execute: async (args) => {
1059
- const client = getRedditClient();
1060
- if (!client) throw new Error("Reddit client not initialized");
1061
- const formattedPost = formatPostInfo(await client.getPost(args.post_id, args.subreddit));
1062
- return `# Post from r/${formattedPost.subreddit}
992
+ return (await unwrapClient().getPost(args.post_id, args.subreddit)).fold((err) => {
993
+ throw new Error(`Failed to get post: ${err.message}`);
994
+ }, (post) => {
995
+ const formattedPost = formatPostInfo(post);
996
+ return `# Post from r/${formattedPost.subreddit}
1063
997
 
1064
998
  ## Post Details
1065
999
  - Title: ${formattedPost.title}
@@ -1076,7 +1010,7 @@ ${formattedPost.content}
1076
1010
 
1077
1011
  ## Metadata
1078
1012
  - Posted: ${formattedPost.metadata.posted}
1079
- - Flags: ${formattedPost.metadata.flags.length ? formattedPost.metadata.flags.join(", ") : "None"}
1013
+ - Flags: ${formattedPost.metadata.flags.length > 0 ? formattedPost.metadata.flags.join(", ") : "None"}
1080
1014
  - Flair: ${formattedPost.metadata.flair}
1081
1015
 
1082
1016
  ## Links
@@ -1088,6 +1022,7 @@ ${formattedPost.content}
1088
1022
 
1089
1023
  ## Best Time to Engage
1090
1024
  ${formattedPost.bestTimeToEngage}`;
1025
+ });
1091
1026
  }
1092
1027
  });
1093
1028
  server.addTool({
@@ -1106,19 +1041,20 @@ server.addTool({
1106
1041
  limit: z.number().min(1).max(100).default(10).describe("Number of posts to retrieve")
1107
1042
  }),
1108
1043
  execute: async (args) => {
1109
- const client = getRedditClient();
1110
- if (!client) throw new Error("Reddit client not initialized");
1111
- const posts = await client.getTopPosts(args.subreddit || "", args.time_filter, args.limit);
1112
- if (posts.length === 0) return `No posts found in ${args.subreddit ? `r/${args.subreddit}` : "home feed"} for the specified time period.`;
1113
- const postSummaries = posts.map(formatPostInfo).map((post, index) => `### ${index + 1}. ${post.title}
1044
+ return (await unwrapClient().getTopPosts(args.subreddit ?? "", args.time_filter, args.limit)).fold((err) => {
1045
+ throw new Error(`Failed to get top posts: ${err.message}`);
1046
+ }, (posts) => {
1047
+ if (posts.length === 0) return `No posts found in ${args.subreddit !== void 0 ? `r/${args.subreddit}` : "home feed"} for the specified time period.`;
1048
+ const postSummaries = posts.map(formatPostInfo).map((post, index) => `### ${index + 1}. ${post.title}
1114
1049
  - Author: u/${post.author}
1115
1050
  - Score: ${post.stats.score.toLocaleString()} (${(post.stats.upvoteRatio * 100).toFixed(1)}% upvoted)
1116
1051
  - Comments: ${post.stats.comments.toLocaleString()}
1117
1052
  - Posted: ${post.metadata.posted}
1118
1053
  - Link: ${post.links.shortLink}`).join("\n\n");
1119
- return `# Top Posts from ${args.subreddit ? `r/${args.subreddit}` : "Home Feed"} (${args.time_filter})
1054
+ return `# Top Posts from ${args.subreddit !== void 0 ? `r/${args.subreddit}` : "Home Feed"} (${args.time_filter})
1120
1055
 
1121
1056
  ${postSummaries}`;
1057
+ });
1122
1058
  }
1123
1059
  });
1124
1060
  server.addTool({
@@ -1126,10 +1062,11 @@ server.addTool({
1126
1062
  description: "Get detailed information about a subreddit including description, stats, and community analysis",
1127
1063
  parameters: z.object({ subreddit_name: z.string().describe("The subreddit name (without r/ prefix)") }),
1128
1064
  execute: async (args) => {
1129
- const client = getRedditClient();
1130
- if (!client) throw new Error("Reddit client not initialized");
1131
- const formattedSubreddit = formatSubredditInfo(await client.getSubredditInfo(args.subreddit_name));
1132
- return `# Subreddit Information: r/${formattedSubreddit.name}
1065
+ return (await unwrapClient().getSubredditInfo(args.subreddit_name)).fold((err) => {
1066
+ throw new Error(`Failed to get subreddit info: ${err.message}`);
1067
+ }, (subreddit) => {
1068
+ const formattedSubreddit = formatSubredditInfo(subreddit);
1069
+ return `# Subreddit Information: r/${formattedSubreddit.name}
1133
1070
 
1134
1071
  ## Overview
1135
1072
  - Name: r/${formattedSubreddit.name}
@@ -1156,6 +1093,7 @@ ${formattedSubreddit.description.full}
1156
1093
 
1157
1094
  ## Engagement Tips
1158
1095
  - ${formattedSubreddit.engagementTips.replace(/\n {2}- /g, "\n- ")}`;
1096
+ });
1159
1097
  }
1160
1098
  });
1161
1099
  server.addTool({
@@ -1163,11 +1101,11 @@ server.addTool({
1163
1101
  description: "Get a list of currently trending subreddits",
1164
1102
  parameters: z.object({}),
1165
1103
  execute: async () => {
1166
- const client = getRedditClient();
1167
- if (!client) throw new Error("Reddit client not initialized");
1168
- return `# Trending Subreddits
1104
+ return (await unwrapClient().getTrendingSubreddits()).fold((err) => {
1105
+ throw new Error(`Failed to get trending subreddits: ${err.message}`);
1106
+ }, (trendingSubreddits) => `# Trending Subreddits
1169
1107
 
1170
- ${(await client.getTrendingSubreddits()).map((subreddit, index) => `${index + 1}. r/${subreddit}`).join("\n")}`;
1108
+ ${trendingSubreddits.map((subreddit, index) => `${index + 1}. r/${subreddit}`).join("\n")}`);
1171
1109
  }
1172
1110
  });
1173
1111
  server.addTool({
@@ -1199,36 +1137,38 @@ server.addTool({
1199
1137
  ]).default("link").describe("Type of content to search")
1200
1138
  }),
1201
1139
  execute: async (args) => {
1202
- const client = getRedditClient();
1203
- if (!client) throw new Error("Reddit client not initialized");
1204
- if (!args.query || args.query.trim() === "") throw new Error("Search query cannot be empty");
1205
- const posts = await client.searchReddit(args.query, {
1140
+ const client = unwrapClient();
1141
+ if (args.query.trim() === "") throw new Error("Search query cannot be empty");
1142
+ return (await client.searchReddit(args.query, {
1206
1143
  subreddit: args.subreddit,
1207
1144
  sort: args.sort,
1208
1145
  timeFilter: args.time_filter,
1209
1146
  limit: args.limit,
1210
1147
  type: args.type
1211
- });
1212
- if (posts.length === 0) {
1213
- const searchLocation = args.subreddit ? ` in r/${args.subreddit}` : "";
1214
- return `No results found for "${args.query}"${searchLocation}.`;
1215
- }
1216
- const searchResults = posts.map((post, index) => {
1217
- const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler ? ["**Spoiler**"] : []];
1218
- return `### ${index + 1}. ${post.title} ${flags.join(" ")}
1148
+ })).fold((err) => {
1149
+ throw new Error(`Failed to search: ${err.message}`);
1150
+ }, (posts) => {
1151
+ if (posts.length === 0) {
1152
+ const searchLocation = args.subreddit !== void 0 ? ` in r/${args.subreddit}` : "";
1153
+ return `No results found for "${args.query}"${searchLocation}.`;
1154
+ }
1155
+ const searchResults = posts.map((post, index) => {
1156
+ const flags = [...post.over18 ? ["**NSFW**"] : [], ...post.spoiler === true ? ["**Spoiler**"] : []];
1157
+ return `### ${index + 1}. ${post.title} ${flags.join(" ")}
1219
1158
  - Subreddit: r/${post.subreddit}
1220
1159
  - Author: u/${post.author}
1221
1160
  - Score: ${post.score.toLocaleString()} (${(post.upvoteRatio * 100).toFixed(1)}% upvoted)
1222
1161
  - Comments: ${post.numComments.toLocaleString()}
1223
1162
  - Posted: ${(/* @__PURE__ */ new Date(post.createdUtc * 1e3)).toLocaleString()}
1224
1163
  - Link: https://reddit.com${post.permalink}`;
1225
- }).join("\n\n");
1226
- const searchLocation = args.subreddit ? ` in r/${args.subreddit}` : "";
1227
- return `# Reddit Search Results for: "${args.query}"${searchLocation}
1164
+ }).join("\n\n");
1165
+ const searchLocation = args.subreddit !== void 0 ? ` in r/${args.subreddit}` : "";
1166
+ return `# Reddit Search Results for: "${args.query}"${searchLocation}
1228
1167
 
1229
1168
  Sorted by: ${args.sort} | Time: ${args.time_filter} | Type: ${args.type}
1230
1169
 
1231
1170
  ${searchResults}`;
1171
+ });
1232
1172
  }
1233
1173
  });
1234
1174
  server.addTool({
@@ -1241,11 +1181,13 @@ server.addTool({
1241
1181
  is_self: z.boolean().default(true).describe("Whether this is a self post (text) or link post")
1242
1182
  }),
1243
1183
  execute: async (args) => {
1244
- const client = getRedditClient();
1245
- if (!client) throw new Error("Reddit client not initialized");
1246
- if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1247
- const formattedPost = formatPostInfo(await client.createPost(args.subreddit, args.title, args.content, args.is_self));
1248
- return `# Post Created Successfully
1184
+ const client = unwrapClient();
1185
+ if (process.env.REDDIT_USERNAME === void 0 || process.env.REDDIT_PASSWORD === void 0) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1186
+ return (await client.createPost(args.subreddit, args.title, args.content, args.is_self)).fold((err) => {
1187
+ throw new Error(`Failed to create post: ${err.message}`);
1188
+ }, (post) => {
1189
+ const formattedPost = formatPostInfo(post);
1190
+ return `# Post Created Successfully
1249
1191
 
1250
1192
  ## Post Details
1251
1193
  - Title: ${formattedPost.title}
@@ -1254,6 +1196,7 @@ server.addTool({
1254
1196
  - Link: ${formattedPost.links.fullPost}
1255
1197
 
1256
1198
  Your post has been successfully submitted to r/${formattedPost.subreddit}.`;
1199
+ });
1257
1200
  }
1258
1201
  });
1259
1202
  server.addTool({
@@ -1264,18 +1207,18 @@ server.addTool({
1264
1207
  content: z.string().describe("The reply content")
1265
1208
  }),
1266
1209
  execute: async (args) => {
1267
- const client = getRedditClient();
1268
- if (!client) throw new Error("Reddit client not initialized");
1269
- if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1270
- const comment = await client.replyToPost(args.post_id, args.content);
1271
- return `# Reply Posted Successfully
1210
+ const client = unwrapClient();
1211
+ if (process.env.REDDIT_USERNAME === void 0 || process.env.REDDIT_PASSWORD === void 0) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1212
+ return (await client.replyToPost(args.post_id, args.content)).fold((err) => {
1213
+ throw new Error(`Failed to reply: ${err.message}`);
1214
+ }, (comment) => `# Reply Posted Successfully
1272
1215
 
1273
1216
  ## Comment Details
1274
1217
  - Posted to: ${args.post_id}
1275
1218
  - Author: u/${process.env.REDDIT_USERNAME}
1276
1219
  - Comment ID: ${comment.id}
1277
1220
 
1278
- Your reply has been successfully posted.`;
1221
+ Your reply has been successfully posted.`);
1279
1222
  }
1280
1223
  });
1281
1224
  server.addTool({
@@ -1283,15 +1226,15 @@ server.addTool({
1283
1226
  description: "Delete your own Reddit post (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
1284
1227
  parameters: z.object({ thing_id: z.string().describe("The full Reddit thing ID (e.g., 't3_abc123' for posts) or just the post ID (e.g., 'abc123'). The 't3_' prefix will be added automatically if missing.") }),
1285
1228
  execute: async (args) => {
1286
- const client = getRedditClient();
1287
- if (!client) throw new Error("Reddit client not initialized");
1288
- if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1289
- await client.deletePost(args.thing_id);
1290
- return `# Post Deleted Successfully
1229
+ const client = unwrapClient();
1230
+ if (process.env.REDDIT_USERNAME === void 0 || process.env.REDDIT_PASSWORD === void 0) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1231
+ return (await client.deletePost(args.thing_id)).fold((err) => {
1232
+ throw new Error(`Failed to delete post: ${err.message}`);
1233
+ }, () => `# Post Deleted Successfully
1291
1234
 
1292
1235
  The post ${args.thing_id} has been permanently deleted from Reddit.
1293
1236
 
1294
- **Note**: This action cannot be undone. The post content has been removed and cannot be recovered.`;
1237
+ **Note**: This action cannot be undone. The post content has been removed and cannot be recovered.`);
1295
1238
  }
1296
1239
  });
1297
1240
  server.addTool({
@@ -1299,15 +1242,15 @@ server.addTool({
1299
1242
  description: "Delete your own Reddit comment (requires REDDIT_USERNAME and REDDIT_PASSWORD). WARNING: This action is permanent and cannot be undone!",
1300
1243
  parameters: z.object({ thing_id: z.string().describe("The full Reddit thing ID (e.g., 't1_abc123' for comments) or just the comment ID (e.g., 'abc123'). The 't1_' prefix will be added automatically if missing.") }),
1301
1244
  execute: async (args) => {
1302
- const client = getRedditClient();
1303
- if (!client) throw new Error("Reddit client not initialized");
1304
- if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1305
- await client.deleteComment(args.thing_id);
1306
- return `# Comment Deleted Successfully
1245
+ const client = unwrapClient();
1246
+ if (process.env.REDDIT_USERNAME === void 0 || process.env.REDDIT_PASSWORD === void 0) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1247
+ return (await client.deleteComment(args.thing_id)).fold((err) => {
1248
+ throw new Error(`Failed to delete comment: ${err.message}`);
1249
+ }, () => `# Comment Deleted Successfully
1307
1250
 
1308
1251
  The comment ${args.thing_id} has been permanently deleted from Reddit.
1309
1252
 
1310
- **Note**: This action cannot be undone. The comment content has been removed and cannot be recovered.`;
1253
+ **Note**: This action cannot be undone. The comment content has been removed and cannot be recovered.`);
1311
1254
  }
1312
1255
  });
1313
1256
  server.addTool({
@@ -1318,11 +1261,11 @@ server.addTool({
1318
1261
  new_text: z.string().describe("The new text content for the post. Supports Reddit markdown formatting.")
1319
1262
  }),
1320
1263
  execute: async (args) => {
1321
- const client = getRedditClient();
1322
- if (!client) throw new Error("Reddit client not initialized");
1323
- if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1324
- await client.editPost(args.thing_id, args.new_text);
1325
- return `# Post Edited Successfully
1264
+ const client = unwrapClient();
1265
+ if (process.env.REDDIT_USERNAME === void 0 || process.env.REDDIT_PASSWORD === void 0) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1266
+ return (await client.editPost(args.thing_id, args.new_text)).fold((err) => {
1267
+ throw new Error(`Failed to edit post: ${err.message}`);
1268
+ }, () => `# Post Edited Successfully
1326
1269
 
1327
1270
  The post ${args.thing_id} has been updated with your new content.
1328
1271
 
@@ -1330,7 +1273,7 @@ The post ${args.thing_id} has been updated with your new content.
1330
1273
  - Only self (text) posts can be edited
1331
1274
  - Post titles cannot be edited
1332
1275
  - Link posts cannot be edited
1333
- - An "edited" marker will appear on your post`;
1276
+ - An "edited" marker will appear on your post`);
1334
1277
  }
1335
1278
  });
1336
1279
  server.addTool({
@@ -1341,15 +1284,15 @@ server.addTool({
1341
1284
  new_text: z.string().describe("The new text content for the comment. Supports Reddit markdown formatting.")
1342
1285
  }),
1343
1286
  execute: async (args) => {
1344
- const client = getRedditClient();
1345
- if (!client) throw new Error("Reddit client not initialized");
1346
- if (!process.env.REDDIT_USERNAME || !process.env.REDDIT_PASSWORD) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1347
- await client.editComment(args.thing_id, args.new_text);
1348
- return `# Comment Edited Successfully
1287
+ const client = unwrapClient();
1288
+ if (process.env.REDDIT_USERNAME === void 0 || process.env.REDDIT_PASSWORD === void 0) throw new Error("User authentication required. Please set REDDIT_USERNAME and REDDIT_PASSWORD environment variables.");
1289
+ return (await client.editComment(args.thing_id, args.new_text)).fold((err) => {
1290
+ throw new Error(`Failed to edit comment: ${err.message}`);
1291
+ }, () => `# Comment Edited Successfully
1349
1292
 
1350
1293
  The comment ${args.thing_id} has been updated with your new content.
1351
1294
 
1352
- **Note**: An "edited" marker will appear on your comment to show it has been modified.`;
1295
+ **Note**: An "edited" marker will appear on your comment to show it has been modified.`);
1353
1296
  }
1354
1297
  });
1355
1298
  server.addTool({
@@ -1369,16 +1312,15 @@ server.addTool({
1369
1312
  limit: z.number().min(1).max(500).default(100).describe("Maximum number of comments to retrieve")
1370
1313
  }),
1371
1314
  execute: async (args) => {
1372
- const client = getRedditClient();
1373
- if (!client) throw new Error("Reddit client not initialized");
1374
- if (!args.post_id || !args.subreddit) throw new Error("post_id and subreddit are required");
1375
- const data = await client.getPostComments(args.post_id, args.subreddit, {
1315
+ const client = unwrapClient();
1316
+ if (args.post_id === "" || args.subreddit === "") throw new Error("post_id and subreddit are required");
1317
+ return (await client.getPostComments(args.post_id, args.subreddit, {
1376
1318
  sort: args.sort,
1377
1319
  limit: args.limit
1378
- });
1379
- const { post } = data;
1380
- const { comments } = data;
1381
- let response = `# Comments for: ${post.title}
1320
+ })).fold((err) => {
1321
+ throw new Error(`Failed to get comments: ${err.message}`);
1322
+ }, ({ post, comments }) => {
1323
+ const header = `# Comments for: ${post.title}
1382
1324
 
1383
1325
  **Post by u/${post.author} in r/${post.subreddit}**
1384
1326
  - Score: ${post.score.toLocaleString()} | Comments: ${post.numComments.toLocaleString()}
@@ -1387,56 +1329,47 @@ server.addTool({
1387
1329
  ---
1388
1330
 
1389
1331
  `;
1390
- if (comments.length === 0) {
1391
- response += "No comments found for this post.";
1392
- return response;
1393
- }
1394
- const commentSummaries = comments.map((comment) => {
1395
- const indent = "└─".repeat(Math.min(comment.depth || 0, 3));
1396
- const authorBadge = comment.isSubmitter ? " **[OP]**" : "";
1397
- const editedBadge = comment.edited ? " *(edited)*" : "";
1398
- return `${indent} **u/${comment.author}**${authorBadge}${editedBadge} (${comment.score.toLocaleString()} points)
1332
+ if (comments.length === 0) return `${header}No comments found for this post.`;
1333
+ return header + comments.map((comment) => {
1334
+ const indent = "└─".repeat(Math.min(comment.depth ?? 0, 3));
1335
+ const authorBadge = comment.isSubmitter ? " **[OP]**" : "";
1336
+ const editedBadge = comment.edited ? " *(edited)*" : "";
1337
+ return `${indent} **u/${comment.author}**${authorBadge}${editedBadge} (${comment.score.toLocaleString()} points)
1399
1338
 
1400
1339
  ${comment.body}
1401
1340
 
1402
1341
  ---`;
1403
- }).join("\n\n");
1404
- response += commentSummaries;
1405
- return response;
1342
+ }).join("\n\n");
1343
+ });
1406
1344
  }
1407
1345
  });
1408
1346
  async function main() {
1409
- try {
1410
- await setupRedditClient();
1411
- const useHttp = process.env.TRANSPORT_TYPE === "httpStream" || process.env.TRANSPORT_TYPE === "http";
1412
- const port = parseInt(process.env.PORT || "3000");
1413
- const host = process.env.HOST || "127.0.0.1";
1414
- if (useHttp) {
1415
- console.error(`[Setup] Starting HTTP server on ${host}:${port}`);
1416
- await server.start({
1417
- transportType: "httpStream",
1418
- httpStream: {
1419
- port,
1420
- host,
1421
- endpoint: "/mcp"
1422
- }
1423
- });
1424
- console.error(`[Setup] HTTP server ready at http://${host}:${port}/mcp`);
1425
- console.error(`[Setup] SSE endpoint available at http://${host}:${port}/sse`);
1426
- } else {
1427
- console.error("[Setup] Starting in stdio mode");
1428
- await server.start({ transportType: "stdio" });
1429
- }
1430
- } catch (error) {
1431
- console.error("[Error] Failed to start server:", error);
1432
- process.exit(1);
1347
+ await setupRedditClient();
1348
+ const useHttp = process.env.TRANSPORT_TYPE === "httpStream" || process.env.TRANSPORT_TYPE === "http";
1349
+ const port = parseInt(process.env.PORT ?? "3000");
1350
+ const host = process.env.HOST ?? "127.0.0.1";
1351
+ if (useHttp) {
1352
+ console.error(`[Setup] Starting HTTP server on ${host}:${port}`);
1353
+ await server.start({
1354
+ transportType: "httpStream",
1355
+ httpStream: {
1356
+ port,
1357
+ host,
1358
+ endpoint: "/mcp"
1359
+ }
1360
+ });
1361
+ console.error(`[Setup] HTTP server ready at http://${host}:${port}/mcp`);
1362
+ console.error(`[Setup] SSE endpoint available at http://${host}:${port}/sse`);
1363
+ } else {
1364
+ console.error("[Setup] Starting in stdio mode");
1365
+ await server.start({ transportType: "stdio" });
1433
1366
  }
1434
1367
  }
1435
- process.on("SIGINT", async () => {
1368
+ process.on("SIGINT", () => {
1436
1369
  console.error("[Shutdown] Shutting down Reddit MCP Server...");
1437
1370
  process.exit(0);
1438
1371
  });
1439
- process.on("SIGTERM", async () => {
1372
+ process.on("SIGTERM", () => {
1440
1373
  console.error("[Shutdown] Shutting down Reddit MCP Server...");
1441
1374
  process.exit(0);
1442
1375
  });