reddit-mcp-server 1.4.0 → 1.4.2

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