openclaw-reddit-agent-server 2026.2.152 → 2026.2.153
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 +393 -22
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +25 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,32 +1,130 @@
|
|
|
1
1
|
// src/ws-bridge.ts
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { randomInt, randomUUID } from "crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
4
7
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
8
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
9
|
+
var DEFAULT_BIND_HOST = "0.0.0.0";
|
|
10
|
+
var DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1e3;
|
|
11
|
+
var DEFAULT_PAIRING_STORE_PATH = path.join(
|
|
12
|
+
homedir(),
|
|
13
|
+
".openclaw",
|
|
14
|
+
"reddit-agent-pairings.json"
|
|
15
|
+
);
|
|
16
|
+
var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
17
|
+
var PAIRING_CODE_LENGTH = 6;
|
|
5
18
|
var WebSocketBridge = class {
|
|
6
19
|
wss = null;
|
|
7
20
|
extensionSocket = null;
|
|
8
21
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
22
|
+
pendingPairings = /* @__PURE__ */ new Map();
|
|
23
|
+
pendingPairingCodeBySocket = /* @__PURE__ */ new Map();
|
|
24
|
+
pairedDevices = /* @__PURE__ */ new Map();
|
|
9
25
|
port;
|
|
26
|
+
host;
|
|
10
27
|
logger;
|
|
11
|
-
|
|
28
|
+
pairingStorePath;
|
|
29
|
+
pairingCodeTtlMs;
|
|
30
|
+
idleTimeoutMs;
|
|
31
|
+
onIdleShutdown;
|
|
32
|
+
idleTimer = null;
|
|
33
|
+
constructor(port, options) {
|
|
12
34
|
this.port = port;
|
|
13
|
-
this.
|
|
35
|
+
this.host = options?.host?.trim() || DEFAULT_BIND_HOST;
|
|
36
|
+
this.logger = options?.logger ?? {
|
|
14
37
|
info: (msg) => console.log(`[openclaw-reddit-agent-server] ${msg}`),
|
|
15
38
|
warn: (msg) => console.warn(`[openclaw-reddit-agent-server] ${msg}`),
|
|
16
39
|
error: (msg) => console.error(`[openclaw-reddit-agent-server] ${msg}`)
|
|
17
40
|
};
|
|
41
|
+
this.pairingStorePath = options?.pairingStorePath?.trim() || DEFAULT_PAIRING_STORE_PATH;
|
|
42
|
+
this.pairingCodeTtlMs = options?.pairingCodeTtlMs ?? DEFAULT_PAIRING_TTL_MS;
|
|
43
|
+
this.idleTimeoutMs = options?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
44
|
+
this.onIdleShutdown = options?.onIdleShutdown;
|
|
45
|
+
this.loadPairedDevices();
|
|
18
46
|
}
|
|
19
47
|
get isExtensionConnected() {
|
|
20
48
|
return this.extensionSocket !== null && this.extensionSocket.readyState === WebSocket.OPEN;
|
|
21
49
|
}
|
|
50
|
+
getPendingPairings() {
|
|
51
|
+
this.pruneExpiredPairings();
|
|
52
|
+
return Array.from(this.pendingPairings.values()).sort((a, b) => b.requestedAt - a.requestedAt).map((entry) => ({
|
|
53
|
+
code: entry.code,
|
|
54
|
+
deviceId: entry.deviceId,
|
|
55
|
+
deviceName: entry.deviceName,
|
|
56
|
+
requestedAt: new Date(entry.requestedAt).toISOString(),
|
|
57
|
+
expiresAt: new Date(entry.expiresAt).toISOString()
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
getPairedDevices() {
|
|
61
|
+
return Array.from(this.pairedDevices.values()).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt)).map((device) => ({
|
|
62
|
+
deviceId: device.deviceId,
|
|
63
|
+
deviceName: device.deviceName,
|
|
64
|
+
approvedAt: device.approvedAt,
|
|
65
|
+
lastSeenAt: device.lastSeenAt
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
approvePairing(codeInput) {
|
|
69
|
+
const code = this.normalizePairingCode(codeInput);
|
|
70
|
+
if (!code) {
|
|
71
|
+
throw new Error("Missing pairing code.");
|
|
72
|
+
}
|
|
73
|
+
this.pruneExpiredPairings();
|
|
74
|
+
const pairing = this.pendingPairings.get(code);
|
|
75
|
+
if (!pairing) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Pairing code "${code}" is invalid or expired. Request a new code in the extension.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (pairing.socket.readyState !== WebSocket.OPEN) {
|
|
81
|
+
this.deletePendingPairing(code);
|
|
82
|
+
throw new Error("Pairing request is no longer active. Request a new code.");
|
|
83
|
+
}
|
|
84
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
85
|
+
const authToken = randomUUID();
|
|
86
|
+
const paired = {
|
|
87
|
+
deviceId: pairing.deviceId,
|
|
88
|
+
deviceName: pairing.deviceName,
|
|
89
|
+
authToken,
|
|
90
|
+
approvedAt: now,
|
|
91
|
+
lastSeenAt: now
|
|
92
|
+
};
|
|
93
|
+
this.pairedDevices.set(paired.deviceId, paired);
|
|
94
|
+
this.persistPairedDevices();
|
|
95
|
+
this.sendJson(pairing.socket, {
|
|
96
|
+
type: "pairing_approved",
|
|
97
|
+
deviceId: paired.deviceId,
|
|
98
|
+
deviceName: paired.deviceName,
|
|
99
|
+
authToken: paired.authToken
|
|
100
|
+
});
|
|
101
|
+
this.deletePendingPairing(code);
|
|
102
|
+
this.markSocketAsExtension(pairing.socket, paired);
|
|
103
|
+
this.logger.info(
|
|
104
|
+
`Approved extension pairing for "${paired.deviceName}" (${paired.deviceId})`
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
deviceId: paired.deviceId,
|
|
108
|
+
deviceName: paired.deviceName,
|
|
109
|
+
approvedAt: paired.approvedAt,
|
|
110
|
+
lastSeenAt: paired.lastSeenAt
|
|
111
|
+
};
|
|
112
|
+
}
|
|
22
113
|
start() {
|
|
23
114
|
if (this.wss) return;
|
|
24
|
-
this.
|
|
115
|
+
this.resetIdleTimer();
|
|
116
|
+
this.wss = new WebSocketServer({
|
|
117
|
+
port: this.port,
|
|
118
|
+
host: this.host,
|
|
119
|
+
path: "/ws"
|
|
120
|
+
});
|
|
25
121
|
this.logger.info(
|
|
26
|
-
`WebSocket bridge listening on ws
|
|
122
|
+
`WebSocket bridge listening on ws://${this.host}:${this.port}/ws`
|
|
27
123
|
);
|
|
28
|
-
this.wss.on("connection", (socket) => {
|
|
29
|
-
this.logger.info(
|
|
124
|
+
this.wss.on("connection", (socket, request) => {
|
|
125
|
+
this.logger.info(
|
|
126
|
+
`New WebSocket connection from ${request.socket.remoteAddress ?? "unknown"}`
|
|
127
|
+
);
|
|
30
128
|
socket.on("message", (raw) => {
|
|
31
129
|
let message;
|
|
32
130
|
try {
|
|
@@ -37,18 +135,21 @@ var WebSocketBridge = class {
|
|
|
37
135
|
this.logger.warn("Failed to parse incoming WebSocket message");
|
|
38
136
|
return;
|
|
39
137
|
}
|
|
40
|
-
if ("type" in message && message.type === "
|
|
41
|
-
this.
|
|
42
|
-
`Extension identified (role: ${message.role})`
|
|
43
|
-
);
|
|
44
|
-
this.extensionSocket = socket;
|
|
138
|
+
if ("type" in message && message.type === "ping") {
|
|
139
|
+
this.sendJson(socket, { type: "pong" });
|
|
45
140
|
return;
|
|
46
141
|
}
|
|
47
|
-
if ("type" in message && message.type === "
|
|
48
|
-
|
|
142
|
+
if ("type" in message && message.type === "identify") {
|
|
143
|
+
this.handleIdentify(socket, message);
|
|
49
144
|
return;
|
|
50
145
|
}
|
|
51
146
|
if ("id" in message && "success" in message) {
|
|
147
|
+
if (socket !== this.extensionSocket) {
|
|
148
|
+
this.logger.warn(
|
|
149
|
+
`Ignoring response from unauthenticated socket id: ${message.id}`
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
52
153
|
const response = message;
|
|
53
154
|
const pending = this.pendingRequests.get(response.id);
|
|
54
155
|
if (!pending) {
|
|
@@ -59,6 +160,7 @@ var WebSocketBridge = class {
|
|
|
59
160
|
}
|
|
60
161
|
this.pendingRequests.delete(response.id);
|
|
61
162
|
clearTimeout(pending.timer);
|
|
163
|
+
this.resetIdleTimer();
|
|
62
164
|
if (response.success) {
|
|
63
165
|
pending.resolve(response.data);
|
|
64
166
|
} else {
|
|
@@ -67,6 +169,10 @@ var WebSocketBridge = class {
|
|
|
67
169
|
}
|
|
68
170
|
});
|
|
69
171
|
socket.on("close", () => {
|
|
172
|
+
const code = this.pendingPairingCodeBySocket.get(socket);
|
|
173
|
+
if (code) {
|
|
174
|
+
this.deletePendingPairing(code);
|
|
175
|
+
}
|
|
70
176
|
if (socket === this.extensionSocket) {
|
|
71
177
|
this.logger.info("Extension disconnected");
|
|
72
178
|
this.extensionSocket = null;
|
|
@@ -78,11 +184,17 @@ var WebSocketBridge = class {
|
|
|
78
184
|
});
|
|
79
185
|
}
|
|
80
186
|
stop() {
|
|
187
|
+
if (this.idleTimer) {
|
|
188
|
+
clearTimeout(this.idleTimer);
|
|
189
|
+
this.idleTimer = null;
|
|
190
|
+
}
|
|
81
191
|
for (const [id, pending] of this.pendingRequests) {
|
|
82
192
|
clearTimeout(pending.timer);
|
|
83
193
|
pending.reject(new Error("WebSocket bridge shutting down"));
|
|
84
194
|
this.pendingRequests.delete(id);
|
|
85
195
|
}
|
|
196
|
+
this.pendingPairings.clear();
|
|
197
|
+
this.pendingPairingCodeBySocket.clear();
|
|
86
198
|
if (this.extensionSocket) {
|
|
87
199
|
this.extensionSocket.close();
|
|
88
200
|
this.extensionSocket = null;
|
|
@@ -99,12 +211,15 @@ var WebSocketBridge = class {
|
|
|
99
211
|
*/
|
|
100
212
|
sendAction(action, params, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
101
213
|
if (!this.isExtensionConnected) {
|
|
214
|
+
const pendingCodes = this.getPendingPairings().map((pairing) => pairing.code);
|
|
215
|
+
const pairingHint = pendingCodes.length > 0 ? `Pending pairing code(s): ${pendingCodes.join(", ")}. Approve one with reddit_approve_pairing.` : "Open the extension popup and request pairing, then approve with reddit_approve_pairing.";
|
|
102
216
|
return Promise.reject(
|
|
103
217
|
new Error(
|
|
104
|
-
`Reddit Agent Chrome extension is not connected.
|
|
218
|
+
`Reddit Agent Chrome extension is not connected. Ensure the extension points to ws://<your-openclaw-host>:${this.port}/ws and is approved. ` + pairingHint
|
|
105
219
|
)
|
|
106
220
|
);
|
|
107
221
|
}
|
|
222
|
+
this.resetIdleTimer();
|
|
108
223
|
const id = randomUUID();
|
|
109
224
|
const request = { id, action, params };
|
|
110
225
|
return new Promise((resolve, reject) => {
|
|
@@ -120,12 +235,226 @@ var WebSocketBridge = class {
|
|
|
120
235
|
this.extensionSocket.send(JSON.stringify(request));
|
|
121
236
|
});
|
|
122
237
|
}
|
|
238
|
+
resetIdleTimer() {
|
|
239
|
+
if (this.idleTimeoutMs <= 0) return;
|
|
240
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
241
|
+
this.idleTimer = setTimeout(() => {
|
|
242
|
+
this.idleTimer = null;
|
|
243
|
+
this.logger.info(
|
|
244
|
+
`No activity for ${this.idleTimeoutMs / 1e3}s \u2014 idle shutdown`
|
|
245
|
+
);
|
|
246
|
+
this.stop();
|
|
247
|
+
this.onIdleShutdown?.();
|
|
248
|
+
}, this.idleTimeoutMs);
|
|
249
|
+
}
|
|
250
|
+
handleIdentify(socket, message) {
|
|
251
|
+
if (message.role !== "extension") {
|
|
252
|
+
this.logger.warn(`Ignoring identify with unsupported role: ${message.role}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const deviceId = message.deviceId?.trim();
|
|
256
|
+
if (!deviceId) {
|
|
257
|
+
this.logger.warn("Identify message missing deviceId");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const deviceName = this.sanitizeDeviceName(message.deviceName);
|
|
261
|
+
const paired = this.pairedDevices.get(deviceId);
|
|
262
|
+
const providedToken = message.authToken?.trim();
|
|
263
|
+
if (paired && providedToken && providedToken === paired.authToken) {
|
|
264
|
+
this.deletePendingPairingBySocket(socket);
|
|
265
|
+
this.markSocketAsExtension(socket, {
|
|
266
|
+
...paired,
|
|
267
|
+
deviceName
|
|
268
|
+
});
|
|
269
|
+
this.logger.info(`Extension authenticated (${deviceName}, ${deviceId})`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const pairing = this.createOrRefreshPairing(socket, deviceId, deviceName);
|
|
273
|
+
this.logger.info(
|
|
274
|
+
`Pairing required for "${pairing.deviceName}" (${pairing.deviceId}). Code: ${pairing.code}`
|
|
275
|
+
);
|
|
276
|
+
this.sendJson(socket, {
|
|
277
|
+
type: "pairing_required",
|
|
278
|
+
code: pairing.code,
|
|
279
|
+
deviceId: pairing.deviceId,
|
|
280
|
+
deviceName: pairing.deviceName,
|
|
281
|
+
requestedAt: new Date(pairing.requestedAt).toISOString(),
|
|
282
|
+
expiresAt: new Date(pairing.expiresAt).toISOString()
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
markSocketAsExtension(socket, paired) {
|
|
286
|
+
if (this.extensionSocket && this.extensionSocket !== socket) {
|
|
287
|
+
this.extensionSocket.close();
|
|
288
|
+
}
|
|
289
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
290
|
+
const updated = {
|
|
291
|
+
...paired,
|
|
292
|
+
lastSeenAt: now
|
|
293
|
+
};
|
|
294
|
+
this.pairedDevices.set(updated.deviceId, updated);
|
|
295
|
+
this.persistPairedDevices();
|
|
296
|
+
this.extensionSocket = socket;
|
|
297
|
+
this.resetIdleTimer();
|
|
298
|
+
this.sendJson(socket, {
|
|
299
|
+
type: "identified",
|
|
300
|
+
deviceId: updated.deviceId,
|
|
301
|
+
deviceName: updated.deviceName
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
createOrRefreshPairing(socket, deviceId, deviceName) {
|
|
305
|
+
this.pruneExpiredPairings();
|
|
306
|
+
this.deletePendingPairingBySocket(socket);
|
|
307
|
+
for (const pairing2 of this.pendingPairings.values()) {
|
|
308
|
+
if (pairing2.deviceId === deviceId && pairing2.socket.readyState === WebSocket.OPEN) {
|
|
309
|
+
this.pendingPairingCodeBySocket.delete(pairing2.socket);
|
|
310
|
+
this.pendingPairingCodeBySocket.set(socket, pairing2.code);
|
|
311
|
+
pairing2.socket = socket;
|
|
312
|
+
pairing2.deviceName = deviceName;
|
|
313
|
+
pairing2.requestedAt = Date.now();
|
|
314
|
+
pairing2.expiresAt = pairing2.requestedAt + this.pairingCodeTtlMs;
|
|
315
|
+
return pairing2;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let code = this.generatePairingCode();
|
|
319
|
+
while (this.pendingPairings.has(code)) {
|
|
320
|
+
code = this.generatePairingCode();
|
|
321
|
+
}
|
|
322
|
+
const requestedAt = Date.now();
|
|
323
|
+
const pairing = {
|
|
324
|
+
code,
|
|
325
|
+
deviceId,
|
|
326
|
+
deviceName,
|
|
327
|
+
requestedAt,
|
|
328
|
+
expiresAt: requestedAt + this.pairingCodeTtlMs,
|
|
329
|
+
socket
|
|
330
|
+
};
|
|
331
|
+
this.pendingPairings.set(code, pairing);
|
|
332
|
+
this.pendingPairingCodeBySocket.set(socket, code);
|
|
333
|
+
return pairing;
|
|
334
|
+
}
|
|
335
|
+
pruneExpiredPairings() {
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
for (const [code, pairing] of this.pendingPairings) {
|
|
338
|
+
if (pairing.expiresAt <= now || pairing.socket.readyState !== WebSocket.OPEN) {
|
|
339
|
+
this.deletePendingPairing(code);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
deletePendingPairing(code) {
|
|
344
|
+
const pairing = this.pendingPairings.get(code);
|
|
345
|
+
if (!pairing) return;
|
|
346
|
+
this.pendingPairings.delete(code);
|
|
347
|
+
this.pendingPairingCodeBySocket.delete(pairing.socket);
|
|
348
|
+
}
|
|
349
|
+
deletePendingPairingBySocket(socket) {
|
|
350
|
+
const code = this.pendingPairingCodeBySocket.get(socket);
|
|
351
|
+
if (!code) return;
|
|
352
|
+
this.deletePendingPairing(code);
|
|
353
|
+
}
|
|
354
|
+
normalizePairingCode(input) {
|
|
355
|
+
return input.trim().toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
356
|
+
}
|
|
357
|
+
generatePairingCode() {
|
|
358
|
+
let code = "";
|
|
359
|
+
for (let index = 0; index < PAIRING_CODE_LENGTH; index += 1) {
|
|
360
|
+
const pos = randomInt(0, PAIRING_CODE_ALPHABET.length);
|
|
361
|
+
code += PAIRING_CODE_ALPHABET[pos];
|
|
362
|
+
}
|
|
363
|
+
return code;
|
|
364
|
+
}
|
|
365
|
+
sanitizeDeviceName(raw) {
|
|
366
|
+
const value = raw?.trim();
|
|
367
|
+
return value ? value.slice(0, 120) : "Reddit Agent Extension";
|
|
368
|
+
}
|
|
369
|
+
loadPairedDevices() {
|
|
370
|
+
try {
|
|
371
|
+
if (!existsSync(this.pairingStorePath)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const raw = readFileSync(this.pairingStorePath, "utf8");
|
|
375
|
+
const parsed = JSON.parse(raw);
|
|
376
|
+
const devices = Array.isArray(parsed.devices) ? parsed.devices : [];
|
|
377
|
+
for (const device of devices) {
|
|
378
|
+
if (typeof device.deviceId === "string" && typeof device.deviceName === "string" && typeof device.authToken === "string" && typeof device.approvedAt === "string" && typeof device.lastSeenAt === "string") {
|
|
379
|
+
this.pairedDevices.set(device.deviceId, {
|
|
380
|
+
deviceId: device.deviceId,
|
|
381
|
+
deviceName: device.deviceName,
|
|
382
|
+
authToken: device.authToken,
|
|
383
|
+
approvedAt: device.approvedAt,
|
|
384
|
+
lastSeenAt: device.lastSeenAt
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
390
|
+
this.logger.warn(
|
|
391
|
+
`Failed to load pairing store at ${this.pairingStorePath}: ${message}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
persistPairedDevices() {
|
|
396
|
+
try {
|
|
397
|
+
mkdirSync(path.dirname(this.pairingStorePath), { recursive: true });
|
|
398
|
+
const payload = {
|
|
399
|
+
version: 1,
|
|
400
|
+
devices: Array.from(this.pairedDevices.values())
|
|
401
|
+
};
|
|
402
|
+
writeFileSync(this.pairingStorePath, JSON.stringify(payload, null, 2));
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
405
|
+
this.logger.error(
|
|
406
|
+
`Failed to persist pairing store at ${this.pairingStorePath}: ${message}`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
sendJson(socket, payload) {
|
|
411
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
412
|
+
socket.send(JSON.stringify(payload));
|
|
413
|
+
}
|
|
123
414
|
};
|
|
124
415
|
|
|
125
416
|
// src/tools.ts
|
|
126
417
|
import { Type } from "@sinclair/typebox";
|
|
127
418
|
var REPLY_TIMEOUT_MS = 9e4;
|
|
128
419
|
function registerRedditTools(registerTool, bridge) {
|
|
420
|
+
registerTool({
|
|
421
|
+
name: "reddit_list_pairing_requests",
|
|
422
|
+
description: "List pending browser-extension pairing requests that need user approval.",
|
|
423
|
+
parameters: Type.Object({}),
|
|
424
|
+
async execute() {
|
|
425
|
+
const data = bridge.getPendingPairings();
|
|
426
|
+
return {
|
|
427
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
registerTool({
|
|
432
|
+
name: "reddit_approve_pairing",
|
|
433
|
+
description: "Approve a pending browser-extension pairing request by its pairing code.",
|
|
434
|
+
parameters: Type.Object({
|
|
435
|
+
code: Type.String({
|
|
436
|
+
description: "Pairing code shown in the extension popup (for example: ABC123)."
|
|
437
|
+
})
|
|
438
|
+
}),
|
|
439
|
+
async execute(_id, params) {
|
|
440
|
+
const code = String(params.code ?? "").trim();
|
|
441
|
+
const data = bridge.approvePairing(code);
|
|
442
|
+
return {
|
|
443
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
registerTool({
|
|
448
|
+
name: "reddit_list_paired_devices",
|
|
449
|
+
description: "List previously approved extension devices that can auto-connect.",
|
|
450
|
+
parameters: Type.Object({}),
|
|
451
|
+
async execute() {
|
|
452
|
+
const data = bridge.getPairedDevices();
|
|
453
|
+
return {
|
|
454
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
});
|
|
129
458
|
registerTool({
|
|
130
459
|
name: "reddit_fetch_subreddit",
|
|
131
460
|
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).",
|
|
@@ -188,6 +517,39 @@ function registerRedditTools(registerTool, bridge) {
|
|
|
188
517
|
};
|
|
189
518
|
}
|
|
190
519
|
});
|
|
520
|
+
registerTool({
|
|
521
|
+
name: "reddit_fetch_user_posts",
|
|
522
|
+
description: "Fetch posts submitted by a Reddit user. Provide the username. Supports sort (hot, new, top, controversial) and time filters (hour, day, week, month, year).",
|
|
523
|
+
parameters: Type.Object({
|
|
524
|
+
username: Type.String({
|
|
525
|
+
description: 'Reddit username (e.g. "richo-s"). Do not include u/ or /u/ prefix.'
|
|
526
|
+
}),
|
|
527
|
+
sort: Type.Optional(
|
|
528
|
+
Type.Unsafe({
|
|
529
|
+
type: "string",
|
|
530
|
+
enum: ["hot", "new", "top", "controversial"],
|
|
531
|
+
description: "Sort order. Default: hot."
|
|
532
|
+
})
|
|
533
|
+
),
|
|
534
|
+
time: Type.Optional(
|
|
535
|
+
Type.Unsafe({
|
|
536
|
+
type: "string",
|
|
537
|
+
enum: ["hour", "day", "week", "month", "year"],
|
|
538
|
+
description: "Time filter (applies when sort is top or controversial). Default: all time."
|
|
539
|
+
})
|
|
540
|
+
)
|
|
541
|
+
}),
|
|
542
|
+
async execute(_id, params) {
|
|
543
|
+
const data = await bridge.sendAction("fetch_user_posts", {
|
|
544
|
+
username: params.username,
|
|
545
|
+
sort: params.sort,
|
|
546
|
+
time: params.time
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
});
|
|
191
553
|
registerTool({
|
|
192
554
|
name: "reddit_fetch_post",
|
|
193
555
|
description: "Fetch a single Reddit post and its full comment tree. Provide the full post URL.",
|
|
@@ -233,17 +595,26 @@ function registerRedditTools(registerTool, bridge) {
|
|
|
233
595
|
}
|
|
234
596
|
|
|
235
597
|
// index.ts
|
|
236
|
-
var
|
|
598
|
+
var BRIDGE_PORT = 7071;
|
|
599
|
+
var DEFAULT_HOST = "0.0.0.0";
|
|
600
|
+
var DEFAULT_PAIRING_CODE_TTL_SECONDS = 600;
|
|
237
601
|
var redditAgentPlugin = {
|
|
238
602
|
id: "openclaw-reddit-agent-server",
|
|
239
603
|
name: "OpenClaw Reddit Agent",
|
|
240
604
|
description: "Bridge to Reddit Chrome extension for browsing, searching, and commenting.",
|
|
241
605
|
register(api) {
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
606
|
+
const host = typeof api.pluginConfig?.host === "string" && api.pluginConfig.host.trim().length > 0 ? api.pluginConfig.host.trim() : DEFAULT_HOST;
|
|
607
|
+
const pairingCodeTtlSeconds = typeof api.pluginConfig?.pairingCodeTtlSeconds === "number" && api.pluginConfig.pairingCodeTtlSeconds > 0 ? api.pluginConfig.pairingCodeTtlSeconds : DEFAULT_PAIRING_CODE_TTL_SECONDS;
|
|
608
|
+
const pairingStorePath = typeof api.pluginConfig?.pairingStorePath === "string" && api.pluginConfig.pairingStorePath.trim().length > 0 ? api.pluginConfig.pairingStorePath.trim() : void 0;
|
|
609
|
+
const bridge = new WebSocketBridge(BRIDGE_PORT, {
|
|
610
|
+
logger: {
|
|
611
|
+
info: (msg) => api.logger.info(msg),
|
|
612
|
+
warn: (msg) => api.logger.warn(msg),
|
|
613
|
+
error: (msg) => api.logger.error(msg)
|
|
614
|
+
},
|
|
615
|
+
host,
|
|
616
|
+
pairingCodeTtlMs: pairingCodeTtlSeconds * 1e3,
|
|
617
|
+
pairingStorePath
|
|
247
618
|
});
|
|
248
619
|
api.registerService({
|
|
249
620
|
id: "reddit-agent-bridge",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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(`[openclaw-reddit-agent-server] ${msg}`),\n warn: (msg: string) => console.warn(`[openclaw-reddit-agent-server] ${msg}`),\n error: (msg: string) => console.error(`[openclaw-reddit-agent-server] ${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: \"openclaw-reddit-agent-server\",\n name: \"OpenClaw 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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/ws-bridge.ts","../src/tools.ts","../index.ts"],"sourcesContent":["import { randomInt, randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport type {\n BridgeRequest,\n BridgeResponse,\n ExtensionMessage,\n PairedDevice,\n PendingPairing,\n PendingRequest,\n} from \"./types.js\";\n\nconst REQUEST_TIMEOUT_MS = 60_000;\nconst DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\nconst DEFAULT_BIND_HOST = \"0.0.0.0\";\nconst DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1000; // 10 minutes\nconst DEFAULT_PAIRING_STORE_PATH = path.join(\n homedir(),\n \".openclaw\",\n \"reddit-agent-pairings.json\"\n);\nconst PAIRING_CODE_ALPHABET = \"ABCDEFGHJKLMNPQRSTUVWXYZ23456789\";\nconst PAIRING_CODE_LENGTH = 6;\n\ninterface PairedDeviceRecord {\n deviceId: string;\n deviceName: string;\n authToken: string;\n approvedAt: string;\n lastSeenAt: string;\n}\n\ninterface PairingStoreFile {\n version: 1;\n devices: PairedDeviceRecord[];\n}\n\ninterface PendingPairingRecord {\n code: string;\n deviceId: string;\n deviceName: string;\n requestedAt: number;\n expiresAt: number;\n socket: WebSocket;\n}\n\nexport interface BridgeLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n}\n\nexport interface BridgeOptions {\n logger?: BridgeLogger;\n host?: string;\n pairingStorePath?: string;\n pairingCodeTtlMs?: number;\n /** Shut down after this many ms of inactivity (0 to disable). Defaults to 5 minutes. */\n idleTimeoutMs?: number;\n /** Called after the bridge stops itself due to inactivity. */\n onIdleShutdown?: () => 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 pendingPairings = new Map<string, PendingPairingRecord>();\n private pendingPairingCodeBySocket = new Map<WebSocket, string>();\n private pairedDevices = new Map<string, PairedDeviceRecord>();\n private port: number;\n private host: string;\n private logger: BridgeLogger;\n private pairingStorePath: string;\n private pairingCodeTtlMs: number;\n private idleTimeoutMs: number;\n private onIdleShutdown?: () => void;\n private idleTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(port: number, options?: BridgeOptions) {\n this.port = port;\n this.host = options?.host?.trim() || DEFAULT_BIND_HOST;\n this.logger = options?.logger ?? {\n info: (msg: string) => console.log(`[openclaw-reddit-agent-server] ${msg}`),\n warn: (msg: string) => console.warn(`[openclaw-reddit-agent-server] ${msg}`),\n error: (msg: string) =>\n console.error(`[openclaw-reddit-agent-server] ${msg}`),\n };\n this.pairingStorePath =\n options?.pairingStorePath?.trim() || DEFAULT_PAIRING_STORE_PATH;\n this.pairingCodeTtlMs = options?.pairingCodeTtlMs ?? DEFAULT_PAIRING_TTL_MS;\n this.idleTimeoutMs = options?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;\n this.onIdleShutdown = options?.onIdleShutdown;\n\n this.loadPairedDevices();\n }\n\n get isExtensionConnected(): boolean {\n return (\n this.extensionSocket !== null &&\n this.extensionSocket.readyState === WebSocket.OPEN\n );\n }\n\n getPendingPairings(): PendingPairing[] {\n this.pruneExpiredPairings();\n return Array.from(this.pendingPairings.values())\n .sort((a, b) => b.requestedAt - a.requestedAt)\n .map((entry) => ({\n code: entry.code,\n deviceId: entry.deviceId,\n deviceName: entry.deviceName,\n requestedAt: new Date(entry.requestedAt).toISOString(),\n expiresAt: new Date(entry.expiresAt).toISOString(),\n }));\n }\n\n getPairedDevices(): PairedDevice[] {\n return Array.from(this.pairedDevices.values())\n .sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))\n .map((device) => ({\n deviceId: device.deviceId,\n deviceName: device.deviceName,\n approvedAt: device.approvedAt,\n lastSeenAt: device.lastSeenAt,\n }));\n }\n\n approvePairing(codeInput: string): PairedDevice {\n const code = this.normalizePairingCode(codeInput);\n if (!code) {\n throw new Error(\"Missing pairing code.\");\n }\n\n this.pruneExpiredPairings();\n const pairing = this.pendingPairings.get(code);\n if (!pairing) {\n throw new Error(\n `Pairing code \"${code}\" is invalid or expired. Request a new code in the extension.`\n );\n }\n\n if (pairing.socket.readyState !== WebSocket.OPEN) {\n this.deletePendingPairing(code);\n throw new Error(\"Pairing request is no longer active. Request a new code.\");\n }\n\n const now = new Date().toISOString();\n const authToken = randomUUID();\n const paired: PairedDeviceRecord = {\n deviceId: pairing.deviceId,\n deviceName: pairing.deviceName,\n authToken,\n approvedAt: now,\n lastSeenAt: now,\n };\n\n this.pairedDevices.set(paired.deviceId, paired);\n this.persistPairedDevices();\n\n this.sendJson(pairing.socket, {\n type: \"pairing_approved\",\n deviceId: paired.deviceId,\n deviceName: paired.deviceName,\n authToken: paired.authToken,\n });\n\n this.deletePendingPairing(code);\n this.markSocketAsExtension(pairing.socket, paired);\n\n this.logger.info(\n `Approved extension pairing for \"${paired.deviceName}\" (${paired.deviceId})`\n );\n return {\n deviceId: paired.deviceId,\n deviceName: paired.deviceName,\n approvedAt: paired.approvedAt,\n lastSeenAt: paired.lastSeenAt,\n };\n }\n\n start(): void {\n if (this.wss) return;\n\n this.resetIdleTimer();\n this.wss = new WebSocketServer({\n port: this.port,\n host: this.host,\n path: \"/ws\",\n });\n this.logger.info(\n `WebSocket bridge listening on ws://${this.host}:${this.port}/ws`\n );\n\n this.wss.on(\"connection\", (socket, request) => {\n this.logger.info(\n `New WebSocket connection from ${request.socket.remoteAddress ?? \"unknown\"}`\n );\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 ) as ExtensionMessage;\n } catch {\n this.logger.warn(\"Failed to parse incoming WebSocket message\");\n return;\n }\n\n if (\"type\" in message && message.type === \"ping\") {\n this.sendJson(socket, { type: \"pong\" });\n return;\n }\n\n if (\"type\" in message && message.type === \"identify\") {\n this.handleIdentify(socket, message);\n return;\n }\n\n if (\"id\" in message && \"success\" in message) {\n if (socket !== this.extensionSocket) {\n this.logger.warn(\n `Ignoring response from unauthenticated socket id: ${message.id}`\n );\n return;\n }\n\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\n this.pendingRequests.delete(response.id);\n clearTimeout(pending.timer);\n this.resetIdleTimer();\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 const code = this.pendingPairingCodeBySocket.get(socket);\n if (code) {\n this.deletePendingPairing(code);\n }\n\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 if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n\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 this.pendingPairings.clear();\n this.pendingPairingCodeBySocket.clear();\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 const pendingCodes = this.getPendingPairings().map((pairing) => pairing.code);\n const pairingHint =\n pendingCodes.length > 0\n ? `Pending pairing code(s): ${pendingCodes.join(\", \")}. Approve one with reddit_approve_pairing.`\n : \"Open the extension popup and request pairing, then approve with reddit_approve_pairing.\";\n return Promise.reject(\n new Error(\n \"Reddit Agent Chrome extension is not connected. \" +\n `Ensure the extension points to ws://<your-openclaw-host>:${this.port}/ws and is approved. ` +\n pairingHint\n )\n );\n }\n\n this.resetIdleTimer();\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 private resetIdleTimer(): void {\n if (this.idleTimeoutMs <= 0) return;\n if (this.idleTimer) clearTimeout(this.idleTimer);\n this.idleTimer = setTimeout(() => {\n this.idleTimer = null;\n this.logger.info(\n `No activity for ${this.idleTimeoutMs / 1000}s — idle shutdown`\n );\n this.stop();\n this.onIdleShutdown?.();\n }, this.idleTimeoutMs);\n }\n\n private handleIdentify(\n socket: WebSocket,\n message: Extract<ExtensionMessage, { type: \"identify\" }>\n ): void {\n if (message.role !== \"extension\") {\n this.logger.warn(`Ignoring identify with unsupported role: ${message.role}`);\n return;\n }\n\n const deviceId = message.deviceId?.trim();\n if (!deviceId) {\n this.logger.warn(\"Identify message missing deviceId\");\n return;\n }\n\n const deviceName = this.sanitizeDeviceName(message.deviceName);\n const paired = this.pairedDevices.get(deviceId);\n const providedToken = message.authToken?.trim();\n\n if (paired && providedToken && providedToken === paired.authToken) {\n this.deletePendingPairingBySocket(socket);\n this.markSocketAsExtension(socket, {\n ...paired,\n deviceName,\n });\n this.logger.info(`Extension authenticated (${deviceName}, ${deviceId})`);\n return;\n }\n\n const pairing = this.createOrRefreshPairing(socket, deviceId, deviceName);\n this.logger.info(\n `Pairing required for \"${pairing.deviceName}\" (${pairing.deviceId}). Code: ${pairing.code}`\n );\n this.sendJson(socket, {\n type: \"pairing_required\",\n code: pairing.code,\n deviceId: pairing.deviceId,\n deviceName: pairing.deviceName,\n requestedAt: new Date(pairing.requestedAt).toISOString(),\n expiresAt: new Date(pairing.expiresAt).toISOString(),\n });\n }\n\n private markSocketAsExtension(\n socket: WebSocket,\n paired: PairedDeviceRecord\n ): void {\n if (this.extensionSocket && this.extensionSocket !== socket) {\n this.extensionSocket.close();\n }\n\n const now = new Date().toISOString();\n const updated: PairedDeviceRecord = {\n ...paired,\n lastSeenAt: now,\n };\n this.pairedDevices.set(updated.deviceId, updated);\n this.persistPairedDevices();\n\n this.extensionSocket = socket;\n this.resetIdleTimer();\n this.sendJson(socket, {\n type: \"identified\",\n deviceId: updated.deviceId,\n deviceName: updated.deviceName,\n });\n }\n\n private createOrRefreshPairing(\n socket: WebSocket,\n deviceId: string,\n deviceName: string\n ): PendingPairingRecord {\n this.pruneExpiredPairings();\n this.deletePendingPairingBySocket(socket);\n\n for (const pairing of this.pendingPairings.values()) {\n if (pairing.deviceId === deviceId && pairing.socket.readyState === WebSocket.OPEN) {\n this.pendingPairingCodeBySocket.delete(pairing.socket);\n this.pendingPairingCodeBySocket.set(socket, pairing.code);\n pairing.socket = socket;\n pairing.deviceName = deviceName;\n pairing.requestedAt = Date.now();\n pairing.expiresAt = pairing.requestedAt + this.pairingCodeTtlMs;\n return pairing;\n }\n }\n\n let code = this.generatePairingCode();\n while (this.pendingPairings.has(code)) {\n code = this.generatePairingCode();\n }\n\n const requestedAt = Date.now();\n const pairing: PendingPairingRecord = {\n code,\n deviceId,\n deviceName,\n requestedAt,\n expiresAt: requestedAt + this.pairingCodeTtlMs,\n socket,\n };\n this.pendingPairings.set(code, pairing);\n this.pendingPairingCodeBySocket.set(socket, code);\n return pairing;\n }\n\n private pruneExpiredPairings(): void {\n const now = Date.now();\n for (const [code, pairing] of this.pendingPairings) {\n if (pairing.expiresAt <= now || pairing.socket.readyState !== WebSocket.OPEN) {\n this.deletePendingPairing(code);\n }\n }\n }\n\n private deletePendingPairing(code: string): void {\n const pairing = this.pendingPairings.get(code);\n if (!pairing) return;\n this.pendingPairings.delete(code);\n this.pendingPairingCodeBySocket.delete(pairing.socket);\n }\n\n private deletePendingPairingBySocket(socket: WebSocket): void {\n const code = this.pendingPairingCodeBySocket.get(socket);\n if (!code) return;\n this.deletePendingPairing(code);\n }\n\n private normalizePairingCode(input: string): string {\n return input.trim().toUpperCase().replace(/[^A-Z0-9]/g, \"\");\n }\n\n private generatePairingCode(): string {\n let code = \"\";\n for (let index = 0; index < PAIRING_CODE_LENGTH; index += 1) {\n const pos = randomInt(0, PAIRING_CODE_ALPHABET.length);\n code += PAIRING_CODE_ALPHABET[pos];\n }\n return code;\n }\n\n private sanitizeDeviceName(raw?: string): string {\n const value = raw?.trim();\n return value ? value.slice(0, 120) : \"Reddit Agent Extension\";\n }\n\n private loadPairedDevices(): void {\n try {\n if (!existsSync(this.pairingStorePath)) {\n return;\n }\n const raw = readFileSync(this.pairingStorePath, \"utf8\");\n const parsed = JSON.parse(raw) as Partial<PairingStoreFile>;\n const devices = Array.isArray(parsed.devices) ? parsed.devices : [];\n\n for (const device of devices) {\n if (\n typeof device.deviceId === \"string\" &&\n typeof device.deviceName === \"string\" &&\n typeof device.authToken === \"string\" &&\n typeof device.approvedAt === \"string\" &&\n typeof device.lastSeenAt === \"string\"\n ) {\n this.pairedDevices.set(device.deviceId, {\n deviceId: device.deviceId,\n deviceName: device.deviceName,\n authToken: device.authToken,\n approvedAt: device.approvedAt,\n lastSeenAt: device.lastSeenAt,\n });\n }\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n this.logger.warn(\n `Failed to load pairing store at ${this.pairingStorePath}: ${message}`\n );\n }\n }\n\n private persistPairedDevices(): void {\n try {\n mkdirSync(path.dirname(this.pairingStorePath), { recursive: true });\n const payload: PairingStoreFile = {\n version: 1,\n devices: Array.from(this.pairedDevices.values()),\n };\n writeFileSync(this.pairingStorePath, JSON.stringify(payload, null, 2));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n this.logger.error(\n `Failed to persist pairing store at ${this.pairingStorePath}: ${message}`\n );\n }\n }\n\n private sendJson(socket: WebSocket, payload: object): void {\n if (socket.readyState !== WebSocket.OPEN) return;\n socket.send(JSON.stringify(payload));\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_list_pairing_requests\",\n description:\n \"List pending browser-extension pairing requests that need user approval.\",\n parameters: Type.Object({}),\n async execute() {\n const data = bridge.getPendingPairings();\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_approve_pairing\",\n description:\n \"Approve a pending browser-extension pairing request by its pairing code.\",\n parameters: Type.Object({\n code: Type.String({\n description:\n \"Pairing code shown in the extension popup (for example: ABC123).\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const code = String(params.code ?? \"\").trim();\n const data = bridge.approvePairing(code);\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_list_paired_devices\",\n description:\n \"List previously approved extension devices that can auto-connect.\",\n parameters: Type.Object({}),\n async execute() {\n const data = bridge.getPairedDevices();\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\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_user_posts\",\n description:\n \"Fetch posts submitted by a Reddit user. Provide the username. \" +\n \"Supports sort (hot, new, top, controversial) and time filters \" +\n \"(hour, day, week, month, year).\",\n parameters: Type.Object({\n username: Type.String({\n description:\n 'Reddit username (e.g. \"richo-s\"). Do not include u/ or /u/ prefix.',\n }),\n sort: Type.Optional(\n Type.Unsafe<\"hot\" | \"new\" | \"top\" | \"controversial\">({\n type: \"string\",\n enum: [\"hot\", \"new\", \"top\", \"controversial\"],\n description: \"Sort order. Default: hot.\",\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 (applies when sort is top or controversial). Default: all time.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_user_posts\", {\n username: params.username,\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 BRIDGE_PORT = 7071;\nconst DEFAULT_HOST = \"0.0.0.0\";\nconst DEFAULT_PAIRING_CODE_TTL_SECONDS = 600;\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: \"openclaw-reddit-agent-server\",\n name: \"OpenClaw Reddit Agent\",\n description:\n \"Bridge to Reddit Chrome extension for browsing, searching, and commenting.\",\n\n register(api: PluginApi) {\n const host =\n typeof api.pluginConfig?.host === \"string\" &&\n api.pluginConfig.host.trim().length > 0\n ? api.pluginConfig.host.trim()\n : DEFAULT_HOST;\n const pairingCodeTtlSeconds =\n typeof api.pluginConfig?.pairingCodeTtlSeconds === \"number\" &&\n api.pluginConfig.pairingCodeTtlSeconds > 0\n ? api.pluginConfig.pairingCodeTtlSeconds\n : DEFAULT_PAIRING_CODE_TTL_SECONDS;\n const pairingStorePath =\n typeof api.pluginConfig?.pairingStorePath === \"string\" &&\n api.pluginConfig.pairingStorePath.trim().length > 0\n ? api.pluginConfig.pairingStorePath.trim()\n : undefined;\n\n const bridge = new WebSocketBridge(BRIDGE_PORT, {\n logger: {\n info: (msg) => api.logger.info(msg),\n warn: (msg) => api.logger.warn(msg),\n error: (msg) => api.logger.error(msg),\n },\n host,\n pairingCodeTtlMs: pairingCodeTtlSeconds * 1000,\n pairingStorePath,\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":["pairing"]}
|
package/openclaw.plugin.json
CHANGED
|
@@ -6,19 +6,36 @@
|
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
|
-
"
|
|
9
|
+
"host": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"default": "0.0.0.0"
|
|
12
|
+
},
|
|
13
|
+
"pairingCodeTtlSeconds": {
|
|
10
14
|
"type": "integer",
|
|
11
|
-
"minimum":
|
|
12
|
-
"maximum":
|
|
13
|
-
"default":
|
|
15
|
+
"minimum": 30,
|
|
16
|
+
"maximum": 86400,
|
|
17
|
+
"default": 600
|
|
18
|
+
},
|
|
19
|
+
"pairingStorePath": {
|
|
20
|
+
"type": "string"
|
|
14
21
|
}
|
|
15
22
|
}
|
|
16
23
|
},
|
|
17
24
|
"uiHints": {
|
|
18
|
-
"
|
|
19
|
-
"label": "
|
|
20
|
-
"placeholder": "
|
|
21
|
-
"help": "
|
|
25
|
+
"host": {
|
|
26
|
+
"label": "Bind Host",
|
|
27
|
+
"placeholder": "0.0.0.0",
|
|
28
|
+
"help": "Network interface to bind the WebSocket bridge to. Use 0.0.0.0 for LAN/Tailscale access, or localhost for same-machine only."
|
|
29
|
+
},
|
|
30
|
+
"pairingCodeTtlSeconds": {
|
|
31
|
+
"label": "Pairing Code TTL (seconds)",
|
|
32
|
+
"placeholder": "600",
|
|
33
|
+
"help": "How long a pairing code remains valid before expiring."
|
|
34
|
+
},
|
|
35
|
+
"pairingStorePath": {
|
|
36
|
+
"label": "Pairing Store Path",
|
|
37
|
+
"placeholder": "~/.openclaw/reddit-agent-pairings.json",
|
|
38
|
+
"help": "Optional file path for persisted approved devices and auth tokens."
|
|
22
39
|
}
|
|
23
40
|
},
|
|
24
41
|
"skills": ["skills/openclaw-reddit-agent-server"]
|