openclaw-reddit-agent-server 2026.2.15
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.d.ts +23 -0
- package/dist/index.js +267 -0
- package/dist/index.js.map +1 -0
- package/openclaw.plugin.json +25 -0
- package/package.json +40 -0
- package/skills/reddit-agent/SKILL.md +31 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface PluginLogger {
|
|
2
|
+
info: (msg: string) => void;
|
|
3
|
+
warn: (msg: string) => void;
|
|
4
|
+
error: (msg: string) => void;
|
|
5
|
+
}
|
|
6
|
+
interface PluginApi {
|
|
7
|
+
pluginConfig: Record<string, unknown> | undefined;
|
|
8
|
+
logger: PluginLogger;
|
|
9
|
+
registerTool: (config: unknown) => void;
|
|
10
|
+
registerService: (config: {
|
|
11
|
+
id: string;
|
|
12
|
+
start: () => void | Promise<void>;
|
|
13
|
+
stop: () => void | Promise<void>;
|
|
14
|
+
}) => void;
|
|
15
|
+
}
|
|
16
|
+
declare const redditAgentPlugin: {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
register(api: PluginApi): void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { redditAgentPlugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/ws-bridge.ts
|
|
2
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
5
|
+
var WebSocketBridge = class {
|
|
6
|
+
wss = null;
|
|
7
|
+
extensionSocket = null;
|
|
8
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
9
|
+
port;
|
|
10
|
+
logger;
|
|
11
|
+
constructor(port, logger) {
|
|
12
|
+
this.port = port;
|
|
13
|
+
this.logger = logger ?? {
|
|
14
|
+
info: (msg) => console.log(`[reddit-agent] ${msg}`),
|
|
15
|
+
warn: (msg) => console.warn(`[reddit-agent] ${msg}`),
|
|
16
|
+
error: (msg) => console.error(`[reddit-agent] ${msg}`)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
get isExtensionConnected() {
|
|
20
|
+
return this.extensionSocket !== null && this.extensionSocket.readyState === WebSocket.OPEN;
|
|
21
|
+
}
|
|
22
|
+
start() {
|
|
23
|
+
if (this.wss) return;
|
|
24
|
+
this.wss = new WebSocketServer({ port: this.port, path: "/ws" });
|
|
25
|
+
this.logger.info(
|
|
26
|
+
`WebSocket bridge listening on ws://localhost:${this.port}/ws`
|
|
27
|
+
);
|
|
28
|
+
this.wss.on("connection", (socket) => {
|
|
29
|
+
this.logger.info("New WebSocket connection");
|
|
30
|
+
socket.on("message", (raw) => {
|
|
31
|
+
let message;
|
|
32
|
+
try {
|
|
33
|
+
message = JSON.parse(
|
|
34
|
+
typeof raw === "string" ? raw : raw.toString("utf8")
|
|
35
|
+
);
|
|
36
|
+
} catch {
|
|
37
|
+
this.logger.warn("Failed to parse incoming WebSocket message");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if ("type" in message && message.type === "identify") {
|
|
41
|
+
this.logger.info(
|
|
42
|
+
`Extension identified (role: ${message.role})`
|
|
43
|
+
);
|
|
44
|
+
this.extensionSocket = socket;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if ("type" in message && message.type === "ping") {
|
|
48
|
+
socket.send(JSON.stringify({ type: "pong" }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if ("id" in message && "success" in message) {
|
|
52
|
+
const response = message;
|
|
53
|
+
const pending = this.pendingRequests.get(response.id);
|
|
54
|
+
if (!pending) {
|
|
55
|
+
this.logger.warn(
|
|
56
|
+
`Received response for unknown request id: ${response.id}`
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.pendingRequests.delete(response.id);
|
|
61
|
+
clearTimeout(pending.timer);
|
|
62
|
+
if (response.success) {
|
|
63
|
+
pending.resolve(response.data);
|
|
64
|
+
} else {
|
|
65
|
+
pending.reject(new Error(response.error));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
socket.on("close", () => {
|
|
70
|
+
if (socket === this.extensionSocket) {
|
|
71
|
+
this.logger.info("Extension disconnected");
|
|
72
|
+
this.extensionSocket = null;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
socket.on("error", (err) => {
|
|
76
|
+
this.logger.error(`WebSocket error: ${err.message}`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
stop() {
|
|
81
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
82
|
+
clearTimeout(pending.timer);
|
|
83
|
+
pending.reject(new Error("WebSocket bridge shutting down"));
|
|
84
|
+
this.pendingRequests.delete(id);
|
|
85
|
+
}
|
|
86
|
+
if (this.extensionSocket) {
|
|
87
|
+
this.extensionSocket.close();
|
|
88
|
+
this.extensionSocket = null;
|
|
89
|
+
}
|
|
90
|
+
if (this.wss) {
|
|
91
|
+
this.wss.close();
|
|
92
|
+
this.wss = null;
|
|
93
|
+
}
|
|
94
|
+
this.logger.info("WebSocket bridge stopped");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Send an action to the connected Chrome extension and wait for a response.
|
|
98
|
+
* Returns the `data` field from the success response.
|
|
99
|
+
*/
|
|
100
|
+
sendAction(action, params, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
101
|
+
if (!this.isExtensionConnected) {
|
|
102
|
+
return Promise.reject(
|
|
103
|
+
new Error(
|
|
104
|
+
`Reddit Agent Chrome extension is not connected. Please ensure the extension is installed, the server URL is configured to ws://localhost:${this.port}/ws in the extension popup, and the browser is running.`
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const id = randomUUID();
|
|
109
|
+
const request = { id, action, params };
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
this.pendingRequests.delete(id);
|
|
113
|
+
reject(
|
|
114
|
+
new Error(
|
|
115
|
+
`Request timed out after ${timeoutMs}ms for action: ${action}`
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
120
|
+
this.extensionSocket.send(JSON.stringify(request));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/tools.ts
|
|
126
|
+
import { Type } from "@sinclair/typebox";
|
|
127
|
+
var REPLY_TIMEOUT_MS = 9e4;
|
|
128
|
+
function registerRedditTools(registerTool, bridge) {
|
|
129
|
+
registerTool({
|
|
130
|
+
name: "reddit_fetch_subreddit",
|
|
131
|
+
description: "Fetch a subreddit's listing from Reddit. Accepts subreddit name, r/name, or full URL. Optionally specify sort order (hot, new, top, rising, best).",
|
|
132
|
+
parameters: Type.Object({
|
|
133
|
+
subreddit: Type.String({
|
|
134
|
+
description: 'Subreddit name or URL. Accepts: "supplements", "r/supplements", "/r/supplements", or "https://www.reddit.com/r/supplements".'
|
|
135
|
+
}),
|
|
136
|
+
sort: Type.Optional(
|
|
137
|
+
Type.Unsafe({
|
|
138
|
+
type: "string",
|
|
139
|
+
enum: ["hot", "new", "top", "rising", "best"],
|
|
140
|
+
description: "Sort order. Default: hot."
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
}),
|
|
144
|
+
async execute(_id, params) {
|
|
145
|
+
const data = await bridge.sendAction("fetch_subreddit", {
|
|
146
|
+
subreddit: params.subreddit,
|
|
147
|
+
sort: params.sort
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
registerTool({
|
|
155
|
+
name: "reddit_search",
|
|
156
|
+
description: "Search Reddit for posts matching a query. Optionally scope to a specific subreddit. Supports sort (relevance, top, new, comments) and time filters (hour, day, week, month, year).",
|
|
157
|
+
parameters: Type.Object({
|
|
158
|
+
query: Type.String({ description: "Search query string." }),
|
|
159
|
+
subreddit: Type.Optional(
|
|
160
|
+
Type.String({
|
|
161
|
+
description: 'Subreddit to search within. Omit to search all of Reddit. Accepts same formats as reddit_fetch_subreddit (e.g. "supplements").'
|
|
162
|
+
})
|
|
163
|
+
),
|
|
164
|
+
sort: Type.Optional(
|
|
165
|
+
Type.Unsafe({
|
|
166
|
+
type: "string",
|
|
167
|
+
enum: ["relevance", "top", "new", "comments"],
|
|
168
|
+
description: "Sort order for results. Default: relevance."
|
|
169
|
+
})
|
|
170
|
+
),
|
|
171
|
+
time: Type.Optional(
|
|
172
|
+
Type.Unsafe({
|
|
173
|
+
type: "string",
|
|
174
|
+
enum: ["hour", "day", "week", "month", "year"],
|
|
175
|
+
description: "Time filter. Default: all time."
|
|
176
|
+
})
|
|
177
|
+
)
|
|
178
|
+
}),
|
|
179
|
+
async execute(_id, params) {
|
|
180
|
+
const data = await bridge.sendAction("search_reddit", {
|
|
181
|
+
query: params.query,
|
|
182
|
+
subreddit: params.subreddit,
|
|
183
|
+
sort: params.sort,
|
|
184
|
+
time: params.time
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
registerTool({
|
|
192
|
+
name: "reddit_fetch_post",
|
|
193
|
+
description: "Fetch a single Reddit post and its full comment tree. Provide the full post URL.",
|
|
194
|
+
parameters: Type.Object({
|
|
195
|
+
url: Type.String({
|
|
196
|
+
description: "Full Reddit post URL (e.g. https://www.reddit.com/r/supplements/comments/abc123/some_post/)."
|
|
197
|
+
})
|
|
198
|
+
}),
|
|
199
|
+
async execute(_id, params) {
|
|
200
|
+
const data = await bridge.sendAction("fetch_post", {
|
|
201
|
+
url: params.url
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
registerTool({
|
|
209
|
+
name: "reddit_reply",
|
|
210
|
+
description: "Reply to a Reddit comment or post using the user's logged-in session in Chrome. Requires the Reddit Agent Chrome extension to be connected and the user to be logged into Reddit. Takes ~7-10 seconds due to DOM interaction.",
|
|
211
|
+
parameters: Type.Object({
|
|
212
|
+
commentUrl: Type.String({
|
|
213
|
+
description: "Direct URL to the comment or post to reply to."
|
|
214
|
+
}),
|
|
215
|
+
replyText: Type.String({
|
|
216
|
+
description: "The reply text to post."
|
|
217
|
+
})
|
|
218
|
+
}),
|
|
219
|
+
async execute(_id, params) {
|
|
220
|
+
const data = await bridge.sendAction(
|
|
221
|
+
"reply_to_comment",
|
|
222
|
+
{
|
|
223
|
+
commentUrl: params.commentUrl,
|
|
224
|
+
replyText: params.replyText
|
|
225
|
+
},
|
|
226
|
+
REPLY_TIMEOUT_MS
|
|
227
|
+
);
|
|
228
|
+
return {
|
|
229
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// index.ts
|
|
236
|
+
var DEFAULT_PORT = 7071;
|
|
237
|
+
var redditAgentPlugin = {
|
|
238
|
+
id: "reddit-agent",
|
|
239
|
+
name: "Reddit Agent",
|
|
240
|
+
description: "Bridge to Reddit Chrome extension for browsing, searching, and commenting.",
|
|
241
|
+
register(api) {
|
|
242
|
+
const port = typeof api.pluginConfig?.port === "number" ? api.pluginConfig.port : DEFAULT_PORT;
|
|
243
|
+
const bridge = new WebSocketBridge(port, {
|
|
244
|
+
info: (msg) => api.logger.info(msg),
|
|
245
|
+
warn: (msg) => api.logger.warn(msg),
|
|
246
|
+
error: (msg) => api.logger.error(msg)
|
|
247
|
+
});
|
|
248
|
+
api.registerService({
|
|
249
|
+
id: "reddit-agent-bridge",
|
|
250
|
+
start: () => {
|
|
251
|
+
bridge.start();
|
|
252
|
+
},
|
|
253
|
+
stop: () => {
|
|
254
|
+
bridge.stop();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
registerRedditTools(
|
|
258
|
+
(config) => api.registerTool(config),
|
|
259
|
+
bridge
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
var index_default = redditAgentPlugin;
|
|
264
|
+
export {
|
|
265
|
+
index_default as default
|
|
266
|
+
};
|
|
267
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ws-bridge.ts","../src/tools.ts","../index.ts"],"sourcesContent":["import { WebSocketServer, WebSocket } from \"ws\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n BridgeRequest,\n BridgeResponse,\n ExtensionMessage,\n PendingRequest,\n} from \"./types.js\";\n\nconst REQUEST_TIMEOUT_MS = 60_000;\n\nexport interface BridgeLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n}\n\nexport class WebSocketBridge {\n private wss: WebSocketServer | null = null;\n private extensionSocket: WebSocket | null = null;\n private pendingRequests = new Map<string, PendingRequest>();\n private port: number;\n private logger: BridgeLogger;\n\n constructor(port: number, logger?: BridgeLogger) {\n this.port = port;\n this.logger = logger ?? {\n info: (msg: string) => console.log(`[reddit-agent] ${msg}`),\n warn: (msg: string) => console.warn(`[reddit-agent] ${msg}`),\n error: (msg: string) => console.error(`[reddit-agent] ${msg}`),\n };\n }\n\n get isExtensionConnected(): boolean {\n return (\n this.extensionSocket !== null &&\n this.extensionSocket.readyState === WebSocket.OPEN\n );\n }\n\n start(): void {\n if (this.wss) return;\n\n this.wss = new WebSocketServer({ port: this.port, path: \"/ws\" });\n this.logger.info(\n `WebSocket bridge listening on ws://localhost:${this.port}/ws`\n );\n\n this.wss.on(\"connection\", (socket) => {\n this.logger.info(\"New WebSocket connection\");\n\n socket.on(\"message\", (raw) => {\n let message: ExtensionMessage;\n try {\n message = JSON.parse(\n typeof raw === \"string\" ? raw : raw.toString(\"utf8\")\n );\n } catch {\n this.logger.warn(\"Failed to parse incoming WebSocket message\");\n return;\n }\n\n // Handle identify\n if (\"type\" in message && message.type === \"identify\") {\n this.logger.info(\n `Extension identified (role: ${(message as { role: string }).role})`\n );\n this.extensionSocket = socket;\n return;\n }\n\n // Handle ping/pong keepalive\n if (\"type\" in message && message.type === \"ping\") {\n socket.send(JSON.stringify({ type: \"pong\" }));\n return;\n }\n\n // Handle response to a pending request\n if (\"id\" in message && \"success\" in message) {\n const response = message as BridgeResponse;\n const pending = this.pendingRequests.get(response.id);\n if (!pending) {\n this.logger.warn(\n `Received response for unknown request id: ${response.id}`\n );\n return;\n }\n this.pendingRequests.delete(response.id);\n clearTimeout(pending.timer);\n\n if (response.success) {\n pending.resolve(response.data);\n } else {\n pending.reject(new Error(response.error));\n }\n }\n });\n\n socket.on(\"close\", () => {\n if (socket === this.extensionSocket) {\n this.logger.info(\"Extension disconnected\");\n this.extensionSocket = null;\n }\n });\n\n socket.on(\"error\", (err) => {\n this.logger.error(`WebSocket error: ${err.message}`);\n });\n });\n }\n\n stop(): void {\n // Reject all pending requests\n for (const [id, pending] of this.pendingRequests) {\n clearTimeout(pending.timer);\n pending.reject(new Error(\"WebSocket bridge shutting down\"));\n this.pendingRequests.delete(id);\n }\n\n if (this.extensionSocket) {\n this.extensionSocket.close();\n this.extensionSocket = null;\n }\n\n if (this.wss) {\n this.wss.close();\n this.wss = null;\n }\n\n this.logger.info(\"WebSocket bridge stopped\");\n }\n\n /**\n * Send an action to the connected Chrome extension and wait for a response.\n * Returns the `data` field from the success response.\n */\n sendAction(\n action: string,\n params: Record<string, unknown>,\n timeoutMs = REQUEST_TIMEOUT_MS\n ): Promise<unknown> {\n if (!this.isExtensionConnected) {\n return Promise.reject(\n new Error(\n \"Reddit Agent Chrome extension is not connected. \" +\n \"Please ensure the extension is installed, the server URL is configured \" +\n `to ws://localhost:${this.port}/ws in the extension popup, and the browser is running.`\n )\n );\n }\n\n const id = randomUUID();\n const request: BridgeRequest = { id, action, params };\n\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pendingRequests.delete(id);\n reject(\n new Error(\n `Request timed out after ${timeoutMs}ms for action: ${action}`\n )\n );\n }, timeoutMs);\n\n this.pendingRequests.set(id, { resolve, reject, timer });\n this.extensionSocket!.send(JSON.stringify(request));\n });\n }\n}\n","import { Type } from \"@sinclair/typebox\";\nimport type { WebSocketBridge } from \"./ws-bridge.js\";\n\nconst REPLY_TIMEOUT_MS = 90_000;\n\ninterface ToolConfig {\n name: string;\n description: string;\n parameters: unknown;\n execute: (\n id: string,\n params: Record<string, unknown>\n ) => Promise<{ content: { type: \"text\"; text: string }[] }>;\n}\n\nexport function registerRedditTools(\n registerTool: (config: ToolConfig) => void,\n bridge: WebSocketBridge\n) {\n registerTool({\n name: \"reddit_fetch_subreddit\",\n description:\n \"Fetch a subreddit's listing from Reddit. Accepts subreddit name, r/name, \" +\n \"or full URL. Optionally specify sort order (hot, new, top, rising, best).\",\n parameters: Type.Object({\n subreddit: Type.String({\n description:\n 'Subreddit name or URL. Accepts: \"supplements\", \"r/supplements\", ' +\n '\"/r/supplements\", or \"https://www.reddit.com/r/supplements\".',\n }),\n sort: Type.Optional(\n Type.Unsafe<\"hot\" | \"new\" | \"top\" | \"rising\" | \"best\">({\n type: \"string\",\n enum: [\"hot\", \"new\", \"top\", \"rising\", \"best\"],\n description: \"Sort order. Default: hot.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_subreddit\", {\n subreddit: params.subreddit,\n sort: params.sort,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_search\",\n description:\n \"Search Reddit for posts matching a query. Optionally scope to a specific \" +\n \"subreddit. Supports sort (relevance, top, new, comments) and time \" +\n \"filters (hour, day, week, month, year).\",\n parameters: Type.Object({\n query: Type.String({ description: \"Search query string.\" }),\n subreddit: Type.Optional(\n Type.String({\n description:\n \"Subreddit to search within. Omit to search all of Reddit. \" +\n 'Accepts same formats as reddit_fetch_subreddit (e.g. \"supplements\").',\n })\n ),\n sort: Type.Optional(\n Type.Unsafe<\"relevance\" | \"top\" | \"new\" | \"comments\">({\n type: \"string\",\n enum: [\"relevance\", \"top\", \"new\", \"comments\"],\n description: \"Sort order for results. Default: relevance.\",\n })\n ),\n time: Type.Optional(\n Type.Unsafe<\"hour\" | \"day\" | \"week\" | \"month\" | \"year\">({\n type: \"string\",\n enum: [\"hour\", \"day\", \"week\", \"month\", \"year\"],\n description: \"Time filter. Default: all time.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"search_reddit\", {\n query: params.query,\n subreddit: params.subreddit,\n sort: params.sort,\n time: params.time,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_fetch_post\",\n description:\n \"Fetch a single Reddit post and its full comment tree. Provide the full post URL.\",\n parameters: Type.Object({\n url: Type.String({\n description:\n \"Full Reddit post URL (e.g. https://www.reddit.com/r/supplements/comments/abc123/some_post/).\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_post\", {\n url: params.url,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_reply\",\n description:\n \"Reply to a Reddit comment or post using the user's logged-in session in Chrome. \" +\n \"Requires the Reddit Agent Chrome extension to be connected and the user to be \" +\n \"logged into Reddit. Takes ~7-10 seconds due to DOM interaction.\",\n parameters: Type.Object({\n commentUrl: Type.String({\n description: \"Direct URL to the comment or post to reply to.\",\n }),\n replyText: Type.String({\n description: \"The reply text to post.\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\n \"reply_to_comment\",\n {\n commentUrl: params.commentUrl,\n replyText: params.replyText,\n },\n REPLY_TIMEOUT_MS\n );\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n}\n","import { WebSocketBridge } from \"./src/ws-bridge.js\";\nimport { registerRedditTools } from \"./src/tools.js\";\n\nconst DEFAULT_PORT = 7071;\n\ninterface PluginLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n}\n\ninterface PluginApi {\n pluginConfig: Record<string, unknown> | undefined;\n logger: PluginLogger;\n registerTool: (config: unknown) => void;\n registerService: (config: {\n id: string;\n start: () => void | Promise<void>;\n stop: () => void | Promise<void>;\n }) => void;\n}\n\nconst redditAgentPlugin = {\n id: \"reddit-agent\",\n name: \"Reddit Agent\",\n description:\n \"Bridge to Reddit Chrome extension for browsing, searching, and commenting.\",\n\n register(api: PluginApi) {\n const port =\n typeof api.pluginConfig?.port === \"number\"\n ? api.pluginConfig.port\n : DEFAULT_PORT;\n\n const bridge = new WebSocketBridge(port, {\n info: (msg) => api.logger.info(msg),\n warn: (msg) => api.logger.warn(msg),\n error: (msg) => api.logger.error(msg),\n });\n\n api.registerService({\n id: \"reddit-agent-bridge\",\n start: () => {\n bridge.start();\n },\n stop: () => {\n bridge.stop();\n },\n });\n\n registerRedditTools(\n (config) => api.registerTool(config),\n bridge\n );\n },\n};\n\nexport default redditAgentPlugin;\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "reddit-agent",
|
|
3
|
+
"name": "Reddit Agent",
|
|
4
|
+
"description": "Bridge to Reddit Chrome extension for browsing, searching, and commenting on Reddit.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"port": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"minimum": 1,
|
|
12
|
+
"maximum": 65535,
|
|
13
|
+
"default": 7071
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"uiHints": {
|
|
18
|
+
"port": {
|
|
19
|
+
"label": "WebSocket Port",
|
|
20
|
+
"placeholder": "7071",
|
|
21
|
+
"help": "Port for the WebSocket bridge server. The Chrome extension connects here."
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"skills": ["skills/reddit-agent"]
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-reddit-agent-server",
|
|
3
|
+
"version": "2026.2.15",
|
|
4
|
+
"description": "OpenClaw plugin for Reddit automation via Chrome extension bridge",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"openclaw.plugin.json",
|
|
11
|
+
"skills",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsup --watch",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@sinclair/typebox": "^0.34.0",
|
|
21
|
+
"ws": "^8.19.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/ws": "^8.5.0",
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"typescript": "~5.8.0"
|
|
27
|
+
},
|
|
28
|
+
"openclaw": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./dist/index.js"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"openclaw",
|
|
35
|
+
"openclaw-plugin",
|
|
36
|
+
"reddit",
|
|
37
|
+
"automation"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reddit-agent
|
|
3
|
+
description: Browse, search, and interact with Reddit via the Chrome extension bridge.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Reddit Agent
|
|
8
|
+
|
|
9
|
+
You have access to Reddit tools via the Reddit Agent plugin. These tools communicate with a Chrome extension that executes actions in a real browser session.
|
|
10
|
+
|
|
11
|
+
## Available Tools
|
|
12
|
+
|
|
13
|
+
### reddit_fetch_subreddit
|
|
14
|
+
Fetch a subreddit listing. Provide a subreddit name (e.g., "supplements", "r/supplements", or a full URL) and optional sort order (hot, new, top, rising, best).
|
|
15
|
+
|
|
16
|
+
### reddit_search
|
|
17
|
+
Search Reddit for posts. Provide a query string and optionally scope to a subreddit. Supports sort (relevance, top, new, comments) and time filters (hour, day, week, month, year).
|
|
18
|
+
|
|
19
|
+
### reddit_fetch_post
|
|
20
|
+
Fetch a single post and its comment tree. Provide the full Reddit post URL.
|
|
21
|
+
|
|
22
|
+
### reddit_reply
|
|
23
|
+
Reply to a comment or post. Requires the user to be logged into Reddit in Chrome. Provide the comment URL and reply text. This action takes ~7-10 seconds.
|
|
24
|
+
|
|
25
|
+
## Important Notes
|
|
26
|
+
|
|
27
|
+
- The Reddit Agent Chrome extension must be installed and connected
|
|
28
|
+
- The extension connects to the bridge server automatically at `ws://localhost:7071/ws`
|
|
29
|
+
- Reply actions require an active Reddit login session in Chrome
|
|
30
|
+
- Reddit rate limits apply — avoid rapid-fire requests
|
|
31
|
+
- If tools return a "not connected" error, check that Chrome is running and the extension is enabled
|