@rrwebcloud/openclaw-session-recording 2026.3.28-1 → 2026.3.28-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/index.ts +54 -21
- package/package.json +8 -6
- package/skills/rrweb-replay/SKILL.md +8 -9
package/README.md
CHANGED
|
@@ -15,15 +15,17 @@ plugins:
|
|
|
15
15
|
entries:
|
|
16
16
|
rrweb-replay:
|
|
17
17
|
enabled: true
|
|
18
|
-
publicKey:
|
|
19
|
-
extensionMode: bundled
|
|
18
|
+
publicKey: public_key_rr_your_public_key
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
## Notes
|
|
23
22
|
|
|
24
|
-
- OpenClaw `2026.3.24+` uses the
|
|
23
|
+
- OpenClaw `2026.3.24+` uses the automatic browser-runtime integration path
|
|
25
24
|
- OpenClaw `2026.3.13` loads in legacy compatibility mode and reports clear next steps instead of failing on install
|
|
26
25
|
- `serverUrl` defaults to `https://api.rrwebcloud.com`
|
|
27
|
-
- `
|
|
26
|
+
- `publicKey` is required for rrwebcloud ingest
|
|
27
|
+
- `secretKey` is not required for recording; it is only needed if you want to query rrwebcloud APIs yourself
|
|
28
|
+
- OpenClaw now records rrweb events locally and uploads authenticated NDJSON from runtime code; it does not rely on browser-side `record.js` ingest
|
|
29
|
+
- the browser extension is optional for the runtime uploader path and is only needed when you specifically want extension-side behavior
|
|
28
30
|
- the plugin targets managed Chromium browser profiles such as `openclaw`
|
|
29
31
|
- `replay_session_info` exposes the active replay metadata
|
package/index.ts
CHANGED
|
@@ -2,9 +2,11 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import {
|
|
6
|
+
type AnyAgentTool,
|
|
7
|
+
type OpenClawConfig,
|
|
8
|
+
type OpenClawPluginApi,
|
|
9
|
+
} from "openclaw/plugin-sdk";
|
|
8
10
|
|
|
9
11
|
type RrwebReplayConfig = {
|
|
10
12
|
enabled: boolean;
|
|
@@ -39,6 +41,7 @@ type ReplayStateStore = {
|
|
|
39
41
|
|
|
40
42
|
const RRWEB_PLUGIN_ID = "rrweb-replay";
|
|
41
43
|
const DEFAULT_RRWEB_API_BASE_URL = "https://api.rrwebcloud.com";
|
|
44
|
+
const DEFAULT_RRWEB_CDN_URL = "https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js";
|
|
42
45
|
const LEGACY_BROWSER_RUNTIME_REASON =
|
|
43
46
|
"This OpenClaw host does not export plugin-sdk/browser-runtime. Automatic rrweb extension wiring requires OpenClaw >=2026.3.24. On legacy hosts, install/configure the rrweb browser addon separately or upgrade OpenClaw.";
|
|
44
47
|
|
|
@@ -57,6 +60,8 @@ type BrowserRuntimeCompat = {
|
|
|
57
60
|
replaySessionId: string;
|
|
58
61
|
replayServerUrl?: string;
|
|
59
62
|
replayUrl?: string;
|
|
63
|
+
replayPublicKey?: string;
|
|
64
|
+
replayRrwebCdnUrl?: string;
|
|
60
65
|
updatedAt: string;
|
|
61
66
|
}) => void;
|
|
62
67
|
unregisterManagedBrowserExtensions: (sourceId: string) => void;
|
|
@@ -187,15 +192,44 @@ function resolvePluginStateFile(api: OpenClawPluginApi): string {
|
|
|
187
192
|
);
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
async function readJsonFileWithFallbackLocal<T>(
|
|
196
|
+
filePath: string,
|
|
197
|
+
fallback: T,
|
|
198
|
+
): Promise<{ value: T; exists: boolean }> {
|
|
199
|
+
try {
|
|
200
|
+
const raw = await fs.promises.readFile(filePath, "utf8");
|
|
201
|
+
return { value: JSON.parse(raw) as T, exists: true };
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const code = (err as { code?: string }).code;
|
|
204
|
+
if (code === "ENOENT") {
|
|
205
|
+
return { value: fallback, exists: false };
|
|
206
|
+
}
|
|
207
|
+
return { value: fallback, exists: false };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function writeJsonFileAtomicallyLocal(filePath: string, value: unknown): Promise<void> {
|
|
212
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 }).catch(() => {
|
|
213
|
+
// Best-effort directory creation; write below will surface any real failure.
|
|
214
|
+
});
|
|
215
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.${crypto.randomUUID()}.tmp`;
|
|
216
|
+
await fs.promises.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
217
|
+
mode: 0o600,
|
|
218
|
+
});
|
|
219
|
+
await fs.promises.rename(tempPath, filePath);
|
|
220
|
+
}
|
|
221
|
+
|
|
190
222
|
async function loadReplayState(api: OpenClawPluginApi): Promise<ReplayStateStore> {
|
|
191
223
|
const filePath = resolvePluginStateFile(api);
|
|
192
|
-
const { value } = await
|
|
224
|
+
const { value } = await readJsonFileWithFallbackLocal<ReplayStateStore>(filePath, {
|
|
225
|
+
sessions: {},
|
|
226
|
+
});
|
|
193
227
|
return value;
|
|
194
228
|
}
|
|
195
229
|
|
|
196
230
|
async function saveReplayState(api: OpenClawPluginApi, state: ReplayStateStore): Promise<void> {
|
|
197
231
|
const filePath = resolvePluginStateFile(api);
|
|
198
|
-
await
|
|
232
|
+
await writeJsonFileAtomicallyLocal(filePath, state);
|
|
199
233
|
}
|
|
200
234
|
|
|
201
235
|
function buildReplaySessionState(params: {
|
|
@@ -225,7 +259,6 @@ function buildReplaySessionState(params: {
|
|
|
225
259
|
|
|
226
260
|
function describeReplayAvailability(params: {
|
|
227
261
|
config: RrwebReplayConfig;
|
|
228
|
-
extensionDir: string | null;
|
|
229
262
|
browserRuntimeAvailable: boolean;
|
|
230
263
|
}): { enabled: boolean; reason?: string } {
|
|
231
264
|
if (!params.config.enabled) {
|
|
@@ -237,12 +270,6 @@ function describeReplayAvailability(params: {
|
|
|
237
270
|
reason: LEGACY_BROWSER_RUNTIME_REASON,
|
|
238
271
|
};
|
|
239
272
|
}
|
|
240
|
-
if (!params.extensionDir) {
|
|
241
|
-
return {
|
|
242
|
-
enabled: false,
|
|
243
|
-
reason: "Replay extension directory is missing. Check rrweb-replay.extensionMode/path.",
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
273
|
const publicKey = resolveConfiguredSecret({
|
|
247
274
|
value: params.config.publicKey,
|
|
248
275
|
envVarName: params.config.publicKeyEnvVar,
|
|
@@ -313,6 +340,13 @@ function resolveBrowserProfileForReplay(params: {
|
|
|
313
340
|
return { name: profileName, driver };
|
|
314
341
|
}
|
|
315
342
|
|
|
343
|
+
function resolveConfiguredPublicKey(config: RrwebReplayConfig): string | undefined {
|
|
344
|
+
return resolveConfiguredSecret({
|
|
345
|
+
value: config.publicKey,
|
|
346
|
+
envVarName: config.publicKeyEnvVar,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
316
350
|
async function upsertReplaySession(params: {
|
|
317
351
|
api: OpenClawPluginApi;
|
|
318
352
|
config: RrwebReplayConfig;
|
|
@@ -425,6 +459,8 @@ async function armReplayContext(params: {
|
|
|
425
459
|
replaySessionId: replayEntry.replaySessionId,
|
|
426
460
|
replayServerUrl: replayEntry.replayServerUrl,
|
|
427
461
|
replayUrl: replayEntry.replayUrl,
|
|
462
|
+
replayPublicKey: resolveConfiguredPublicKey(params.pluginConfig),
|
|
463
|
+
replayRrwebCdnUrl: DEFAULT_RRWEB_CDN_URL,
|
|
428
464
|
updatedAt: new Date().toISOString(),
|
|
429
465
|
});
|
|
430
466
|
return await upsertReplaySession({
|
|
@@ -445,7 +481,6 @@ async function armReplayContext(params: {
|
|
|
445
481
|
function createReplaySessionInfoTool(params: {
|
|
446
482
|
api: OpenClawPluginApi;
|
|
447
483
|
pluginConfig: RrwebReplayConfig;
|
|
448
|
-
extensionDir: string | null;
|
|
449
484
|
browserRuntimePromise: Promise<BrowserRuntimeCompat | null>;
|
|
450
485
|
context: {
|
|
451
486
|
sessionKey?: string;
|
|
@@ -476,7 +511,6 @@ function createReplaySessionInfoTool(params: {
|
|
|
476
511
|
rawParams && typeof rawParams === "object" ? (rawParams as Record<string, unknown>) : null;
|
|
477
512
|
const availability = describeReplayAvailability({
|
|
478
513
|
config: params.pluginConfig,
|
|
479
|
-
extensionDir: params.extensionDir,
|
|
480
514
|
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
481
515
|
});
|
|
482
516
|
const activate = toolParams?.activate === true;
|
|
@@ -530,7 +564,7 @@ function createReplaySessionInfoTool(params: {
|
|
|
530
564
|
};
|
|
531
565
|
}
|
|
532
566
|
|
|
533
|
-
|
|
567
|
+
const rrwebReplayPlugin = {
|
|
534
568
|
id: RRWEB_PLUGIN_ID,
|
|
535
569
|
name: "rrweb Replay",
|
|
536
570
|
description: "Portable rrweb replay bootstrap for OpenClaw-managed browsers.",
|
|
@@ -554,8 +588,8 @@ export default definePluginEntry({
|
|
|
554
588
|
return;
|
|
555
589
|
}
|
|
556
590
|
if (!extensionDir) {
|
|
557
|
-
ctx.logger.
|
|
558
|
-
"[rrweb-replay] extension directory missing;
|
|
591
|
+
ctx.logger.info(
|
|
592
|
+
"[rrweb-replay] extension directory missing; continuing with runtime-side rrwebcloud upload only",
|
|
559
593
|
);
|
|
560
594
|
return;
|
|
561
595
|
}
|
|
@@ -578,7 +612,6 @@ export default definePluginEntry({
|
|
|
578
612
|
const browserRuntime = await browserRuntimePromise;
|
|
579
613
|
const availability = describeReplayAvailability({
|
|
580
614
|
config: pluginConfig,
|
|
581
|
-
extensionDir,
|
|
582
615
|
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
583
616
|
});
|
|
584
617
|
await upsertReplaySession({
|
|
@@ -621,7 +654,6 @@ export default definePluginEntry({
|
|
|
621
654
|
const browserRuntime = await browserRuntimePromise;
|
|
622
655
|
const availability = describeReplayAvailability({
|
|
623
656
|
config: pluginConfig,
|
|
624
|
-
extensionDir,
|
|
625
657
|
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
626
658
|
});
|
|
627
659
|
if (event.toolName !== "browser" || pluginConfig.recordingPolicy !== "browser-only") {
|
|
@@ -649,7 +681,6 @@ export default definePluginEntry({
|
|
|
649
681
|
createReplaySessionInfoTool({
|
|
650
682
|
api,
|
|
651
683
|
pluginConfig,
|
|
652
|
-
extensionDir,
|
|
653
684
|
browserRuntimePromise,
|
|
654
685
|
context: {
|
|
655
686
|
sessionKey: context.sessionKey,
|
|
@@ -659,4 +690,6 @@ export default definePluginEntry({
|
|
|
659
690
|
{ name: "replay_session_info", optional: true },
|
|
660
691
|
);
|
|
661
692
|
},
|
|
662
|
-
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
export default rrwebReplayPlugin;
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rrwebcloud/openclaw-session-recording",
|
|
3
|
-
"version": "2026.3.28-
|
|
3
|
+
"version": "2026.3.28-2",
|
|
4
4
|
"description": "OpenClaw rrweb replay plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
6
|
"files": [
|
|
8
7
|
".codex-plugin/",
|
|
9
8
|
"README.md",
|
|
@@ -12,6 +11,10 @@
|
|
|
12
11
|
"openclaw.plugin.json",
|
|
13
12
|
"skills/"
|
|
14
13
|
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
15
18
|
"dependencies": {
|
|
16
19
|
"@sinclair/typebox": "0.34.48"
|
|
17
20
|
},
|
|
@@ -26,11 +29,10 @@
|
|
|
26
29
|
"optional": true
|
|
27
30
|
}
|
|
28
31
|
},
|
|
29
|
-
"publishConfig": {
|
|
30
|
-
"access": "public"
|
|
31
|
-
},
|
|
32
32
|
"openclaw": {
|
|
33
|
-
"extensions": [
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./index.ts"
|
|
35
|
+
],
|
|
34
36
|
"install": {
|
|
35
37
|
"npmSpec": "@rrwebcloud/openclaw-session-recording",
|
|
36
38
|
"localPath": "extensions/rrweb-replay",
|
|
@@ -7,14 +7,15 @@ What it does:
|
|
|
7
7
|
- Explains whether the `rrweb-replay` plugin is configured and active
|
|
8
8
|
- Shows the current replay session id via `replay_session_info`
|
|
9
9
|
- Helps activate replay context when `recordingPolicy` is `opt-in-tool`
|
|
10
|
-
- Points you to browser profile and
|
|
10
|
+
- Points you to browser profile and rrwebcloud upload troubleshooting
|
|
11
11
|
|
|
12
12
|
Operational notes:
|
|
13
13
|
|
|
14
14
|
- Runtime setup is handled by the native `rrweb-replay` plugin, not by this skill
|
|
15
15
|
- The core browser runtime can also expose replay through `browser.replay.*` when the rrwebcloud extension artifact is wired directly into managed browser startup
|
|
16
16
|
- The plugin targets OpenClaw-managed Chromium profiles such as `openclaw`
|
|
17
|
-
- The streamlined OpenClaw path
|
|
17
|
+
- The streamlined OpenClaw path records rrweb events in the managed browser and uploads authenticated NDJSON from runtime code
|
|
18
|
+
- The streamlined OpenClaw path only needs a public key; the rrweb Cloud API endpoint defaults to `https://api.rrwebcloud.com` and recording does not require a secret key
|
|
18
19
|
- Minimal config:
|
|
19
20
|
|
|
20
21
|
```yaml
|
|
@@ -22,17 +23,15 @@ plugins:
|
|
|
22
23
|
entries:
|
|
23
24
|
rrweb-replay:
|
|
24
25
|
enabled: true
|
|
25
|
-
publicKey:
|
|
26
|
-
extensionMode: bundled
|
|
26
|
+
publicKey: public_key_rr_your_public_key
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
- If replay reports disabled, check:
|
|
30
|
-
- `browser.replay.enabled`
|
|
31
|
-
- `browser.replay.extensionPath`
|
|
32
30
|
- `plugins.entries.rrweb-replay`
|
|
33
|
-
- `rrweb-replay.extensionMode` / `rrweb-replay.extensionPath`
|
|
34
31
|
- `rrweb-replay.publicKey` or `rrweb-replay.publicKeyEnvVar`
|
|
35
|
-
- `
|
|
32
|
+
- `rrweb-replay.serverUrl` only if you intentionally override the default API host
|
|
33
|
+
- `browser.profiles.<name>.driver` is a managed profile such as `openclaw`
|
|
34
|
+
- `browser.profiles.<name>.extensions` only if you are intentionally layering extension-side behavior on top of runtime upload
|
|
36
35
|
|
|
37
36
|
Useful command/tool flow:
|
|
38
37
|
|
|
@@ -42,7 +41,7 @@ Useful command/tool flow:
|
|
|
42
41
|
|
|
43
42
|
Troubleshooting:
|
|
44
43
|
|
|
45
|
-
- Missing extension directory:
|
|
44
|
+
- Missing extension directory: runtime upload can still work; only extension-side behavior is unavailable
|
|
46
45
|
- No replay session id: start a new OpenClaw session or use the browser tool so the plugin can bind session metadata
|
|
47
46
|
- Wrong profile: pass `profile` to `replay_session_info activate=true` or use the browser tool with an explicit `profile`
|
|
48
47
|
- Advanced setup: only override `rrweb-replay.serverUrl` if you are targeting a non-default replay API endpoint
|