@rrwebcloud/openclaw-session-recording 2026.3.28-1 → 2026.3.28-3
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 +55 -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,12 @@ 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
|
+
definePluginEntry,
|
|
7
|
+
type AnyAgentTool,
|
|
8
|
+
type OpenClawConfig,
|
|
9
|
+
type OpenClawPluginApi,
|
|
10
|
+
} from "openclaw/plugin-sdk/plugin-entry";
|
|
8
11
|
|
|
9
12
|
type RrwebReplayConfig = {
|
|
10
13
|
enabled: boolean;
|
|
@@ -39,6 +42,7 @@ type ReplayStateStore = {
|
|
|
39
42
|
|
|
40
43
|
const RRWEB_PLUGIN_ID = "rrweb-replay";
|
|
41
44
|
const DEFAULT_RRWEB_API_BASE_URL = "https://api.rrwebcloud.com";
|
|
45
|
+
const DEFAULT_RRWEB_CDN_URL = "https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js";
|
|
42
46
|
const LEGACY_BROWSER_RUNTIME_REASON =
|
|
43
47
|
"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
48
|
|
|
@@ -57,6 +61,8 @@ type BrowserRuntimeCompat = {
|
|
|
57
61
|
replaySessionId: string;
|
|
58
62
|
replayServerUrl?: string;
|
|
59
63
|
replayUrl?: string;
|
|
64
|
+
replayPublicKey?: string;
|
|
65
|
+
replayRrwebCdnUrl?: string;
|
|
60
66
|
updatedAt: string;
|
|
61
67
|
}) => void;
|
|
62
68
|
unregisterManagedBrowserExtensions: (sourceId: string) => void;
|
|
@@ -187,15 +193,44 @@ function resolvePluginStateFile(api: OpenClawPluginApi): string {
|
|
|
187
193
|
);
|
|
188
194
|
}
|
|
189
195
|
|
|
196
|
+
async function readJsonFileWithFallbackLocal<T>(
|
|
197
|
+
filePath: string,
|
|
198
|
+
fallback: T,
|
|
199
|
+
): Promise<{ value: T; exists: boolean }> {
|
|
200
|
+
try {
|
|
201
|
+
const raw = await fs.promises.readFile(filePath, "utf8");
|
|
202
|
+
return { value: JSON.parse(raw) as T, exists: true };
|
|
203
|
+
} catch (err) {
|
|
204
|
+
const code = (err as { code?: string }).code;
|
|
205
|
+
if (code === "ENOENT") {
|
|
206
|
+
return { value: fallback, exists: false };
|
|
207
|
+
}
|
|
208
|
+
return { value: fallback, exists: false };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function writeJsonFileAtomicallyLocal(filePath: string, value: unknown): Promise<void> {
|
|
213
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 }).catch(() => {
|
|
214
|
+
// Best-effort directory creation; write below will surface any real failure.
|
|
215
|
+
});
|
|
216
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.${crypto.randomUUID()}.tmp`;
|
|
217
|
+
await fs.promises.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
218
|
+
mode: 0o600,
|
|
219
|
+
});
|
|
220
|
+
await fs.promises.rename(tempPath, filePath);
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
async function loadReplayState(api: OpenClawPluginApi): Promise<ReplayStateStore> {
|
|
191
224
|
const filePath = resolvePluginStateFile(api);
|
|
192
|
-
const { value } = await
|
|
225
|
+
const { value } = await readJsonFileWithFallbackLocal<ReplayStateStore>(filePath, {
|
|
226
|
+
sessions: {},
|
|
227
|
+
});
|
|
193
228
|
return value;
|
|
194
229
|
}
|
|
195
230
|
|
|
196
231
|
async function saveReplayState(api: OpenClawPluginApi, state: ReplayStateStore): Promise<void> {
|
|
197
232
|
const filePath = resolvePluginStateFile(api);
|
|
198
|
-
await
|
|
233
|
+
await writeJsonFileAtomicallyLocal(filePath, state);
|
|
199
234
|
}
|
|
200
235
|
|
|
201
236
|
function buildReplaySessionState(params: {
|
|
@@ -225,7 +260,6 @@ function buildReplaySessionState(params: {
|
|
|
225
260
|
|
|
226
261
|
function describeReplayAvailability(params: {
|
|
227
262
|
config: RrwebReplayConfig;
|
|
228
|
-
extensionDir: string | null;
|
|
229
263
|
browserRuntimeAvailable: boolean;
|
|
230
264
|
}): { enabled: boolean; reason?: string } {
|
|
231
265
|
if (!params.config.enabled) {
|
|
@@ -237,12 +271,6 @@ function describeReplayAvailability(params: {
|
|
|
237
271
|
reason: LEGACY_BROWSER_RUNTIME_REASON,
|
|
238
272
|
};
|
|
239
273
|
}
|
|
240
|
-
if (!params.extensionDir) {
|
|
241
|
-
return {
|
|
242
|
-
enabled: false,
|
|
243
|
-
reason: "Replay extension directory is missing. Check rrweb-replay.extensionMode/path.",
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
274
|
const publicKey = resolveConfiguredSecret({
|
|
247
275
|
value: params.config.publicKey,
|
|
248
276
|
envVarName: params.config.publicKeyEnvVar,
|
|
@@ -313,6 +341,13 @@ function resolveBrowserProfileForReplay(params: {
|
|
|
313
341
|
return { name: profileName, driver };
|
|
314
342
|
}
|
|
315
343
|
|
|
344
|
+
function resolveConfiguredPublicKey(config: RrwebReplayConfig): string | undefined {
|
|
345
|
+
return resolveConfiguredSecret({
|
|
346
|
+
value: config.publicKey,
|
|
347
|
+
envVarName: config.publicKeyEnvVar,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
316
351
|
async function upsertReplaySession(params: {
|
|
317
352
|
api: OpenClawPluginApi;
|
|
318
353
|
config: RrwebReplayConfig;
|
|
@@ -425,6 +460,8 @@ async function armReplayContext(params: {
|
|
|
425
460
|
replaySessionId: replayEntry.replaySessionId,
|
|
426
461
|
replayServerUrl: replayEntry.replayServerUrl,
|
|
427
462
|
replayUrl: replayEntry.replayUrl,
|
|
463
|
+
replayPublicKey: resolveConfiguredPublicKey(params.pluginConfig),
|
|
464
|
+
replayRrwebCdnUrl: DEFAULT_RRWEB_CDN_URL,
|
|
428
465
|
updatedAt: new Date().toISOString(),
|
|
429
466
|
});
|
|
430
467
|
return await upsertReplaySession({
|
|
@@ -445,7 +482,6 @@ async function armReplayContext(params: {
|
|
|
445
482
|
function createReplaySessionInfoTool(params: {
|
|
446
483
|
api: OpenClawPluginApi;
|
|
447
484
|
pluginConfig: RrwebReplayConfig;
|
|
448
|
-
extensionDir: string | null;
|
|
449
485
|
browserRuntimePromise: Promise<BrowserRuntimeCompat | null>;
|
|
450
486
|
context: {
|
|
451
487
|
sessionKey?: string;
|
|
@@ -476,7 +512,6 @@ function createReplaySessionInfoTool(params: {
|
|
|
476
512
|
rawParams && typeof rawParams === "object" ? (rawParams as Record<string, unknown>) : null;
|
|
477
513
|
const availability = describeReplayAvailability({
|
|
478
514
|
config: params.pluginConfig,
|
|
479
|
-
extensionDir: params.extensionDir,
|
|
480
515
|
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
481
516
|
});
|
|
482
517
|
const activate = toolParams?.activate === true;
|
|
@@ -530,7 +565,7 @@ function createReplaySessionInfoTool(params: {
|
|
|
530
565
|
};
|
|
531
566
|
}
|
|
532
567
|
|
|
533
|
-
|
|
568
|
+
const rrwebReplayPlugin = {
|
|
534
569
|
id: RRWEB_PLUGIN_ID,
|
|
535
570
|
name: "rrweb Replay",
|
|
536
571
|
description: "Portable rrweb replay bootstrap for OpenClaw-managed browsers.",
|
|
@@ -554,8 +589,8 @@ export default definePluginEntry({
|
|
|
554
589
|
return;
|
|
555
590
|
}
|
|
556
591
|
if (!extensionDir) {
|
|
557
|
-
ctx.logger.
|
|
558
|
-
"[rrweb-replay] extension directory missing;
|
|
592
|
+
ctx.logger.info(
|
|
593
|
+
"[rrweb-replay] extension directory missing; continuing with runtime-side rrwebcloud upload only",
|
|
559
594
|
);
|
|
560
595
|
return;
|
|
561
596
|
}
|
|
@@ -578,7 +613,6 @@ export default definePluginEntry({
|
|
|
578
613
|
const browserRuntime = await browserRuntimePromise;
|
|
579
614
|
const availability = describeReplayAvailability({
|
|
580
615
|
config: pluginConfig,
|
|
581
|
-
extensionDir,
|
|
582
616
|
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
583
617
|
});
|
|
584
618
|
await upsertReplaySession({
|
|
@@ -621,7 +655,6 @@ export default definePluginEntry({
|
|
|
621
655
|
const browserRuntime = await browserRuntimePromise;
|
|
622
656
|
const availability = describeReplayAvailability({
|
|
623
657
|
config: pluginConfig,
|
|
624
|
-
extensionDir,
|
|
625
658
|
browserRuntimeAvailable: Boolean(browserRuntime),
|
|
626
659
|
});
|
|
627
660
|
if (event.toolName !== "browser" || pluginConfig.recordingPolicy !== "browser-only") {
|
|
@@ -649,7 +682,6 @@ export default definePluginEntry({
|
|
|
649
682
|
createReplaySessionInfoTool({
|
|
650
683
|
api,
|
|
651
684
|
pluginConfig,
|
|
652
|
-
extensionDir,
|
|
653
685
|
browserRuntimePromise,
|
|
654
686
|
context: {
|
|
655
687
|
sessionKey: context.sessionKey,
|
|
@@ -659,4 +691,6 @@ export default definePluginEntry({
|
|
|
659
691
|
{ name: "replay_session_info", optional: true },
|
|
660
692
|
);
|
|
661
693
|
},
|
|
662
|
-
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
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-3",
|
|
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
|