openclaw-reddit-agent-server 2026.2.153 → 2026.2.2701
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 +141 -33
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
// src/ws-bridge.ts
|
|
2
|
-
import { randomInt, randomUUID } from "crypto";
|
|
3
|
-
import {
|
|
2
|
+
import { createHash, randomInt, randomUUID } from "crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync
|
|
9
|
+
} from "fs";
|
|
4
10
|
import { homedir } from "os";
|
|
5
11
|
import path from "path";
|
|
6
12
|
import { WebSocket, WebSocketServer } from "ws";
|
|
7
13
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
8
14
|
var DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
9
|
-
var DEFAULT_BIND_HOST = "
|
|
15
|
+
var DEFAULT_BIND_HOST = "localhost";
|
|
10
16
|
var DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1e3;
|
|
17
|
+
var DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
|
|
11
18
|
var DEFAULT_PAIRING_STORE_PATH = path.join(
|
|
12
19
|
homedir(),
|
|
13
20
|
".openclaw",
|
|
@@ -15,6 +22,16 @@ var DEFAULT_PAIRING_STORE_PATH = path.join(
|
|
|
15
22
|
);
|
|
16
23
|
var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
17
24
|
var PAIRING_CODE_LENGTH = 6;
|
|
25
|
+
var MAX_RESPONSE_ID_LENGTH = 200;
|
|
26
|
+
var MAX_RESPONSE_ERROR_LENGTH = 2e3;
|
|
27
|
+
var ALLOWED_ACTIONS = /* @__PURE__ */ new Set([
|
|
28
|
+
"get_skill",
|
|
29
|
+
"fetch_subreddit",
|
|
30
|
+
"search_reddit",
|
|
31
|
+
"fetch_user_posts",
|
|
32
|
+
"fetch_post",
|
|
33
|
+
"reply_to_comment"
|
|
34
|
+
]);
|
|
18
35
|
var WebSocketBridge = class {
|
|
19
36
|
wss = null;
|
|
20
37
|
extensionSocket = null;
|
|
@@ -86,7 +103,7 @@ var WebSocketBridge = class {
|
|
|
86
103
|
const paired = {
|
|
87
104
|
deviceId: pairing.deviceId,
|
|
88
105
|
deviceName: pairing.deviceName,
|
|
89
|
-
authToken,
|
|
106
|
+
authTokenHash: this.hashAuthToken(authToken),
|
|
90
107
|
approvedAt: now,
|
|
91
108
|
lastSeenAt: now
|
|
92
109
|
};
|
|
@@ -96,7 +113,7 @@ var WebSocketBridge = class {
|
|
|
96
113
|
type: "pairing_approved",
|
|
97
114
|
deviceId: paired.deviceId,
|
|
98
115
|
deviceName: paired.deviceName,
|
|
99
|
-
authToken
|
|
116
|
+
authToken
|
|
100
117
|
});
|
|
101
118
|
this.deletePendingPairing(code);
|
|
102
119
|
this.markSocketAsExtension(pairing.socket, paired);
|
|
@@ -116,7 +133,8 @@ var WebSocketBridge = class {
|
|
|
116
133
|
this.wss = new WebSocketServer({
|
|
117
134
|
port: this.port,
|
|
118
135
|
host: this.host,
|
|
119
|
-
path: "/ws"
|
|
136
|
+
path: "/ws",
|
|
137
|
+
maxPayload: DEFAULT_MAX_PAYLOAD_BYTES
|
|
120
138
|
});
|
|
121
139
|
this.logger.info(
|
|
122
140
|
`WebSocket bridge listening on ws://${this.host}:${this.port}/ws`
|
|
@@ -126,47 +144,59 @@ var WebSocketBridge = class {
|
|
|
126
144
|
`New WebSocket connection from ${request.socket.remoteAddress ?? "unknown"}`
|
|
127
145
|
);
|
|
128
146
|
socket.on("message", (raw) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
message = JSON.parse(
|
|
132
|
-
typeof raw === "string" ? raw : raw.toString("utf8")
|
|
133
|
-
);
|
|
134
|
-
} catch {
|
|
147
|
+
const message = this.parseIncomingMessage(raw);
|
|
148
|
+
if (!message) {
|
|
135
149
|
this.logger.warn("Failed to parse incoming WebSocket message");
|
|
150
|
+
this.closeProtocolViolation(socket, "Invalid JSON message");
|
|
136
151
|
return;
|
|
137
152
|
}
|
|
138
|
-
if (
|
|
153
|
+
if (this.isPingMessage(message)) {
|
|
139
154
|
this.sendJson(socket, { type: "pong" });
|
|
140
155
|
return;
|
|
141
156
|
}
|
|
142
|
-
if (
|
|
157
|
+
if (this.isIdentifyMessage(message)) {
|
|
143
158
|
this.handleIdentify(socket, message);
|
|
144
159
|
return;
|
|
145
160
|
}
|
|
146
|
-
if (
|
|
161
|
+
if (this.isBridgeResponseMessage(message)) {
|
|
147
162
|
if (socket !== this.extensionSocket) {
|
|
148
163
|
this.logger.warn(
|
|
149
|
-
`
|
|
164
|
+
`Dropping response from unauthenticated socket id: ${message.id}`
|
|
150
165
|
);
|
|
166
|
+
this.closeProtocolViolation(socket, "Unauthenticated response");
|
|
151
167
|
return;
|
|
152
168
|
}
|
|
153
|
-
const
|
|
154
|
-
const pending = this.pendingRequests.get(response.id);
|
|
169
|
+
const pending = this.pendingRequests.get(message.id);
|
|
155
170
|
if (!pending) {
|
|
156
171
|
this.logger.warn(
|
|
157
|
-
`Received response for unknown request id: ${
|
|
172
|
+
`Received response for unknown request id: ${message.id}`
|
|
158
173
|
);
|
|
159
174
|
return;
|
|
160
175
|
}
|
|
161
|
-
this.pendingRequests.delete(
|
|
176
|
+
this.pendingRequests.delete(message.id);
|
|
162
177
|
clearTimeout(pending.timer);
|
|
163
178
|
this.resetIdleTimer();
|
|
164
|
-
if (
|
|
165
|
-
pending.resolve(
|
|
179
|
+
if (message.success) {
|
|
180
|
+
pending.resolve(message.data);
|
|
166
181
|
} else {
|
|
167
|
-
pending.reject(new Error(
|
|
182
|
+
pending.reject(new Error(message.error));
|
|
168
183
|
}
|
|
184
|
+
return;
|
|
169
185
|
}
|
|
186
|
+
if (this.isObject(message) && "action" in message) {
|
|
187
|
+
this.logger.warn(
|
|
188
|
+
`Rejected illegal inbound action from extension socket ${this.describeSocket(socket)}`
|
|
189
|
+
);
|
|
190
|
+
this.closeProtocolViolation(
|
|
191
|
+
socket,
|
|
192
|
+
"Inbound actions are not allowed from extension"
|
|
193
|
+
);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.logger.warn(
|
|
197
|
+
`Ignoring unexpected bridge message from ${this.describeSocket(socket)}`
|
|
198
|
+
);
|
|
199
|
+
this.closeProtocolViolation(socket, "Unexpected message shape");
|
|
170
200
|
});
|
|
171
201
|
socket.on("close", () => {
|
|
172
202
|
const code = this.pendingPairingCodeBySocket.get(socket);
|
|
@@ -210,6 +240,9 @@ var WebSocketBridge = class {
|
|
|
210
240
|
* Returns the `data` field from the success response.
|
|
211
241
|
*/
|
|
212
242
|
sendAction(action, params, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
243
|
+
if (!ALLOWED_ACTIONS.has(action)) {
|
|
244
|
+
return Promise.reject(new Error(`Unsupported bridge action: ${action}`));
|
|
245
|
+
}
|
|
213
246
|
if (!this.isExtensionConnected) {
|
|
214
247
|
const pendingCodes = this.getPendingPairings().map((pairing) => pairing.code);
|
|
215
248
|
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.";
|
|
@@ -252,15 +285,17 @@ var WebSocketBridge = class {
|
|
|
252
285
|
this.logger.warn(`Ignoring identify with unsupported role: ${message.role}`);
|
|
253
286
|
return;
|
|
254
287
|
}
|
|
255
|
-
const deviceId = message.deviceId
|
|
288
|
+
const deviceId = this.sanitizeDeviceId(message.deviceId);
|
|
256
289
|
if (!deviceId) {
|
|
257
|
-
this.logger.warn("Identify message
|
|
290
|
+
this.logger.warn("Identify message has an invalid deviceId");
|
|
291
|
+
this.closeProtocolViolation(socket, "Invalid deviceId");
|
|
258
292
|
return;
|
|
259
293
|
}
|
|
260
294
|
const deviceName = this.sanitizeDeviceName(message.deviceName);
|
|
261
295
|
const paired = this.pairedDevices.get(deviceId);
|
|
262
296
|
const providedToken = message.authToken?.trim();
|
|
263
|
-
|
|
297
|
+
const providedTokenHash = providedToken ? this.hashAuthToken(providedToken) : null;
|
|
298
|
+
if (paired && providedTokenHash && providedTokenHash === paired.authTokenHash) {
|
|
264
299
|
this.deletePendingPairingBySocket(socket);
|
|
265
300
|
this.markSocketAsExtension(socket, {
|
|
266
301
|
...paired,
|
|
@@ -271,7 +306,7 @@ var WebSocketBridge = class {
|
|
|
271
306
|
}
|
|
272
307
|
const pairing = this.createOrRefreshPairing(socket, deviceId, deviceName);
|
|
273
308
|
this.logger.info(
|
|
274
|
-
`Pairing required for "${pairing.deviceName}" (${pairing.deviceId}).
|
|
309
|
+
`Pairing required for "${pairing.deviceName}" (${pairing.deviceId}). Awaiting user approval.`
|
|
275
310
|
);
|
|
276
311
|
this.sendJson(socket, {
|
|
277
312
|
type: "pairing_required",
|
|
@@ -366,6 +401,14 @@ var WebSocketBridge = class {
|
|
|
366
401
|
const value = raw?.trim();
|
|
367
402
|
return value ? value.slice(0, 120) : "Reddit Agent Extension";
|
|
368
403
|
}
|
|
404
|
+
sanitizeDeviceId(raw) {
|
|
405
|
+
const value = raw?.trim();
|
|
406
|
+
if (!value || value.length > 120) return null;
|
|
407
|
+
return /^[A-Za-z0-9._:-]+$/.test(value) ? value : null;
|
|
408
|
+
}
|
|
409
|
+
hashAuthToken(token) {
|
|
410
|
+
return createHash("sha256").update(token).digest("hex");
|
|
411
|
+
}
|
|
369
412
|
loadPairedDevices() {
|
|
370
413
|
try {
|
|
371
414
|
if (!existsSync(this.pairingStorePath)) {
|
|
@@ -374,17 +417,25 @@ var WebSocketBridge = class {
|
|
|
374
417
|
const raw = readFileSync(this.pairingStorePath, "utf8");
|
|
375
418
|
const parsed = JSON.parse(raw);
|
|
376
419
|
const devices = Array.isArray(parsed.devices) ? parsed.devices : [];
|
|
420
|
+
let needsMigration = false;
|
|
377
421
|
for (const device of devices) {
|
|
378
|
-
|
|
422
|
+
const authTokenHash = typeof device.authTokenHash === "string" ? device.authTokenHash : typeof device.authToken === "string" ? this.hashAuthToken(device.authToken) : null;
|
|
423
|
+
if (typeof device.deviceId === "string" && typeof device.deviceName === "string" && typeof authTokenHash === "string" && authTokenHash.length > 0 && typeof device.approvedAt === "string" && typeof device.lastSeenAt === "string") {
|
|
424
|
+
if (typeof device.authToken === "string" && !device.authTokenHash) {
|
|
425
|
+
needsMigration = true;
|
|
426
|
+
}
|
|
379
427
|
this.pairedDevices.set(device.deviceId, {
|
|
380
428
|
deviceId: device.deviceId,
|
|
381
429
|
deviceName: device.deviceName,
|
|
382
|
-
|
|
430
|
+
authTokenHash,
|
|
383
431
|
approvedAt: device.approvedAt,
|
|
384
432
|
lastSeenAt: device.lastSeenAt
|
|
385
433
|
});
|
|
386
434
|
}
|
|
387
435
|
}
|
|
436
|
+
if (needsMigration) {
|
|
437
|
+
this.persistPairedDevices();
|
|
438
|
+
}
|
|
388
439
|
} catch (error) {
|
|
389
440
|
const message = error instanceof Error ? error.message : String(error);
|
|
390
441
|
this.logger.warn(
|
|
@@ -394,12 +445,21 @@ var WebSocketBridge = class {
|
|
|
394
445
|
}
|
|
395
446
|
persistPairedDevices() {
|
|
396
447
|
try {
|
|
397
|
-
|
|
448
|
+
const storeDir = path.dirname(this.pairingStorePath);
|
|
449
|
+
mkdirSync(storeDir, { recursive: true, mode: 448 });
|
|
398
450
|
const payload = {
|
|
399
|
-
version:
|
|
451
|
+
version: 2,
|
|
400
452
|
devices: Array.from(this.pairedDevices.values())
|
|
401
453
|
};
|
|
402
|
-
writeFileSync(this.pairingStorePath, JSON.stringify(payload, null, 2)
|
|
454
|
+
writeFileSync(this.pairingStorePath, JSON.stringify(payload, null, 2), {
|
|
455
|
+
encoding: "utf8",
|
|
456
|
+
mode: 384
|
|
457
|
+
});
|
|
458
|
+
try {
|
|
459
|
+
chmodSync(storeDir, 448);
|
|
460
|
+
chmodSync(this.pairingStorePath, 384);
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
403
463
|
} catch (error) {
|
|
404
464
|
const message = error instanceof Error ? error.message : String(error);
|
|
405
465
|
this.logger.error(
|
|
@@ -411,6 +471,54 @@ var WebSocketBridge = class {
|
|
|
411
471
|
if (socket.readyState !== WebSocket.OPEN) return;
|
|
412
472
|
socket.send(JSON.stringify(payload));
|
|
413
473
|
}
|
|
474
|
+
parseIncomingMessage(raw) {
|
|
475
|
+
try {
|
|
476
|
+
let text;
|
|
477
|
+
if (typeof raw === "string") {
|
|
478
|
+
text = raw;
|
|
479
|
+
} else if (raw instanceof Buffer) {
|
|
480
|
+
text = raw.toString("utf8");
|
|
481
|
+
} else if (raw instanceof ArrayBuffer) {
|
|
482
|
+
text = Buffer.from(raw).toString("utf8");
|
|
483
|
+
} else if (Array.isArray(raw)) {
|
|
484
|
+
text = Buffer.concat(raw).toString("utf8");
|
|
485
|
+
} else {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
return JSON.parse(text);
|
|
489
|
+
} catch {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
isObject(value) {
|
|
494
|
+
return typeof value === "object" && value !== null;
|
|
495
|
+
}
|
|
496
|
+
isPingMessage(message) {
|
|
497
|
+
return this.isObject(message) && message.type === "ping";
|
|
498
|
+
}
|
|
499
|
+
isIdentifyMessage(message) {
|
|
500
|
+
return this.isObject(message) && message.type === "identify" && message.role === "extension" && typeof message.deviceId === "string" && (typeof message.deviceName === "undefined" || typeof message.deviceName === "string") && (typeof message.authToken === "undefined" || typeof message.authToken === "string");
|
|
501
|
+
}
|
|
502
|
+
isBridgeResponseMessage(message) {
|
|
503
|
+
if (!this.isObject(message)) return false;
|
|
504
|
+
if (typeof message.id !== "string" || message.id.length === 0) return false;
|
|
505
|
+
if (message.id.length > MAX_RESPONSE_ID_LENGTH) return false;
|
|
506
|
+
if (typeof message.success !== "boolean") return false;
|
|
507
|
+
if (message.success) {
|
|
508
|
+
return "data" in message;
|
|
509
|
+
}
|
|
510
|
+
return typeof message.error === "string" && message.error.length <= MAX_RESPONSE_ERROR_LENGTH;
|
|
511
|
+
}
|
|
512
|
+
closeProtocolViolation(socket, reason) {
|
|
513
|
+
try {
|
|
514
|
+
socket.close(1008, reason.slice(0, 123));
|
|
515
|
+
} catch {
|
|
516
|
+
socket.close();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
describeSocket(socket) {
|
|
520
|
+
return socket === this.extensionSocket ? "authenticated" : "unauthenticated";
|
|
521
|
+
}
|
|
414
522
|
};
|
|
415
523
|
|
|
416
524
|
// src/tools.ts
|
|
@@ -596,7 +704,7 @@ function registerRedditTools(registerTool, bridge) {
|
|
|
596
704
|
|
|
597
705
|
// index.ts
|
|
598
706
|
var BRIDGE_PORT = 7071;
|
|
599
|
-
var DEFAULT_HOST = "
|
|
707
|
+
var DEFAULT_HOST = "localhost";
|
|
600
708
|
var DEFAULT_PAIRING_CODE_TTL_SECONDS = 600;
|
|
601
709
|
var redditAgentPlugin = {
|
|
602
710
|
id: "openclaw-reddit-agent-server",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"sources":["../src/ws-bridge.ts","../src/tools.ts","../index.ts"],"sourcesContent":["import { createHash, randomInt, randomUUID } from \"node:crypto\";\nimport {\n chmodSync,\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} 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 IdentifyMessage,\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 = \"localhost\";\nconst DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1000; // 10 minutes\nconst DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024; // 8 MiB\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;\nconst MAX_RESPONSE_ID_LENGTH = 200;\nconst MAX_RESPONSE_ERROR_LENGTH = 2_000;\nconst ALLOWED_ACTIONS = new Set([\n \"get_skill\",\n \"fetch_subreddit\",\n \"search_reddit\",\n \"fetch_user_posts\",\n \"fetch_post\",\n \"reply_to_comment\",\n]);\n\ninterface PairedDeviceRecord {\n deviceId: string;\n deviceName: string;\n authTokenHash: string;\n approvedAt: string;\n lastSeenAt: string;\n}\n\ninterface PairingStoreFile {\n version: number;\n devices: Array<\n Omit<PairedDeviceRecord, \"authTokenHash\"> & {\n authTokenHash?: string;\n authToken?: string;\n }\n >;\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 authTokenHash: this.hashAuthToken(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,\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 maxPayload: DEFAULT_MAX_PAYLOAD_BYTES,\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 const message = this.parseIncomingMessage(raw);\n if (!message) {\n this.logger.warn(\"Failed to parse incoming WebSocket message\");\n this.closeProtocolViolation(socket, \"Invalid JSON message\");\n return;\n }\n\n if (this.isPingMessage(message)) {\n this.sendJson(socket, { type: \"pong\" });\n return;\n }\n\n if (this.isIdentifyMessage(message)) {\n this.handleIdentify(socket, message);\n return;\n }\n\n if (this.isBridgeResponseMessage(message)) {\n if (socket !== this.extensionSocket) {\n this.logger.warn(\n `Dropping response from unauthenticated socket id: ${message.id}`\n );\n this.closeProtocolViolation(socket, \"Unauthenticated response\");\n return;\n }\n\n const pending = this.pendingRequests.get(message.id);\n if (!pending) {\n this.logger.warn(\n `Received response for unknown request id: ${message.id}`\n );\n return;\n }\n\n this.pendingRequests.delete(message.id);\n clearTimeout(pending.timer);\n this.resetIdleTimer();\n\n if (message.success) {\n pending.resolve(message.data);\n } else {\n pending.reject(new Error(message.error));\n }\n return;\n }\n\n if (this.isObject(message) && \"action\" in message) {\n this.logger.warn(\n `Rejected illegal inbound action from extension socket ${this.describeSocket(socket)}`\n );\n this.closeProtocolViolation(\n socket,\n \"Inbound actions are not allowed from extension\"\n );\n return;\n }\n\n this.logger.warn(\n `Ignoring unexpected bridge message from ${this.describeSocket(socket)}`\n );\n this.closeProtocolViolation(socket, \"Unexpected message shape\");\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 (!ALLOWED_ACTIONS.has(action)) {\n return Promise.reject(new Error(`Unsupported bridge action: ${action}`));\n }\n\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: IdentifyMessage\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 = this.sanitizeDeviceId(message.deviceId);\n if (!deviceId) {\n this.logger.warn(\"Identify message has an invalid deviceId\");\n this.closeProtocolViolation(socket, \"Invalid 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 const providedTokenHash = providedToken\n ? this.hashAuthToken(providedToken)\n : null;\n\n if (paired && providedTokenHash && providedTokenHash === paired.authTokenHash) {\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}). Awaiting user approval.`\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 sanitizeDeviceId(raw?: string): string | null {\n const value = raw?.trim();\n if (!value || value.length > 120) return null;\n return /^[A-Za-z0-9._:-]+$/.test(value) ? value : null;\n }\n\n private hashAuthToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\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 let needsMigration = false;\n\n for (const device of devices) {\n const authTokenHash =\n typeof device.authTokenHash === \"string\"\n ? device.authTokenHash\n : typeof device.authToken === \"string\"\n ? this.hashAuthToken(device.authToken)\n : null;\n if (\n typeof device.deviceId === \"string\" &&\n typeof device.deviceName === \"string\" &&\n typeof authTokenHash === \"string\" &&\n authTokenHash.length > 0 &&\n typeof device.approvedAt === \"string\" &&\n typeof device.lastSeenAt === \"string\"\n ) {\n if (typeof device.authToken === \"string\" && !device.authTokenHash) {\n needsMigration = true;\n }\n this.pairedDevices.set(device.deviceId, {\n deviceId: device.deviceId,\n deviceName: device.deviceName,\n authTokenHash,\n approvedAt: device.approvedAt,\n lastSeenAt: device.lastSeenAt,\n });\n }\n }\n\n if (needsMigration) {\n this.persistPairedDevices();\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 const storeDir = path.dirname(this.pairingStorePath);\n mkdirSync(storeDir, { recursive: true, mode: 0o700 });\n const payload: PairingStoreFile = {\n version: 2,\n devices: Array.from(this.pairedDevices.values()),\n };\n writeFileSync(this.pairingStorePath, JSON.stringify(payload, null, 2), {\n encoding: \"utf8\",\n mode: 0o600,\n });\n try {\n chmodSync(storeDir, 0o700);\n chmodSync(this.pairingStorePath, 0o600);\n } catch {\n // Best-effort on platforms that don't support POSIX permissions.\n }\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 private parseIncomingMessage(raw: unknown): unknown | null {\n try {\n let text: string;\n if (typeof raw === \"string\") {\n text = raw;\n } else if (raw instanceof Buffer) {\n text = raw.toString(\"utf8\");\n } else if (raw instanceof ArrayBuffer) {\n text = Buffer.from(raw).toString(\"utf8\");\n } else if (Array.isArray(raw)) {\n text = Buffer.concat(raw).toString(\"utf8\");\n } else {\n return null;\n }\n return JSON.parse(text);\n } catch {\n return null;\n }\n }\n\n private isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n }\n\n private isPingMessage(message: unknown): message is { type: \"ping\" } {\n return this.isObject(message) && message.type === \"ping\";\n }\n\n private isIdentifyMessage(message: unknown): message is IdentifyMessage {\n return (\n this.isObject(message) &&\n message.type === \"identify\" &&\n message.role === \"extension\" &&\n typeof message.deviceId === \"string\" &&\n (typeof message.deviceName === \"undefined\" ||\n typeof message.deviceName === \"string\") &&\n (typeof message.authToken === \"undefined\" ||\n typeof message.authToken === \"string\")\n );\n }\n\n private isBridgeResponseMessage(message: unknown): message is BridgeResponse {\n if (!this.isObject(message)) return false;\n if (typeof message.id !== \"string\" || message.id.length === 0) return false;\n if (message.id.length > MAX_RESPONSE_ID_LENGTH) return false;\n if (typeof message.success !== \"boolean\") return false;\n\n if (message.success) {\n return \"data\" in message;\n }\n\n return (\n typeof message.error === \"string\" &&\n message.error.length <= MAX_RESPONSE_ERROR_LENGTH\n );\n }\n\n private closeProtocolViolation(socket: WebSocket, reason: string): void {\n try {\n socket.close(1008, reason.slice(0, 123));\n } catch {\n socket.close();\n }\n }\n\n private describeSocket(socket: WebSocket): string {\n return socket === this.extensionSocket ? \"authenticated\" : \"unauthenticated\";\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 = \"localhost\";\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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"properties": {
|
|
9
9
|
"host": {
|
|
10
10
|
"type": "string",
|
|
11
|
-
"default": "
|
|
11
|
+
"default": "localhost"
|
|
12
12
|
},
|
|
13
13
|
"pairingCodeTtlSeconds": {
|
|
14
14
|
"type": "integer",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"uiHints": {
|
|
25
25
|
"host": {
|
|
26
26
|
"label": "Bind Host",
|
|
27
|
-
"placeholder": "
|
|
28
|
-
"help": "Network interface to bind the WebSocket bridge to. Use 0.0.0.0
|
|
27
|
+
"placeholder": "localhost",
|
|
28
|
+
"help": "Network interface to bind the WebSocket bridge to. localhost is safest for same-machine use. Use 0.0.0.0 only when you explicitly need LAN/Tailscale access."
|
|
29
29
|
},
|
|
30
30
|
"pairingCodeTtlSeconds": {
|
|
31
31
|
"label": "Pairing Code TTL (seconds)",
|