@harness-fe/runtime 3.0.1
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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/buffer.d.ts +13 -0
- package/dist/buffer.js +26 -0
- package/dist/capture.d.ts +47 -0
- package/dist/capture.js +112 -0
- package/dist/client.d.ts +82 -0
- package/dist/client.js +364 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.js +304 -0
- package/dist/dashboardUrl.d.ts +18 -0
- package/dist/dashboardUrl.js +20 -0
- package/dist/fetchPatch.d.ts +39 -0
- package/dist/fetchPatch.js +311 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +23 -0
- package/dist/outbox.d.ts +37 -0
- package/dist/outbox.js +80 -0
- package/dist/overlay.d.ts +68 -0
- package/dist/overlay.js +1946 -0
- package/dist/parent-inherit.d.ts +25 -0
- package/dist/parent-inherit.js +43 -0
- package/dist/recording.d.ts +27 -0
- package/dist/recording.js +86 -0
- package/dist/rrweb-types.d.ts +13 -0
- package/dist/rrweb-types.js +20 -0
- package/dist/selectors.d.ts +14 -0
- package/dist/selectors.js +91 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +111 -0
- package/dist/visitor.d.ts +28 -0
- package/dist/visitor.js +107 -0
- package/dist/xhrPatch.d.ts +26 -0
- package/dist/xhrPatch.js +269 -0
- package/package.json +50 -0
- package/src/buffer.test.ts +26 -0
- package/src/buffer.ts +29 -0
- package/src/capture.ts +126 -0
- package/src/client.test.ts +89 -0
- package/src/client.ts +423 -0
- package/src/commands.test.ts +128 -0
- package/src/commands.ts +335 -0
- package/src/dashboardUrl.test.ts +59 -0
- package/src/dashboardUrl.ts +36 -0
- package/src/fetchPatch.test.ts +203 -0
- package/src/fetchPatch.ts +371 -0
- package/src/index.ts +32 -0
- package/src/outbox.test.ts +115 -0
- package/src/outbox.ts +84 -0
- package/src/overlay.test.ts +319 -0
- package/src/overlay.ts +2070 -0
- package/src/parent-inherit.ts +54 -0
- package/src/recording.ts +88 -0
- package/src/rrweb-types.test.ts +40 -0
- package/src/rrweb-types.ts +24 -0
- package/src/selectors.test.ts +50 -0
- package/src/selectors.ts +103 -0
- package/src/snapshot.ts +112 -0
- package/src/visitor.ts +116 -0
- package/src/xhrPatch.test.ts +191 -0
- package/src/xhrPatch.ts +314 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 MorphixAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Morphicai/harness-fe/main/branding/logo.svg" alt="Harness-FE" width="96" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# @harness-fe/runtime
|
|
6
|
+
|
|
7
|
+
> Browser runtime client for [Harness-FE](https://github.com/Morphicai/harness-fe). Captures DOM/console/network events and executes commands from the MCP server in the user's real browser tab.
|
|
8
|
+
|
|
9
|
+
Auto-injected by the [Vite plugin](https://www.npmjs.com/package/@harness-fe/vite) / [Webpack plugin](https://www.npmjs.com/package/@harness-fe/webpack) — you typically install it as a peer of the plugin.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add -D @harness-fe/runtime
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What it does
|
|
18
|
+
|
|
19
|
+
- Connects to the MCP server via WebSocket on dev page load
|
|
20
|
+
- Streams `console.*`, `fetch`/`XHR`, `window.error`, `unhandledrejection` events
|
|
21
|
+
- Captures rrweb session recordings for replay
|
|
22
|
+
- Executes commands (`page.click`, `page.type`, `page.dom_query`, etc.)
|
|
23
|
+
- Renders the annotation overlay (point-and-task)
|
|
24
|
+
|
|
25
|
+
Disabled automatically in production builds.
|
|
26
|
+
|
|
27
|
+
## Manual usage (advanced)
|
|
28
|
+
|
|
29
|
+
For non-Vite/Webpack setups:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { RuntimeClient } from '@harness-fe/runtime';
|
|
33
|
+
|
|
34
|
+
const client = new RuntimeClient({
|
|
35
|
+
projectId: 'my-app',
|
|
36
|
+
wsUrl: 'ws://localhost:47729',
|
|
37
|
+
});
|
|
38
|
+
client.start();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Docs
|
|
42
|
+
|
|
43
|
+
- [Root README](https://github.com/Morphicai/harness-fe#readme)
|
|
44
|
+
- [Architecture](https://github.com/Morphicai/harness-fe/blob/main/ARCHITECTURE.md)
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT
|
package/dist/buffer.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded ring buffer for console / network / error tails.
|
|
3
|
+
* O(1) push, O(n) drain. Fixed capacity to keep page memory bounded.
|
|
4
|
+
*/
|
|
5
|
+
export declare class RingBuffer<T> {
|
|
6
|
+
private readonly capacity;
|
|
7
|
+
private items;
|
|
8
|
+
constructor(capacity: number);
|
|
9
|
+
push(item: T): void;
|
|
10
|
+
tail(n: number): T[];
|
|
11
|
+
size(): number;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
package/dist/buffer.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded ring buffer for console / network / error tails.
|
|
3
|
+
* O(1) push, O(n) drain. Fixed capacity to keep page memory bounded.
|
|
4
|
+
*/
|
|
5
|
+
export class RingBuffer {
|
|
6
|
+
capacity;
|
|
7
|
+
items = [];
|
|
8
|
+
constructor(capacity) {
|
|
9
|
+
this.capacity = capacity;
|
|
10
|
+
}
|
|
11
|
+
push(item) {
|
|
12
|
+
this.items.push(item);
|
|
13
|
+
if (this.items.length > this.capacity) {
|
|
14
|
+
this.items.splice(0, this.items.length - this.capacity);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
tail(n) {
|
|
18
|
+
return this.items.slice(Math.max(0, this.items.length - n));
|
|
19
|
+
}
|
|
20
|
+
size() {
|
|
21
|
+
return this.items.length;
|
|
22
|
+
}
|
|
23
|
+
clear() {
|
|
24
|
+
this.items = [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console + network + error capture. Monkey-patches in place once on init.
|
|
3
|
+
*
|
|
4
|
+
* Network capture covers fetch + XMLHttpRequest. Body capture is opt-in
|
|
5
|
+
* per-request to keep memory bounded.
|
|
6
|
+
*/
|
|
7
|
+
import { RingBuffer } from './buffer.js';
|
|
8
|
+
export declare class CaptureStore {
|
|
9
|
+
readonly console: RingBuffer<{
|
|
10
|
+
ts: number;
|
|
11
|
+
level: "error" | "log" | "info" | "warn" | "debug";
|
|
12
|
+
args: unknown[];
|
|
13
|
+
source?: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
readonly network: RingBuffer<{
|
|
16
|
+
ts: number;
|
|
17
|
+
method: string;
|
|
18
|
+
url: string;
|
|
19
|
+
id?: string | undefined;
|
|
20
|
+
phase?: "req" | "res" | undefined;
|
|
21
|
+
status?: number | undefined;
|
|
22
|
+
durationMs?: number | undefined;
|
|
23
|
+
requestHeaders?: Record<string, string> | undefined;
|
|
24
|
+
responseHeaders?: Record<string, string> | undefined;
|
|
25
|
+
requestBody?: unknown;
|
|
26
|
+
responseBody?: unknown;
|
|
27
|
+
requestBodyTruncated?: boolean | undefined;
|
|
28
|
+
responseBodyTruncated?: boolean | undefined;
|
|
29
|
+
error?: string | undefined;
|
|
30
|
+
}>;
|
|
31
|
+
readonly errors: RingBuffer<{
|
|
32
|
+
ts: number;
|
|
33
|
+
message: string;
|
|
34
|
+
stack?: string | undefined;
|
|
35
|
+
source?: string | undefined;
|
|
36
|
+
}>;
|
|
37
|
+
private installed;
|
|
38
|
+
private fetchDispose?;
|
|
39
|
+
private xhrDispose?;
|
|
40
|
+
install(onEvent: (name: string, payload: unknown) => void): void;
|
|
41
|
+
dispose(): void;
|
|
42
|
+
private installConsole;
|
|
43
|
+
private installFetch;
|
|
44
|
+
private installXhr;
|
|
45
|
+
private installErrors;
|
|
46
|
+
}
|
|
47
|
+
export declare function getCaptureStore(): CaptureStore;
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console + network + error capture. Monkey-patches in place once on init.
|
|
3
|
+
*
|
|
4
|
+
* Network capture covers fetch + XMLHttpRequest. Body capture is opt-in
|
|
5
|
+
* per-request to keep memory bounded.
|
|
6
|
+
*/
|
|
7
|
+
import { RingBuffer } from './buffer.js';
|
|
8
|
+
import { installFetchPatch } from './fetchPatch.js';
|
|
9
|
+
import { installXhrPatch } from './xhrPatch.js';
|
|
10
|
+
const CONSOLE_CAP = 500;
|
|
11
|
+
const NETWORK_CAP = 200;
|
|
12
|
+
const ERROR_CAP = 200;
|
|
13
|
+
export class CaptureStore {
|
|
14
|
+
console = new RingBuffer(CONSOLE_CAP);
|
|
15
|
+
network = new RingBuffer(NETWORK_CAP);
|
|
16
|
+
errors = new RingBuffer(ERROR_CAP);
|
|
17
|
+
installed = false;
|
|
18
|
+
fetchDispose;
|
|
19
|
+
xhrDispose;
|
|
20
|
+
install(onEvent) {
|
|
21
|
+
if (this.installed)
|
|
22
|
+
return;
|
|
23
|
+
this.installed = true;
|
|
24
|
+
this.installConsole(onEvent);
|
|
25
|
+
this.installFetch(onEvent);
|
|
26
|
+
this.installXhr(onEvent);
|
|
27
|
+
this.installErrors(onEvent);
|
|
28
|
+
}
|
|
29
|
+
dispose() {
|
|
30
|
+
this.fetchDispose?.();
|
|
31
|
+
this.fetchDispose = undefined;
|
|
32
|
+
this.xhrDispose?.();
|
|
33
|
+
this.xhrDispose = undefined;
|
|
34
|
+
this.installed = false;
|
|
35
|
+
}
|
|
36
|
+
installConsole(onEvent) {
|
|
37
|
+
const methods = ['log', 'info', 'warn', 'error', 'debug'];
|
|
38
|
+
for (const level of methods) {
|
|
39
|
+
const original = console[level].bind(console);
|
|
40
|
+
console[level] = (...args) => {
|
|
41
|
+
const entry = {
|
|
42
|
+
ts: Date.now(),
|
|
43
|
+
level,
|
|
44
|
+
args: args.map(safeClone),
|
|
45
|
+
};
|
|
46
|
+
this.console.push(entry);
|
|
47
|
+
onEvent('console', entry);
|
|
48
|
+
original(...args);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
installFetch(onEvent) {
|
|
53
|
+
this.fetchDispose = installFetchPatch({
|
|
54
|
+
onEntry: (entry) => {
|
|
55
|
+
this.network.push(entry);
|
|
56
|
+
onEvent('network', entry);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
installXhr(onEvent) {
|
|
61
|
+
this.xhrDispose = installXhrPatch({
|
|
62
|
+
onEntry: (entry) => {
|
|
63
|
+
this.network.push(entry);
|
|
64
|
+
onEvent('network', entry);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
installErrors(onEvent) {
|
|
69
|
+
if (typeof window === 'undefined')
|
|
70
|
+
return;
|
|
71
|
+
window.addEventListener('error', (e) => {
|
|
72
|
+
const entry = {
|
|
73
|
+
ts: Date.now(),
|
|
74
|
+
message: e.message,
|
|
75
|
+
stack: e.error?.stack,
|
|
76
|
+
source: e.filename ? `${e.filename}:${e.lineno}:${e.colno}` : undefined,
|
|
77
|
+
};
|
|
78
|
+
this.errors.push(entry);
|
|
79
|
+
onEvent('error', entry);
|
|
80
|
+
});
|
|
81
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
82
|
+
const reason = e.reason;
|
|
83
|
+
const message = reason instanceof Error ? reason.message : String(reason ?? 'unhandled rejection');
|
|
84
|
+
const stack = reason instanceof Error ? reason.stack : undefined;
|
|
85
|
+
const entry = {
|
|
86
|
+
ts: Date.now(),
|
|
87
|
+
message: `Unhandled: ${message}`,
|
|
88
|
+
stack,
|
|
89
|
+
};
|
|
90
|
+
this.errors.push(entry);
|
|
91
|
+
onEvent('error', entry);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let captureStoreSingleton;
|
|
96
|
+
export function getCaptureStore() {
|
|
97
|
+
captureStoreSingleton ??= new CaptureStore();
|
|
98
|
+
return captureStoreSingleton;
|
|
99
|
+
}
|
|
100
|
+
function safeClone(value) {
|
|
101
|
+
if (value === null)
|
|
102
|
+
return null;
|
|
103
|
+
if (typeof value === 'object') {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(JSON.stringify(value));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return String(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime client core. Connects to the MCP server over WS, executes
|
|
3
|
+
* commands dispatched by the server, and forwards page events back.
|
|
4
|
+
*
|
|
5
|
+
* Started lazily by `auto-start.ts` when the script is imported.
|
|
6
|
+
*/
|
|
7
|
+
import { COMMAND } from '@harness-fe/protocol';
|
|
8
|
+
import type { QueryMethod } from '@harness-fe/protocol';
|
|
9
|
+
export interface ClientOptions {
|
|
10
|
+
projectId: string;
|
|
11
|
+
mcpUrl?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Build artifact id, threaded through `window.__HARNESS_FE__.buildId`.
|
|
14
|
+
* Stamped on every event so agents can trace "what code was running".
|
|
15
|
+
*/
|
|
16
|
+
buildId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Parent project's id. Set by the plugin when the host app declares it,
|
|
19
|
+
* or auto-inferred at runtime via `tryInheritFromParent()` when this
|
|
20
|
+
* runtime is loaded inside a same-origin iframe.
|
|
21
|
+
*/
|
|
22
|
+
parentProjectId?: string;
|
|
23
|
+
/** Optional human-readable name; mostly used by the project tree. */
|
|
24
|
+
displayName?: string;
|
|
25
|
+
/**
|
|
26
|
+
* App-supplied user identifier (e.g. supabase.user.id, auth0 sub, …).
|
|
27
|
+
* Optional. When absent, traffic is treated as anonymous (only stitched
|
|
28
|
+
* by visitorId). Propagated by HarnessScript via window.__HARNESS_FE__.userId.
|
|
29
|
+
*/
|
|
30
|
+
userId?: string;
|
|
31
|
+
}
|
|
32
|
+
export { tryInheritFromParent } from './parent-inherit.js';
|
|
33
|
+
export type { ParentInheritance } from './parent-inherit.js';
|
|
34
|
+
export declare class RuntimeClient {
|
|
35
|
+
private readonly opts;
|
|
36
|
+
private ws?;
|
|
37
|
+
readonly tabId: string;
|
|
38
|
+
readonly sessionId: string;
|
|
39
|
+
readonly visitorId: string;
|
|
40
|
+
readonly parentProjectId?: string;
|
|
41
|
+
/** Read-only accessors exposed for the in-page info panel. */
|
|
42
|
+
get projectId(): string;
|
|
43
|
+
get buildId(): string | undefined;
|
|
44
|
+
get displayName(): string | undefined;
|
|
45
|
+
get userId(): string | undefined;
|
|
46
|
+
get mcpUrl(): string | undefined;
|
|
47
|
+
/** WebSocket state: 'connecting' | 'open' | 'closed'. */
|
|
48
|
+
getConnectionState(): 'connecting' | 'open' | 'closed';
|
|
49
|
+
private pageLoadSent;
|
|
50
|
+
private readonly ctx;
|
|
51
|
+
private readonly recorder;
|
|
52
|
+
private reconnectAttempts;
|
|
53
|
+
private closed;
|
|
54
|
+
private static readonly MAX_OUTBOX_FRAMES;
|
|
55
|
+
private static readonly MAX_OUTBOX_BYTES;
|
|
56
|
+
private readonly outbox;
|
|
57
|
+
constructor(opts: ClientOptions);
|
|
58
|
+
start(): void;
|
|
59
|
+
stop(): void;
|
|
60
|
+
private connect;
|
|
61
|
+
private onOpen;
|
|
62
|
+
private onClose;
|
|
63
|
+
private onMessage;
|
|
64
|
+
private onQueryResponse;
|
|
65
|
+
private onHelloAck;
|
|
66
|
+
private handleCommand;
|
|
67
|
+
sendEvent(name: string, payload: unknown): void;
|
|
68
|
+
/**
|
|
69
|
+
* Request/reply RPC to the daemon. Currently used by the in-page
|
|
70
|
+
* overlay to fetch / mutate the visitor's own tasks. Resolves with the
|
|
71
|
+
* remote `result`, rejects with the remote `error.message` (or a
|
|
72
|
+
* timeout after 10 s).
|
|
73
|
+
*/
|
|
74
|
+
query<TResult = unknown>(method: QueryMethod, args?: unknown, timeoutMs?: number): Promise<TResult>;
|
|
75
|
+
private pendingQueries;
|
|
76
|
+
private send;
|
|
77
|
+
private drainOutbox;
|
|
78
|
+
}
|
|
79
|
+
/** Pull the well-known config object planted by the Vite plugin on window. */
|
|
80
|
+
export declare function readInjectedConfig(): ClientOptions;
|
|
81
|
+
/** Re-export command names for outside callers. */
|
|
82
|
+
export { COMMAND };
|