@prmichaelsen/reddit-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.env.example +13 -0
  3. package/AGENT.md +1772 -0
  4. package/README.md +54 -0
  5. package/agent/commands/acp.clarification-capture.md +386 -0
  6. package/agent/commands/acp.clarification-create.md +432 -0
  7. package/agent/commands/acp.clarifications-research.md +326 -0
  8. package/agent/commands/acp.command-create.md +432 -0
  9. package/agent/commands/acp.design-create.md +286 -0
  10. package/agent/commands/acp.design-reference.md +355 -0
  11. package/agent/commands/acp.index.md +423 -0
  12. package/agent/commands/acp.init.md +546 -0
  13. package/agent/commands/acp.package-create.md +895 -0
  14. package/agent/commands/acp.package-info.md +212 -0
  15. package/agent/commands/acp.package-install.md +539 -0
  16. package/agent/commands/acp.package-list.md +280 -0
  17. package/agent/commands/acp.package-publish.md +541 -0
  18. package/agent/commands/acp.package-remove.md +293 -0
  19. package/agent/commands/acp.package-search.md +307 -0
  20. package/agent/commands/acp.package-update.md +361 -0
  21. package/agent/commands/acp.package-validate.md +540 -0
  22. package/agent/commands/acp.pattern-create.md +386 -0
  23. package/agent/commands/acp.plan.md +577 -0
  24. package/agent/commands/acp.proceed.md +882 -0
  25. package/agent/commands/acp.project-create.md +675 -0
  26. package/agent/commands/acp.project-info.md +312 -0
  27. package/agent/commands/acp.project-list.md +226 -0
  28. package/agent/commands/acp.project-remove.md +379 -0
  29. package/agent/commands/acp.project-set.md +227 -0
  30. package/agent/commands/acp.project-update.md +307 -0
  31. package/agent/commands/acp.projects-restore.md +228 -0
  32. package/agent/commands/acp.projects-sync.md +347 -0
  33. package/agent/commands/acp.report.md +407 -0
  34. package/agent/commands/acp.resume.md +239 -0
  35. package/agent/commands/acp.sessions.md +301 -0
  36. package/agent/commands/acp.status.md +293 -0
  37. package/agent/commands/acp.sync.md +364 -0
  38. package/agent/commands/acp.task-create.md +500 -0
  39. package/agent/commands/acp.update.md +302 -0
  40. package/agent/commands/acp.validate.md +466 -0
  41. package/agent/commands/acp.version-check-for-updates.md +276 -0
  42. package/agent/commands/acp.version-check.md +191 -0
  43. package/agent/commands/acp.version-update.md +289 -0
  44. package/agent/commands/command.template.md +339 -0
  45. package/agent/commands/git.commit.md +526 -0
  46. package/agent/commands/git.init.md +514 -0
  47. package/agent/design/.gitkeep +0 -0
  48. package/agent/design/design.template.md +154 -0
  49. package/agent/design/requirements.md +332 -0
  50. package/agent/design/requirements.template.md +387 -0
  51. package/agent/index/.gitkeep +0 -0
  52. package/agent/index/local.main.template.yaml +37 -0
  53. package/agent/manifest.template.yaml +13 -0
  54. package/agent/manifest.yaml +61 -0
  55. package/agent/milestones/.gitkeep +0 -0
  56. package/agent/milestones/milestone-1-foundation-listings-mvp.md +140 -0
  57. package/agent/milestones/milestone-1-{title}.template.md +206 -0
  58. package/agent/milestones/milestone-2-content-interaction.md +56 -0
  59. package/agent/milestones/milestone-3-users-and-messaging.md +54 -0
  60. package/agent/milestones/milestone-4-subreddits-and-flair.md +56 -0
  61. package/agent/milestones/milestone-5-moderation.md +53 -0
  62. package/agent/milestones/milestone-6-advanced-features-and-polish.md +56 -0
  63. package/agent/package.template.yaml +86 -0
  64. package/agent/patterns/.gitkeep +0 -0
  65. package/agent/patterns/bootstrap.template.md +1237 -0
  66. package/agent/patterns/pattern.template.md +382 -0
  67. package/agent/progress.template.yaml +161 -0
  68. package/agent/progress.yaml +223 -0
  69. package/agent/schemas/package.schema.yaml +276 -0
  70. package/agent/scripts/acp.common.sh +1781 -0
  71. package/agent/scripts/acp.yaml-parser.sh +985 -0
  72. package/agent/tasks/.gitkeep +0 -0
  73. package/agent/tasks/milestone-1-foundation-listings-mvp/task-1-project-scaffolding.md +75 -0
  74. package/agent/tasks/milestone-1-foundation-listings-mvp/task-2-reddit-oauth.md +71 -0
  75. package/agent/tasks/milestone-1-foundation-listings-mvp/task-3-reddit-api-client.md +71 -0
  76. package/agent/tasks/milestone-1-foundation-listings-mvp/task-4-listing-tools.md +65 -0
  77. package/agent/tasks/milestone-1-foundation-listings-mvp/task-5-search-tools.md +43 -0
  78. package/agent/tasks/milestone-1-foundation-listings-mvp/task-6-testing-verification.md +49 -0
  79. package/agent/tasks/milestone-2-content-interaction/task-7-post-tools.md +56 -0
  80. package/agent/tasks/milestone-2-content-interaction/task-8-comment-tools.md +49 -0
  81. package/agent/tasks/milestone-2-content-interaction/task-9-vote-save-report-tools.md +50 -0
  82. package/agent/tasks/milestone-3-users-and-messaging/task-10-account-tools.md +44 -0
  83. package/agent/tasks/milestone-3-users-and-messaging/task-11-user-profile-tools.md +50 -0
  84. package/agent/tasks/milestone-3-users-and-messaging/task-12-private-message-tools.md +50 -0
  85. package/agent/tasks/milestone-4-subreddits-and-flair/task-13-subreddit-tools.md +47 -0
  86. package/agent/tasks/milestone-4-subreddits-and-flair/task-14-flair-tools.md +46 -0
  87. package/agent/tasks/milestone-4-subreddits-and-flair/task-15-http-transport.md +53 -0
  88. package/agent/tasks/milestone-5-moderation/task-16-mod-action-tools.md +48 -0
  89. package/agent/tasks/milestone-5-moderation/task-17-mod-listing-tools.md +47 -0
  90. package/agent/tasks/milestone-5-moderation/task-18-mod-management-tools.md +42 -0
  91. package/agent/tasks/milestone-6-advanced-features-and-polish/task-19-multireddit-tools.md +49 -0
  92. package/agent/tasks/milestone-6-advanced-features-and-polish/task-20-wiki-tools.md +47 -0
  93. package/agent/tasks/milestone-6-advanced-features-and-polish/task-21-documentation-polish.md +65 -0
  94. package/agent/tasks/task-1-{title}.template.md +244 -0
  95. package/dist/auth/oauth.d.ts +15 -0
  96. package/dist/auth/oauth.d.ts.map +1 -0
  97. package/dist/client/reddit.d.ts +28 -0
  98. package/dist/client/reddit.d.ts.map +1 -0
  99. package/dist/factory.d.ts +2 -0
  100. package/dist/factory.d.ts.map +1 -0
  101. package/dist/factory.js +30394 -0
  102. package/dist/factory.js.map +7 -0
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +31955 -0
  106. package/dist/index.js.map +7 -0
  107. package/dist/server.d.ts +5 -0
  108. package/dist/server.d.ts.map +1 -0
  109. package/dist/server.js +30401 -0
  110. package/dist/server.js.map +7 -0
  111. package/dist/tools/listings.d.ts +4 -0
  112. package/dist/tools/listings.d.ts.map +1 -0
  113. package/dist/tools/search.d.ts +4 -0
  114. package/dist/tools/search.d.ts.map +1 -0
  115. package/dist/transport/http.d.ts +7 -0
  116. package/dist/transport/http.d.ts.map +1 -0
  117. package/dist/types/index.d.ts +78 -0
  118. package/dist/types/index.d.ts.map +1 -0
  119. package/esbuild.build.js +21 -0
  120. package/jest.config.js +18 -0
  121. package/package.json +46 -0
  122. package/src/auth/oauth.ts +200 -0
  123. package/src/client/reddit.ts +245 -0
  124. package/src/factory.ts +5 -0
  125. package/src/index.ts +31 -0
  126. package/src/server.ts +36 -0
  127. package/src/tools/listings.ts +202 -0
  128. package/src/tools/search.ts +85 -0
  129. package/src/transport/http.ts +49 -0
  130. package/src/types/index.ts +83 -0
  131. package/tests/fixtures/reddit-responses.ts +132 -0
  132. package/tests/helpers/mock-client.ts +36 -0
  133. package/tests/unit/auth.test.ts +89 -0
  134. package/tests/unit/client.test.ts +218 -0
  135. package/tests/unit/listings.test.ts +113 -0
  136. package/tests/unit/search.test.ts +59 -0
  137. package/tests/unit/server.test.ts +14 -0
  138. package/tsconfig.json +21 -0
package/src/server.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { RedditAuth } from "./auth/oauth.js";
3
+ import { RedditClient } from "./client/reddit.js";
4
+ import { registerListingTools } from "./tools/listings.js";
5
+ import { registerSearchTools } from "./tools/search.js";
6
+
7
+ export function createServer(auth?: RedditAuth): McpServer {
8
+ const server = new McpServer({
9
+ name: "reddit-mcp",
10
+ version: "0.1.0",
11
+ });
12
+
13
+ if (auth) {
14
+ const client = new RedditClient(auth);
15
+ registerAllTools(server, client);
16
+ }
17
+
18
+ return server;
19
+ }
20
+
21
+ export function createServerWithToken(accessToken: string): McpServer {
22
+ const server = new McpServer({
23
+ name: "reddit-mcp",
24
+ version: "0.1.0",
25
+ });
26
+
27
+ const client = new RedditClient(accessToken);
28
+ registerAllTools(server, client);
29
+
30
+ return server;
31
+ }
32
+
33
+ function registerAllTools(server: McpServer, client: RedditClient): void {
34
+ registerListingTools(server, client);
35
+ registerSearchTools(server, client);
36
+ }
@@ -0,0 +1,202 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { RedditClient } from "../client/reddit.js";
4
+
5
+ const paginationParams = {
6
+ limit: z
7
+ .number()
8
+ .min(1)
9
+ .max(100)
10
+ .optional()
11
+ .describe("Number of items to return (1-100, default 25)"),
12
+ after: z.string().optional().describe("Fullname of item to paginate after"),
13
+ before: z
14
+ .string()
15
+ .optional()
16
+ .describe("Fullname of item to paginate before"),
17
+ };
18
+
19
+ const timeFilter = z
20
+ .enum(["hour", "day", "week", "month", "year", "all"])
21
+ .optional()
22
+ .describe("Time filter for top/controversial listings");
23
+
24
+ function buildParams(
25
+ input: Record<string, unknown>,
26
+ ): Record<string, string | undefined> {
27
+ const params: Record<string, string | undefined> = {};
28
+ for (const [key, value] of Object.entries(input)) {
29
+ if (value !== undefined && value !== null) {
30
+ params[key] = String(value);
31
+ }
32
+ }
33
+ return params;
34
+ }
35
+
36
+ export function registerListingTools(
37
+ server: McpServer,
38
+ client: RedditClient,
39
+ ): void {
40
+ server.tool(
41
+ "reddit_listings_best",
42
+ "Get the best posts from Reddit. Requires 'read' scope.",
43
+ { ...paginationParams },
44
+ async (input) => {
45
+ const data = await client.get("/best", buildParams(input));
46
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
47
+ },
48
+ );
49
+
50
+ server.tool(
51
+ "reddit_listings_hot",
52
+ "Get hot posts from Reddit frontpage or a specific subreddit. Requires 'read' scope.",
53
+ {
54
+ subreddit: z
55
+ .string()
56
+ .optional()
57
+ .describe("Subreddit name (without r/ prefix). Omit for frontpage."),
58
+ ...paginationParams,
59
+ },
60
+ async (input) => {
61
+ const { subreddit, ...rest } = input;
62
+ const path = subreddit ? `/r/${subreddit}/hot` : "/hot";
63
+ const data = await client.get(path, buildParams(rest));
64
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
65
+ },
66
+ );
67
+
68
+ server.tool(
69
+ "reddit_listings_new",
70
+ "Get newest posts from Reddit frontpage or a specific subreddit. Requires 'read' scope.",
71
+ {
72
+ subreddit: z
73
+ .string()
74
+ .optional()
75
+ .describe("Subreddit name (without r/ prefix). Omit for frontpage."),
76
+ ...paginationParams,
77
+ },
78
+ async (input) => {
79
+ const { subreddit, ...rest } = input;
80
+ const path = subreddit ? `/r/${subreddit}/new` : "/new";
81
+ const data = await client.get(path, buildParams(rest));
82
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
83
+ },
84
+ );
85
+
86
+ server.tool(
87
+ "reddit_listings_rising",
88
+ "Get rising posts from Reddit frontpage or a specific subreddit. Requires 'read' scope.",
89
+ {
90
+ subreddit: z
91
+ .string()
92
+ .optional()
93
+ .describe("Subreddit name (without r/ prefix). Omit for frontpage."),
94
+ ...paginationParams,
95
+ },
96
+ async (input) => {
97
+ const { subreddit, ...rest } = input;
98
+ const path = subreddit ? `/r/${subreddit}/rising` : "/rising";
99
+ const data = await client.get(path, buildParams(rest));
100
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
101
+ },
102
+ );
103
+
104
+ server.tool(
105
+ "reddit_listings_top",
106
+ "Get top posts from Reddit frontpage or a specific subreddit. Requires 'read' scope.",
107
+ {
108
+ subreddit: z
109
+ .string()
110
+ .optional()
111
+ .describe("Subreddit name (without r/ prefix). Omit for frontpage."),
112
+ t: timeFilter,
113
+ ...paginationParams,
114
+ },
115
+ async (input) => {
116
+ const { subreddit, ...rest } = input;
117
+ const path = subreddit ? `/r/${subreddit}/top` : "/top";
118
+ const data = await client.get(path, buildParams(rest));
119
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
120
+ },
121
+ );
122
+
123
+ server.tool(
124
+ "reddit_listings_controversial",
125
+ "Get controversial posts from Reddit frontpage or a specific subreddit. Requires 'read' scope.",
126
+ {
127
+ subreddit: z
128
+ .string()
129
+ .optional()
130
+ .describe("Subreddit name (without r/ prefix). Omit for frontpage."),
131
+ t: timeFilter,
132
+ ...paginationParams,
133
+ },
134
+ async (input) => {
135
+ const { subreddit, ...rest } = input;
136
+ const path = subreddit
137
+ ? `/r/${subreddit}/controversial`
138
+ : "/controversial";
139
+ const data = await client.get(path, buildParams(rest));
140
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
141
+ },
142
+ );
143
+
144
+ server.tool(
145
+ "reddit_comments_thread",
146
+ "Get a post with its full comment tree. Requires 'read' scope.",
147
+ {
148
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
149
+ article: z.string().describe("Post ID (without t3_ prefix)"),
150
+ sort: z
151
+ .enum(["confidence", "top", "new", "controversial", "old", "qa"])
152
+ .optional()
153
+ .describe("Comment sort order"),
154
+ limit: z
155
+ .number()
156
+ .optional()
157
+ .describe("Maximum number of comments to return"),
158
+ depth: z.number().optional().describe("Maximum comment depth"),
159
+ },
160
+ async (input) => {
161
+ const { subreddit, article, ...rest } = input;
162
+ const data = await client.get(
163
+ `/r/${subreddit}/comments/${article}`,
164
+ buildParams(rest),
165
+ );
166
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
167
+ },
168
+ );
169
+
170
+ server.tool(
171
+ "reddit_duplicates",
172
+ "Get cross-posts and duplicates of a post. Requires 'read' scope.",
173
+ {
174
+ article: z.string().describe("Post ID (without t3_ prefix)"),
175
+ ...paginationParams,
176
+ },
177
+ async (input) => {
178
+ const { article, ...rest } = input;
179
+ const data = await client.get(
180
+ `/duplicates/${article}`,
181
+ buildParams(rest),
182
+ );
183
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
184
+ },
185
+ );
186
+
187
+ server.tool(
188
+ "reddit_info",
189
+ "Get info about things by fullname (e.g. t3_abc123, t1_xyz789). Requires 'read' scope.",
190
+ {
191
+ id: z
192
+ .string()
193
+ .describe(
194
+ "Comma-separated fullnames (e.g. t3_abc123,t1_xyz789)",
195
+ ),
196
+ },
197
+ async (input) => {
198
+ const data = await client.get("/api/info", { id: input.id });
199
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
200
+ },
201
+ );
202
+ }
@@ -0,0 +1,85 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { RedditClient } from "../client/reddit.js";
4
+
5
+ function buildParams(
6
+ input: Record<string, unknown>,
7
+ ): Record<string, string | undefined> {
8
+ const params: Record<string, string | undefined> = {};
9
+ for (const [key, value] of Object.entries(input)) {
10
+ if (value !== undefined && value !== null) {
11
+ params[key] = String(value);
12
+ }
13
+ }
14
+ return params;
15
+ }
16
+
17
+ export function registerSearchTools(
18
+ server: McpServer,
19
+ client: RedditClient,
20
+ ): void {
21
+ server.tool(
22
+ "reddit_search",
23
+ "Search across all of Reddit for posts, subreddits, or users. Requires 'read' scope.",
24
+ {
25
+ q: z.string().describe("Search query"),
26
+ sort: z
27
+ .enum(["relevance", "hot", "top", "new", "comments"])
28
+ .optional()
29
+ .describe("Sort order for results"),
30
+ t: z
31
+ .enum(["hour", "day", "week", "month", "year", "all"])
32
+ .optional()
33
+ .describe("Time filter"),
34
+ type: z
35
+ .enum(["link", "sr", "user"])
36
+ .optional()
37
+ .describe("Type of results: link (posts), sr (subreddits), user"),
38
+ limit: z
39
+ .number()
40
+ .min(1)
41
+ .max(100)
42
+ .optional()
43
+ .describe("Number of results (1-100, default 25)"),
44
+ after: z.string().optional().describe("Pagination cursor (after)"),
45
+ before: z.string().optional().describe("Pagination cursor (before)"),
46
+ },
47
+ async (input) => {
48
+ const data = await client.get("/search", buildParams(input));
49
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
50
+ },
51
+ );
52
+
53
+ server.tool(
54
+ "reddit_search_subreddit",
55
+ "Search for posts within a specific subreddit. Requires 'read' scope.",
56
+ {
57
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
58
+ q: z.string().describe("Search query"),
59
+ sort: z
60
+ .enum(["relevance", "hot", "top", "new", "comments"])
61
+ .optional()
62
+ .describe("Sort order for results"),
63
+ t: z
64
+ .enum(["hour", "day", "week", "month", "year", "all"])
65
+ .optional()
66
+ .describe("Time filter"),
67
+ limit: z
68
+ .number()
69
+ .min(1)
70
+ .max(100)
71
+ .optional()
72
+ .describe("Number of results (1-100, default 25)"),
73
+ after: z.string().optional().describe("Pagination cursor (after)"),
74
+ before: z.string().optional().describe("Pagination cursor (before)"),
75
+ },
76
+ async (input) => {
77
+ const { subreddit, ...rest } = input;
78
+ const data = await client.get(`/r/${subreddit}/search`, {
79
+ ...buildParams(rest),
80
+ restrict_sr: "true",
81
+ });
82
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
83
+ },
84
+ );
85
+ }
@@ -0,0 +1,49 @@
1
+ import {
2
+ createServer as createHttpServer,
3
+ IncomingMessage,
4
+ ServerResponse,
5
+ } from "node:http";
6
+ import { randomUUID } from "node:crypto";
7
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+
10
+ export interface HttpTransportOptions {
11
+ port: number;
12
+ host: string;
13
+ }
14
+
15
+ export async function startHttpTransport(
16
+ server: McpServer,
17
+ options: HttpTransportOptions,
18
+ ): Promise<void> {
19
+ const transport = new StreamableHTTPServerTransport({
20
+ sessionIdGenerator: () => randomUUID(),
21
+ });
22
+
23
+ const httpServer = createHttpServer(
24
+ (req: IncomingMessage, res: ServerResponse) => {
25
+ if (req.url === "/mcp" || req.url === "/mcp/") {
26
+ transport.handleRequest(req, res);
27
+ } else if (req.url === "/health") {
28
+ res.writeHead(200, { "Content-Type": "application/json" });
29
+ res.end(JSON.stringify({ status: "ok" }));
30
+ } else {
31
+ res.writeHead(404, { "Content-Type": "application/json" });
32
+ res.end(JSON.stringify({ error: "Not found" }));
33
+ }
34
+ },
35
+ );
36
+
37
+ await server.connect(transport);
38
+
39
+ return new Promise((resolve, reject) => {
40
+ httpServer.listen(options.port, options.host, () => {
41
+ console.error(
42
+ `Reddit MCP server running on http://${options.host}:${options.port}/mcp`,
43
+ );
44
+ resolve();
45
+ });
46
+
47
+ httpServer.on("error", reject);
48
+ });
49
+ }
@@ -0,0 +1,83 @@
1
+ export interface RedditAuthConfig {
2
+ clientId: string;
3
+ clientSecret: string;
4
+ redirectUri: string;
5
+ userAgent: string;
6
+ tokenStoragePath?: string;
7
+ }
8
+
9
+ export interface TokenData {
10
+ access_token: string;
11
+ refresh_token: string;
12
+ token_type: string;
13
+ expires_in: number;
14
+ scope: string;
15
+ obtained_at: number;
16
+ }
17
+
18
+ export interface RedditApiError {
19
+ status: number;
20
+ reason: string;
21
+ message: string;
22
+ }
23
+
24
+ export interface RedditListing<T> {
25
+ kind: "Listing";
26
+ data: {
27
+ after: string | null;
28
+ before: string | null;
29
+ children: Array<{
30
+ kind: string;
31
+ data: T;
32
+ }>;
33
+ dist: number;
34
+ modhash: string;
35
+ };
36
+ }
37
+
38
+ export interface RedditPost {
39
+ id: string;
40
+ name: string;
41
+ title: string;
42
+ author: string;
43
+ subreddit: string;
44
+ subreddit_name_prefixed: string;
45
+ selftext: string;
46
+ url: string;
47
+ permalink: string;
48
+ score: number;
49
+ ups: number;
50
+ downs: number;
51
+ num_comments: number;
52
+ created_utc: number;
53
+ is_self: boolean;
54
+ over_18: boolean;
55
+ spoiler: boolean;
56
+ locked: boolean;
57
+ stickied: boolean;
58
+ link_flair_text: string | null;
59
+ thumbnail: string;
60
+ }
61
+
62
+ export interface RedditComment {
63
+ id: string;
64
+ name: string;
65
+ body: string;
66
+ author: string;
67
+ subreddit: string;
68
+ score: number;
69
+ ups: number;
70
+ downs: number;
71
+ created_utc: number;
72
+ parent_id: string;
73
+ link_id: string;
74
+ is_submitter: boolean;
75
+ stickied: boolean;
76
+ distinguished: string | null;
77
+ }
78
+
79
+ export interface RateLimitInfo {
80
+ remaining: number;
81
+ used: number;
82
+ resetSeconds: number;
83
+ }
@@ -0,0 +1,132 @@
1
+ export const mockListing = {
2
+ kind: "Listing",
3
+ data: {
4
+ after: "t3_abc123",
5
+ before: null,
6
+ children: [
7
+ {
8
+ kind: "t3",
9
+ data: {
10
+ id: "abc123",
11
+ name: "t3_abc123",
12
+ title: "Test Post",
13
+ author: "testuser",
14
+ subreddit: "test",
15
+ subreddit_name_prefixed: "r/test",
16
+ selftext: "This is a test post",
17
+ url: "https://www.reddit.com/r/test/comments/abc123/test_post/",
18
+ permalink: "/r/test/comments/abc123/test_post/",
19
+ score: 42,
20
+ ups: 50,
21
+ downs: 8,
22
+ num_comments: 10,
23
+ created_utc: 1710000000,
24
+ is_self: true,
25
+ over_18: false,
26
+ spoiler: false,
27
+ locked: false,
28
+ stickied: false,
29
+ link_flair_text: null,
30
+ thumbnail: "self",
31
+ },
32
+ },
33
+ ],
34
+ dist: 1,
35
+ modhash: "",
36
+ },
37
+ };
38
+
39
+ export const mockCommentThread = [
40
+ mockListing,
41
+ {
42
+ kind: "Listing",
43
+ data: {
44
+ after: null,
45
+ before: null,
46
+ children: [
47
+ {
48
+ kind: "t1",
49
+ data: {
50
+ id: "xyz789",
51
+ name: "t1_xyz789",
52
+ body: "This is a test comment",
53
+ author: "commenter",
54
+ subreddit: "test",
55
+ score: 5,
56
+ ups: 6,
57
+ downs: 1,
58
+ created_utc: 1710001000,
59
+ parent_id: "t3_abc123",
60
+ link_id: "t3_abc123",
61
+ is_submitter: false,
62
+ stickied: false,
63
+ distinguished: null,
64
+ },
65
+ },
66
+ ],
67
+ dist: 1,
68
+ modhash: "",
69
+ },
70
+ },
71
+ ];
72
+
73
+ export const mockSearchResults = {
74
+ kind: "Listing",
75
+ data: {
76
+ after: null,
77
+ before: null,
78
+ children: [
79
+ {
80
+ kind: "t3",
81
+ data: {
82
+ id: "search1",
83
+ name: "t3_search1",
84
+ title: "Search Result Post",
85
+ author: "searchuser",
86
+ subreddit: "programming",
87
+ selftext: "Found via search",
88
+ score: 100,
89
+ num_comments: 25,
90
+ created_utc: 1710000000,
91
+ },
92
+ },
93
+ ],
94
+ dist: 1,
95
+ modhash: "",
96
+ },
97
+ };
98
+
99
+ export const mockUserProfile = {
100
+ kind: "t2",
101
+ data: {
102
+ name: "testuser",
103
+ id: "user123",
104
+ created_utc: 1600000000,
105
+ link_karma: 1000,
106
+ comment_karma: 5000,
107
+ is_gold: false,
108
+ is_mod: true,
109
+ has_verified_email: true,
110
+ icon_img: "https://www.redditstatic.com/avatars/default.png",
111
+ },
112
+ };
113
+
114
+ export const mockSubredditAbout = {
115
+ kind: "t5",
116
+ data: {
117
+ display_name: "test",
118
+ title: "Test Subreddit",
119
+ public_description: "A subreddit for testing",
120
+ subscribers: 100000,
121
+ active_user_count: 500,
122
+ created_utc: 1500000000,
123
+ over18: false,
124
+ description: "Full description of the test subreddit",
125
+ },
126
+ };
127
+
128
+ export const mockErrorResponse = {
129
+ error: 403,
130
+ message: "Forbidden",
131
+ reason: "private",
132
+ };
@@ -0,0 +1,36 @@
1
+ import { jest } from "@jest/globals";
2
+ import { RedditClient } from "../../src/client/reddit.js";
3
+
4
+ export function createMockClient(
5
+ responses: Map<string, unknown> = new Map(),
6
+ ): RedditClient & {
7
+ mockGet: jest.Mock;
8
+ mockPost: jest.Mock;
9
+ } {
10
+ const mockGet = jest.fn().mockImplementation(async (path: string) => {
11
+ if (responses.has(path)) {
12
+ return responses.get(path);
13
+ }
14
+ return { kind: "Listing", data: { children: [], after: null, before: null, dist: 0, modhash: "" } };
15
+ });
16
+
17
+ const mockPost = jest.fn().mockImplementation(async (path: string) => {
18
+ if (responses.has(path)) {
19
+ return responses.get(path);
20
+ }
21
+ return { json: { errors: [] } };
22
+ });
23
+
24
+ const client = {
25
+ get: mockGet,
26
+ post: mockPost,
27
+ patch: jest.fn(),
28
+ put: jest.fn(),
29
+ delete: jest.fn(),
30
+ getRateLimitInfo: jest.fn().mockReturnValue(null),
31
+ mockGet,
32
+ mockPost,
33
+ } as unknown as RedditClient & { mockGet: jest.Mock; mockPost: jest.Mock };
34
+
35
+ return client;
36
+ }