@wordbricks/playwright-mcp 0.1.22 → 0.1.24
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/package.json +6 -6
- package/lib/browserContextFactory.js +0 -399
- package/lib/browserServerBackend.js +0 -86
- package/lib/config.js +0 -300
- package/lib/context.js +0 -311
- package/lib/extension/cdpRelay.js +0 -352
- package/lib/extension/extensionContextFactory.js +0 -56
- package/lib/frameworkPatterns.js +0 -35
- package/lib/hooks/antiBotDetectionHook.js +0 -178
- package/lib/hooks/core.js +0 -145
- package/lib/hooks/eventConsumer.js +0 -52
- package/lib/hooks/events.js +0 -42
- package/lib/hooks/formatToolCallEvent.js +0 -12
- package/lib/hooks/frameworkStateHook.js +0 -182
- package/lib/hooks/grouping.js +0 -72
- package/lib/hooks/jsonLdDetectionHook.js +0 -182
- package/lib/hooks/networkFilters.js +0 -82
- package/lib/hooks/networkSetup.js +0 -61
- package/lib/hooks/networkTrackingHook.js +0 -67
- package/lib/hooks/pageHeightHook.js +0 -75
- package/lib/hooks/registry.js +0 -41
- package/lib/hooks/requireTabHook.js +0 -26
- package/lib/hooks/schema.js +0 -89
- package/lib/hooks/waitHook.js +0 -33
- package/lib/index.js +0 -41
- package/lib/mcp/inProcessTransport.js +0 -71
- package/lib/mcp/proxyBackend.js +0 -130
- package/lib/mcp/server.js +0 -91
- package/lib/mcp/tool.js +0 -44
- package/lib/mcp/transport.js +0 -188
- package/lib/playwrightTransformer.js +0 -520
- package/lib/program.js +0 -112
- package/lib/response.js +0 -192
- package/lib/sessionLog.js +0 -123
- package/lib/tab.js +0 -251
- package/lib/tools/common.js +0 -55
- package/lib/tools/console.js +0 -33
- package/lib/tools/dialogs.js +0 -50
- package/lib/tools/evaluate.js +0 -62
- package/lib/tools/extractFrameworkState.js +0 -225
- package/lib/tools/files.js +0 -48
- package/lib/tools/form.js +0 -66
- package/lib/tools/getSnapshot.js +0 -36
- package/lib/tools/getVisibleHtml.js +0 -68
- package/lib/tools/install.js +0 -51
- package/lib/tools/keyboard.js +0 -83
- package/lib/tools/mouse.js +0 -97
- package/lib/tools/navigate.js +0 -66
- package/lib/tools/network.js +0 -121
- package/lib/tools/networkDetail.js +0 -238
- package/lib/tools/networkSearch/bodySearch.js +0 -161
- package/lib/tools/networkSearch/grouping.js +0 -37
- package/lib/tools/networkSearch/helpers.js +0 -32
- package/lib/tools/networkSearch/searchHtml.js +0 -76
- package/lib/tools/networkSearch/types.js +0 -1
- package/lib/tools/networkSearch/urlSearch.js +0 -124
- package/lib/tools/networkSearch.js +0 -278
- package/lib/tools/pdf.js +0 -41
- package/lib/tools/repl.js +0 -414
- package/lib/tools/screenshot.js +0 -103
- package/lib/tools/scroll.js +0 -131
- package/lib/tools/snapshot.js +0 -161
- package/lib/tools/tabs.js +0 -62
- package/lib/tools/tool.js +0 -35
- package/lib/tools/utils.js +0 -78
- package/lib/tools/wait.js +0 -60
- package/lib/tools.js +0 -68
- package/lib/utils/adBlockFilter.js +0 -90
- package/lib/utils/codegen.js +0 -55
- package/lib/utils/extensionPath.js +0 -10
- package/lib/utils/fileUtils.js +0 -40
- package/lib/utils/graphql.js +0 -269
- package/lib/utils/guid.js +0 -22
- package/lib/utils/httpServer.js +0 -39
- package/lib/utils/log.js +0 -21
- package/lib/utils/manualPromise.js +0 -111
- package/lib/utils/networkFormat.js +0 -14
- package/lib/utils/package.js +0 -20
- package/lib/utils/result.js +0 -2
- package/lib/utils/sanitizeHtml.js +0 -130
- package/lib/utils/truncate.js +0 -103
- package/lib/utils/withTimeout.js +0 -7
- package/src/index.ts +0 -60
package/lib/hooks/core.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { reduce } from "@fxts/core";
|
|
2
|
-
import { consumeEvents } from "./eventConsumer.js";
|
|
3
|
-
import { getEventStore, trackEvent } from "./events.js";
|
|
4
|
-
import { toolNameSchema } from "./schema.js";
|
|
5
|
-
export { Err, Ok } from "../utils/result.js";
|
|
6
|
-
export const runHook = async (hook, ctx) => {
|
|
7
|
-
const result = await hook.handler(ctx);
|
|
8
|
-
if (!result.ok)
|
|
9
|
-
throw result.error;
|
|
10
|
-
return ctx;
|
|
11
|
-
};
|
|
12
|
-
export const hookRegistryMap = new WeakMap();
|
|
13
|
-
export const createHookRegistry = () => ({
|
|
14
|
-
tools: new Map(),
|
|
15
|
-
});
|
|
16
|
-
export const setToolHooks = (registry, toolHooks) => ({
|
|
17
|
-
tools: new Map([...registry.tools, [toolHooks.toolName, toolHooks]]),
|
|
18
|
-
});
|
|
19
|
-
export const getToolHooks = (registry, toolName) => {
|
|
20
|
-
return registry.tools.get(toolName);
|
|
21
|
-
};
|
|
22
|
-
export const wrapToolWithHooks = (tool, registry) => {
|
|
23
|
-
const parsedName = toolNameSchema.safeParse(tool.schema.name);
|
|
24
|
-
if (!parsedName.success)
|
|
25
|
-
return tool; // Tool name not in our schema, don't apply hooks
|
|
26
|
-
// NOTE: This means tool call events won't be tracked for tools not in toolNameSchema.
|
|
27
|
-
// All tools exposed by this MCP server should be added to the schema.
|
|
28
|
-
const toolName = parsedName.data;
|
|
29
|
-
const toolHooks = getToolHooks(registry, toolName);
|
|
30
|
-
// Even if no hooks configured, we still need to consume events and track tool calls
|
|
31
|
-
if (!toolHooks ||
|
|
32
|
-
(toolHooks.preHooks.length === 0 && toolHooks.postHooks.length === 0)) {
|
|
33
|
-
return {
|
|
34
|
-
...tool,
|
|
35
|
-
handle: async (context, params, response) => {
|
|
36
|
-
const eventStore = getEventStore(context);
|
|
37
|
-
// Consume pre-tool events
|
|
38
|
-
consumeEvents(context, eventStore, response);
|
|
39
|
-
// Track tool execution
|
|
40
|
-
const startTime = Date.now();
|
|
41
|
-
let success = true;
|
|
42
|
-
try {
|
|
43
|
-
await tool.handle(context, params, response);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
success = false;
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
finally {
|
|
50
|
-
// Record tool call completion
|
|
51
|
-
const executionTime = Date.now() - startTime;
|
|
52
|
-
trackEvent(context, {
|
|
53
|
-
type: "tool-call",
|
|
54
|
-
data: {
|
|
55
|
-
toolName,
|
|
56
|
-
params: params,
|
|
57
|
-
executionTime,
|
|
58
|
-
success,
|
|
59
|
-
},
|
|
60
|
-
timestamp: startTime,
|
|
61
|
-
});
|
|
62
|
-
// Consume post-tool events
|
|
63
|
-
consumeEvents(context, eventStore, response);
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
...tool,
|
|
70
|
-
handle: async (context, params, response) => {
|
|
71
|
-
const eventStore = getEventStore(context);
|
|
72
|
-
// Consume pre-tool events
|
|
73
|
-
consumeEvents(context, eventStore, response);
|
|
74
|
-
// Track tool execution
|
|
75
|
-
const startTime = Date.now();
|
|
76
|
-
// Run pre-hooks
|
|
77
|
-
const hookContext = {
|
|
78
|
-
context,
|
|
79
|
-
tab: context.currentTab(),
|
|
80
|
-
params,
|
|
81
|
-
toolName,
|
|
82
|
-
response,
|
|
83
|
-
eventStore,
|
|
84
|
-
};
|
|
85
|
-
try {
|
|
86
|
-
await reduce(async (ctx, hook) => runHook(hook, await ctx), Promise.resolve(hookContext), toolHooks.preHooks);
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
// Pre-hook already handled messaging (e.g., require-tab pre-hook sets tabs section)
|
|
90
|
-
// Avoid adding a duplicate error line in the Result section.
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
// Run original tool
|
|
94
|
-
let success = true;
|
|
95
|
-
try {
|
|
96
|
-
await tool.handle(context, params, response);
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
success = false;
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
// Record tool call completion
|
|
104
|
-
const executionTime = Date.now() - startTime;
|
|
105
|
-
trackEvent(context, {
|
|
106
|
-
type: "tool-call",
|
|
107
|
-
data: {
|
|
108
|
-
toolName,
|
|
109
|
-
params: params,
|
|
110
|
-
executionTime,
|
|
111
|
-
success,
|
|
112
|
-
},
|
|
113
|
-
timestamp: startTime,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
// Run post-hooks
|
|
117
|
-
const postHookContext = {
|
|
118
|
-
context,
|
|
119
|
-
tab: context.currentTab(),
|
|
120
|
-
params,
|
|
121
|
-
toolName,
|
|
122
|
-
response,
|
|
123
|
-
eventStore: getEventStore(context),
|
|
124
|
-
};
|
|
125
|
-
try {
|
|
126
|
-
await reduce(async (ctx, hook) => runHook(hook, await ctx), Promise.resolve(postHookContext), toolHooks.postHooks);
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
response.addError(error instanceof Error ? error.message : "Post-hook failed");
|
|
130
|
-
}
|
|
131
|
-
// Consume post-tool events
|
|
132
|
-
consumeEvents(context, eventStore, response);
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
};
|
|
136
|
-
export const getHookRegistry = (context) => {
|
|
137
|
-
const registry = hookRegistryMap.get(context);
|
|
138
|
-
return registry || createHookRegistry();
|
|
139
|
-
};
|
|
140
|
-
export const applyHooksToTools = (tools, context) => {
|
|
141
|
-
const registry = getHookRegistry(context);
|
|
142
|
-
if (registry.tools.size === 0)
|
|
143
|
-
return tools;
|
|
144
|
-
return tools.map((tool) => wrapToolWithHooks(tool, registry));
|
|
145
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { formatAntiBotEvent, getAntiBotProviderConfigs, } from "./antiBotDetectionHook.js";
|
|
2
|
-
import { getEventsAfter, isEventType, updateLastSeenId } from "./events.js";
|
|
3
|
-
import { formatToolCallEvent } from "./formatToolCallEvent.js";
|
|
4
|
-
import { formatFrameworkStateEvent } from "./frameworkStateHook.js";
|
|
5
|
-
import { planGroupedMessages } from "./grouping.js";
|
|
6
|
-
import { formatJsonLdEvent } from "./jsonLdDetectionHook.js";
|
|
7
|
-
import { isAntiBotUrl } from "./networkFilters.js";
|
|
8
|
-
import { formatNetworkEvent } from "./networkTrackingHook.js";
|
|
9
|
-
import { formatPageHeightEvent } from "./pageHeightHook.js";
|
|
10
|
-
import { formatWaitEvent } from "./waitHook.js";
|
|
11
|
-
const eventFormatters = {
|
|
12
|
-
wait: formatWaitEvent,
|
|
13
|
-
"page-height-change": formatPageHeightEvent,
|
|
14
|
-
"network-request": formatNetworkEvent,
|
|
15
|
-
"tool-call": formatToolCallEvent,
|
|
16
|
-
"framework-state": formatFrameworkStateEvent,
|
|
17
|
-
"json-ld": formatJsonLdEvent,
|
|
18
|
-
"anti-bot": formatAntiBotEvent,
|
|
19
|
-
};
|
|
20
|
-
const formatEvent = (event) => {
|
|
21
|
-
const formatter = eventFormatters[event.type];
|
|
22
|
-
return formatter(event);
|
|
23
|
-
};
|
|
24
|
-
const consumeEvent = (event, response, plan) => {
|
|
25
|
-
if (plan.skipIds.has(event.id))
|
|
26
|
-
return;
|
|
27
|
-
const replacement = plan.replacementById.get(event.id);
|
|
28
|
-
const formattedMessage = replacement ?? formatEvent(event);
|
|
29
|
-
response.addEvent(`[${event.id}] ${formattedMessage}`);
|
|
30
|
-
};
|
|
31
|
-
const shouldHideEvent = (event) => {
|
|
32
|
-
const isNetworkRequest = isEventType("network-request");
|
|
33
|
-
if (!isNetworkRequest(event))
|
|
34
|
-
return false;
|
|
35
|
-
if (isAntiBotUrl(event.data.url))
|
|
36
|
-
return true;
|
|
37
|
-
const configs = getAntiBotProviderConfigs().filter((config) => config.provider === "cloudflare-turnstile");
|
|
38
|
-
return configs.some((config) => config.match(event));
|
|
39
|
-
};
|
|
40
|
-
export const consumeEvents = (context, eventStore, response) => {
|
|
41
|
-
const unconsumedEvents = getEventsAfter(eventStore, eventStore.lastSeenEventId);
|
|
42
|
-
if (unconsumedEvents.length === 0)
|
|
43
|
-
return;
|
|
44
|
-
const visibleEvents = unconsumedEvents.filter((event) => !shouldHideEvent(event));
|
|
45
|
-
const plan = planGroupedMessages(visibleEvents);
|
|
46
|
-
// Consume all events in chronological order
|
|
47
|
-
for (const event of visibleEvents)
|
|
48
|
-
consumeEvent(event, response, plan);
|
|
49
|
-
// Update last seen event ID
|
|
50
|
-
const latestEvent = unconsumedEvents[unconsumedEvents.length - 1];
|
|
51
|
-
updateLastSeenId(context, latestEvent.id);
|
|
52
|
-
};
|
package/lib/hooks/events.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { filter, pipe, toArray } from "@fxts/core";
|
|
2
|
-
export const isEventType = (type) => (event) => event.type === type;
|
|
3
|
-
export const createEventStore = () => ({
|
|
4
|
-
events: new Map(),
|
|
5
|
-
lastSeenEventId: undefined,
|
|
6
|
-
nextEventId: 1,
|
|
7
|
-
});
|
|
8
|
-
export const trackEvent = (context, params) => {
|
|
9
|
-
const store = getEventStore(context);
|
|
10
|
-
const eventId = store.nextEventId++;
|
|
11
|
-
const event = {
|
|
12
|
-
id: eventId,
|
|
13
|
-
type: params.type,
|
|
14
|
-
data: params.data,
|
|
15
|
-
timestamp: params.timestamp ?? Date.now(),
|
|
16
|
-
};
|
|
17
|
-
store.events.set(eventId, event);
|
|
18
|
-
return eventId;
|
|
19
|
-
};
|
|
20
|
-
export const updateLastSeenId = (context, eventId) => {
|
|
21
|
-
const store = getEventStore(context);
|
|
22
|
-
store.lastSeenEventId = eventId;
|
|
23
|
-
return context;
|
|
24
|
-
};
|
|
25
|
-
export const getEventsAfter = (store, afterEventId) => {
|
|
26
|
-
if (!afterEventId) {
|
|
27
|
-
return pipe(store.events.values(), toArray);
|
|
28
|
-
}
|
|
29
|
-
return pipe(store.events.values(), filter((event) => event.id > afterEventId), toArray);
|
|
30
|
-
};
|
|
31
|
-
const eventStoreMap = new WeakMap();
|
|
32
|
-
export const getEventStore = (context) => {
|
|
33
|
-
let store = eventStoreMap.get(context);
|
|
34
|
-
if (!store) {
|
|
35
|
-
store = createEventStore();
|
|
36
|
-
eventStoreMap.set(context, store);
|
|
37
|
-
}
|
|
38
|
-
return store;
|
|
39
|
-
};
|
|
40
|
-
export const setEventStore = (context, store) => {
|
|
41
|
-
eventStoreMap.set(context, store);
|
|
42
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export const formatToolCallEvent = (event) => {
|
|
2
|
-
const { toolName, params, executionTime, success } = event.data;
|
|
3
|
-
// Format parameters (truncate if too long)
|
|
4
|
-
const paramStr = params && Object.keys(params).length > 0
|
|
5
|
-
? ` with params: ${JSON.stringify(params, null, 0).slice(0, 100)}`
|
|
6
|
-
: "";
|
|
7
|
-
// Format execution time if available
|
|
8
|
-
const timeStr = executionTime !== undefined ? ` (${executionTime}ms)` : "";
|
|
9
|
-
// Format success status if available
|
|
10
|
-
const statusStr = success !== undefined ? (success ? " ✓" : " ✗") : "";
|
|
11
|
-
return `Tool ${toolName}${paramStr}${timeStr}${statusStr}`;
|
|
12
|
-
};
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { FRAMEWORK_STATE_PATTERNS, MAX_DISPLAY_ITEMS, } from "../frameworkPatterns.js";
|
|
2
|
-
import { Ok } from "../utils/result.js";
|
|
3
|
-
import { trackEvent } from "./events.js";
|
|
4
|
-
import { hookNameSchema } from "./schema.js";
|
|
5
|
-
const pageFrameworkStates = new WeakMap();
|
|
6
|
-
const seenFrameworkKeysByContext = new WeakMap();
|
|
7
|
-
const getSeenFrameworkKeys = (context) => {
|
|
8
|
-
let set = seenFrameworkKeysByContext.get(context);
|
|
9
|
-
if (!set) {
|
|
10
|
-
set = new Set();
|
|
11
|
-
seenFrameworkKeysByContext.set(context, set);
|
|
12
|
-
}
|
|
13
|
-
return set;
|
|
14
|
-
};
|
|
15
|
-
export const frameworkStatePreHook = {
|
|
16
|
-
name: hookNameSchema.enum["framework-state-pre"],
|
|
17
|
-
handler: async (context) => {
|
|
18
|
-
const frameworkState = await detectFrameworkState(context);
|
|
19
|
-
if (frameworkState) {
|
|
20
|
-
// Store the initial state
|
|
21
|
-
if (context.tab?.page)
|
|
22
|
-
pageFrameworkStates.set(context.tab.page, frameworkState);
|
|
23
|
-
// Track event for newly detected framework state
|
|
24
|
-
const newKeys = Object.keys(frameworkState).filter((key) => !getSeenFrameworkKeys(context.context).has(key));
|
|
25
|
-
if (newKeys.length > 0) {
|
|
26
|
-
trackEvent(context.context, {
|
|
27
|
-
type: "framework-state",
|
|
28
|
-
data: {
|
|
29
|
-
state: frameworkState,
|
|
30
|
-
action: "detected",
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
// Mark keys as seen
|
|
34
|
-
newKeys.forEach((key) => getSeenFrameworkKeys(context.context).add(key));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return Ok(undefined);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
export const frameworkStatePostHook = {
|
|
41
|
-
name: hookNameSchema.enum["framework-state-post"],
|
|
42
|
-
handler: async (context) => {
|
|
43
|
-
const newFrameworkState = await detectFrameworkState(context);
|
|
44
|
-
const initialState = context.tab?.page
|
|
45
|
-
? pageFrameworkStates.get(context.tab.page)
|
|
46
|
-
: undefined;
|
|
47
|
-
if (newFrameworkState) {
|
|
48
|
-
const changes = [];
|
|
49
|
-
if (initialState) {
|
|
50
|
-
// Compare states
|
|
51
|
-
const allKeys = new Set([
|
|
52
|
-
...Object.keys(initialState),
|
|
53
|
-
...Object.keys(newFrameworkState),
|
|
54
|
-
]);
|
|
55
|
-
for (const key of allKeys) {
|
|
56
|
-
const initVal = initialState[key];
|
|
57
|
-
const currVal = newFrameworkState[key];
|
|
58
|
-
if (!initVal && currVal)
|
|
59
|
-
changes.push(`+ ${key}: ${formatValue(currVal)}`);
|
|
60
|
-
else if (initVal && !currVal)
|
|
61
|
-
changes.push(`- ${key}`);
|
|
62
|
-
else if (initVal &&
|
|
63
|
-
currVal &&
|
|
64
|
-
JSON.stringify(initVal) !== JSON.stringify(currVal))
|
|
65
|
-
changes.push(`~ ${key}: changed`);
|
|
66
|
-
}
|
|
67
|
-
if (changes.length > 0) {
|
|
68
|
-
trackEvent(context.context, {
|
|
69
|
-
type: "framework-state",
|
|
70
|
-
data: {
|
|
71
|
-
state: newFrameworkState,
|
|
72
|
-
changes,
|
|
73
|
-
action: "changed",
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
// No initial state, but we have state now
|
|
80
|
-
const newKeys = Object.keys(newFrameworkState).filter((key) => !getSeenFrameworkKeys(context.context).has(key));
|
|
81
|
-
if (newKeys.length > 0) {
|
|
82
|
-
trackEvent(context.context, {
|
|
83
|
-
type: "framework-state",
|
|
84
|
-
data: {
|
|
85
|
-
state: newFrameworkState,
|
|
86
|
-
action: "detected",
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
newKeys.forEach((key) => getSeenFrameworkKeys(context.context).add(key));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
// Update stored state
|
|
93
|
-
if (context.tab?.page)
|
|
94
|
-
pageFrameworkStates.set(context.tab.page, newFrameworkState);
|
|
95
|
-
}
|
|
96
|
-
return Ok(undefined);
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
async function detectFrameworkState(context) {
|
|
100
|
-
if (!context.tab?.page)
|
|
101
|
-
return null;
|
|
102
|
-
const result = await context.tab.page.evaluate((patterns) => {
|
|
103
|
-
const state = {};
|
|
104
|
-
const MAX_ITEMS = 5;
|
|
105
|
-
// Scan window object for these patterns
|
|
106
|
-
for (const pattern of patterns) {
|
|
107
|
-
if (pattern in window) {
|
|
108
|
-
try {
|
|
109
|
-
const value = window[pattern];
|
|
110
|
-
// Only capture if it's a non-empty object or has meaningful content
|
|
111
|
-
if (value &&
|
|
112
|
-
(typeof value === "object" || typeof value === "string")) {
|
|
113
|
-
state[pattern] =
|
|
114
|
-
typeof value === "object"
|
|
115
|
-
? {
|
|
116
|
-
type: "object",
|
|
117
|
-
keys: Object.keys(value).slice(0, MAX_ITEMS * 2),
|
|
118
|
-
}
|
|
119
|
-
: {
|
|
120
|
-
type: typeof value,
|
|
121
|
-
preview: String(value).slice(0, 200),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
// Skip inaccessible properties
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// Also check for React Fiber internals
|
|
131
|
-
const reactRootSelectors = [
|
|
132
|
-
"#__next",
|
|
133
|
-
"#root",
|
|
134
|
-
"#app",
|
|
135
|
-
"[data-reactroot]",
|
|
136
|
-
];
|
|
137
|
-
for (const selector of reactRootSelectors) {
|
|
138
|
-
const element = document.querySelector(selector);
|
|
139
|
-
if (element) {
|
|
140
|
-
const fiberKey = Object.keys(element).find((key) => key.startsWith("__reactInternalInstance") ||
|
|
141
|
-
key.startsWith("__reactFiber") ||
|
|
142
|
-
key.startsWith("_reactRootContainer"));
|
|
143
|
-
if (fiberKey) {
|
|
144
|
-
state["React Fiber Root"] = { selector, fiberKey };
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return Object.keys(state).length > 0 ? state : null;
|
|
150
|
-
}, FRAMEWORK_STATE_PATTERNS);
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
function formatValue(value) {
|
|
154
|
-
if (typeof value === "object" && value !== null && "type" in value) {
|
|
155
|
-
if (value.type === "object" && "keys" in value && Array.isArray(value.keys))
|
|
156
|
-
return `{${value.keys.join(", ")}${value.keys.length >= MAX_DISPLAY_ITEMS * 2 ? ", ..." : ""}}`;
|
|
157
|
-
else if ("preview" in value && typeof value.preview === "string")
|
|
158
|
-
return `"${value.preview}${value.preview.length >= 200 ? "..." : ""}"`;
|
|
159
|
-
}
|
|
160
|
-
return JSON.stringify(value);
|
|
161
|
-
}
|
|
162
|
-
export const formatFrameworkStateEvent = (event) => {
|
|
163
|
-
const { state, changes, action } = event.data;
|
|
164
|
-
const messages = [];
|
|
165
|
-
if (action === "detected") {
|
|
166
|
-
messages.push("Framework state detected:");
|
|
167
|
-
const keys = Object.keys(state);
|
|
168
|
-
for (const key of keys) {
|
|
169
|
-
const value = state[key];
|
|
170
|
-
messages.push(` ${key}: ${formatValue(value)}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
else if (action === "changed" && changes) {
|
|
174
|
-
messages.push("Framework state changed:");
|
|
175
|
-
messages.push(...changes.map((change) => ` ${change}`));
|
|
176
|
-
}
|
|
177
|
-
return messages.join("\n");
|
|
178
|
-
};
|
|
179
|
-
export const frameworkStateHooks = {
|
|
180
|
-
pre: frameworkStatePreHook,
|
|
181
|
-
post: frameworkStatePostHook,
|
|
182
|
-
};
|
package/lib/hooks/grouping.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
const rules = new Map();
|
|
2
|
-
// Helper to define a rule with typed callbacks without using type assertions in callers
|
|
3
|
-
export const defineGroupingRule = (spec) => {
|
|
4
|
-
const { match } = spec;
|
|
5
|
-
return {
|
|
6
|
-
match,
|
|
7
|
-
keyOf: (e) => {
|
|
8
|
-
// Planner guarantees keyOf is only called when match(e) is true
|
|
9
|
-
if (!match(e))
|
|
10
|
-
return "";
|
|
11
|
-
return spec.keyOf(e);
|
|
12
|
-
},
|
|
13
|
-
summaryOf: (first, run) => {
|
|
14
|
-
// Build a typed run defensively using the provided type guard
|
|
15
|
-
const typedRun = [];
|
|
16
|
-
for (const ev of run) {
|
|
17
|
-
if (match(ev))
|
|
18
|
-
typedRun.push(ev);
|
|
19
|
-
}
|
|
20
|
-
const typedFirst = match(first) ? first : typedRun[0];
|
|
21
|
-
if (!typedFirst)
|
|
22
|
-
return "";
|
|
23
|
-
return spec.summaryOf(typedFirst, typedRun);
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
export const registerGroupingRule = (type, rule) => {
|
|
28
|
-
rules.set(type, rule);
|
|
29
|
-
};
|
|
30
|
-
const getGroupingRule = (type) => {
|
|
31
|
-
return rules.get(type);
|
|
32
|
-
};
|
|
33
|
-
export const planGroupedMessages = (events) => {
|
|
34
|
-
const replacementById = new Map();
|
|
35
|
-
const skipIds = new Set();
|
|
36
|
-
let currentType;
|
|
37
|
-
let currentKey;
|
|
38
|
-
let run = [];
|
|
39
|
-
const flush = () => {
|
|
40
|
-
if (currentType && run.length > 1) {
|
|
41
|
-
const rule = getGroupingRule(currentType);
|
|
42
|
-
if (rule) {
|
|
43
|
-
const first = run[0];
|
|
44
|
-
const summary = rule.summaryOf(first, run);
|
|
45
|
-
replacementById.set(first.id, summary);
|
|
46
|
-
for (let i = 1; i < run.length; i++)
|
|
47
|
-
skipIds.add(run[i].id);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
currentType = undefined;
|
|
51
|
-
currentKey = undefined;
|
|
52
|
-
run = [];
|
|
53
|
-
};
|
|
54
|
-
for (const ev of events) {
|
|
55
|
-
const rule = getGroupingRule(ev.type);
|
|
56
|
-
if (!rule || !rule.match(ev)) {
|
|
57
|
-
flush();
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const key = rule.keyOf(ev);
|
|
61
|
-
if (currentType === ev.type && currentKey === key) {
|
|
62
|
-
run.push(ev);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
flush();
|
|
66
|
-
currentType = ev.type;
|
|
67
|
-
currentKey = key;
|
|
68
|
-
run = [ev];
|
|
69
|
-
}
|
|
70
|
-
flush();
|
|
71
|
-
return { replacementById, skipIds };
|
|
72
|
-
};
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { Ok } from "../utils/result.js";
|
|
2
|
-
import { trackEvent } from "./events.js";
|
|
3
|
-
import { hookNameSchema } from "./schema.js";
|
|
4
|
-
const MAX_DISPLAY_ITEMS = 5;
|
|
5
|
-
const pageJsonLdStates = new WeakMap();
|
|
6
|
-
const seenJsonLdTypesByContext = new WeakMap();
|
|
7
|
-
const getSeenJsonLdTypes = (context) => {
|
|
8
|
-
let set = seenJsonLdTypesByContext.get(context);
|
|
9
|
-
if (!set) {
|
|
10
|
-
set = new Set();
|
|
11
|
-
seenJsonLdTypesByContext.set(context, set);
|
|
12
|
-
}
|
|
13
|
-
return set;
|
|
14
|
-
};
|
|
15
|
-
export const jsonLdDetectionPreHook = {
|
|
16
|
-
name: hookNameSchema.enum["json-ld-detection-pre"],
|
|
17
|
-
handler: async (context) => {
|
|
18
|
-
const jsonLdState = await detectJsonLdState(context);
|
|
19
|
-
if (jsonLdState) {
|
|
20
|
-
// Store the initial state
|
|
21
|
-
if (context.tab?.page)
|
|
22
|
-
pageJsonLdStates.set(context.tab.page, jsonLdState);
|
|
23
|
-
// Track event for newly detected JSON-LD types
|
|
24
|
-
const newTypes = Object.keys(jsonLdState).filter((type) => !getSeenJsonLdTypes(context.context).has(type));
|
|
25
|
-
if (newTypes.length > 0) {
|
|
26
|
-
trackEvent(context.context, {
|
|
27
|
-
type: "json-ld",
|
|
28
|
-
data: {
|
|
29
|
-
state: jsonLdState,
|
|
30
|
-
action: "detected",
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
// Mark types as seen
|
|
34
|
-
newTypes.forEach((type) => getSeenJsonLdTypes(context.context).add(type));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return Ok(undefined);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
export const jsonLdDetectionPostHook = {
|
|
41
|
-
name: hookNameSchema.enum["json-ld-detection-post"],
|
|
42
|
-
handler: async (context) => {
|
|
43
|
-
const newJsonLdState = await detectJsonLdState(context);
|
|
44
|
-
const initialState = context.tab?.page
|
|
45
|
-
? pageJsonLdStates.get(context.tab.page)
|
|
46
|
-
: undefined;
|
|
47
|
-
if (newJsonLdState || initialState) {
|
|
48
|
-
const changes = [];
|
|
49
|
-
if (initialState && newJsonLdState) {
|
|
50
|
-
// Compare states
|
|
51
|
-
const allTypes = new Set([
|
|
52
|
-
...Object.keys(initialState),
|
|
53
|
-
...Object.keys(newJsonLdState),
|
|
54
|
-
]);
|
|
55
|
-
for (const type of allTypes) {
|
|
56
|
-
const initInfo = initialState[type];
|
|
57
|
-
const currInfo = newJsonLdState[type];
|
|
58
|
-
if (!initInfo && currInfo)
|
|
59
|
-
changes.push(`+ ${type}${currInfo.count > 1 ? ` (${currInfo.count} instances)` : ""}`);
|
|
60
|
-
else if (initInfo && !currInfo)
|
|
61
|
-
changes.push(`- ${type}${initInfo.count > 1 ? ` (${initInfo.count} instances)` : ""}`);
|
|
62
|
-
else if (initInfo && currInfo && initInfo.count !== currInfo.count)
|
|
63
|
-
changes.push(`~ ${type}: ${initInfo.count} → ${currInfo.count} instances`);
|
|
64
|
-
}
|
|
65
|
-
if (changes.length > 0) {
|
|
66
|
-
trackEvent(context.context, {
|
|
67
|
-
type: "json-ld",
|
|
68
|
-
data: {
|
|
69
|
-
state: newJsonLdState,
|
|
70
|
-
changes,
|
|
71
|
-
action: "changed",
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
else if (newJsonLdState && !initialState) {
|
|
77
|
-
// No initial state, but we have state now
|
|
78
|
-
const newTypes = Object.keys(newJsonLdState).filter((type) => !getSeenJsonLdTypes(context.context).has(type));
|
|
79
|
-
if (newTypes.length > 0) {
|
|
80
|
-
trackEvent(context.context, {
|
|
81
|
-
type: "json-ld",
|
|
82
|
-
data: {
|
|
83
|
-
state: newJsonLdState,
|
|
84
|
-
action: "detected",
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
newTypes.forEach((type) => getSeenJsonLdTypes(context.context).add(type));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// Update stored state
|
|
91
|
-
if (context.tab?.page && newJsonLdState)
|
|
92
|
-
pageJsonLdStates.set(context.tab.page, newJsonLdState);
|
|
93
|
-
}
|
|
94
|
-
return Ok(undefined);
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
async function detectJsonLdState(context) {
|
|
98
|
-
if (!context.tab?.page)
|
|
99
|
-
return null;
|
|
100
|
-
const result = await context.tab.page.evaluate(() => {
|
|
101
|
-
const state = {};
|
|
102
|
-
// Find all JSON-LD scripts
|
|
103
|
-
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
104
|
-
scripts.forEach((script, index) => {
|
|
105
|
-
try {
|
|
106
|
-
// Parse JSON
|
|
107
|
-
const data = JSON.parse(script.textContent || "{}");
|
|
108
|
-
// Extract @type - handle both single and array types
|
|
109
|
-
let types = [];
|
|
110
|
-
if (data["@type"]) {
|
|
111
|
-
types = Array.isArray(data["@type"])
|
|
112
|
-
? data["@type"]
|
|
113
|
-
: [data["@type"]];
|
|
114
|
-
}
|
|
115
|
-
else if (data["@graph"] && Array.isArray(data["@graph"])) {
|
|
116
|
-
// Handle @graph structures
|
|
117
|
-
data["@graph"].forEach((item) => {
|
|
118
|
-
if (item["@type"]) {
|
|
119
|
-
const itemTypes = Array.isArray(item["@type"])
|
|
120
|
-
? item["@type"]
|
|
121
|
-
: [item["@type"]];
|
|
122
|
-
types.push(...itemTypes);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
// Count occurrences of each type
|
|
127
|
-
types.forEach((type) => {
|
|
128
|
-
if (!state[type])
|
|
129
|
-
state[type] = { count: 0, indices: [] };
|
|
130
|
-
state[type].count++;
|
|
131
|
-
state[type].indices.push(index);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
catch (e) {
|
|
135
|
-
state["InvalidJSON-LD"] = state["InvalidJSON-LD"] || {
|
|
136
|
-
count: 0,
|
|
137
|
-
indices: [],
|
|
138
|
-
};
|
|
139
|
-
state["InvalidJSON-LD"].count++;
|
|
140
|
-
state["InvalidJSON-LD"].indices.push(index);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
return Object.keys(state).length > 0 ? state : null;
|
|
144
|
-
});
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
function buildStateMessages(state, types) {
|
|
148
|
-
const msgs = [];
|
|
149
|
-
const targetTypes = types ?? Object.keys(state);
|
|
150
|
-
// Sort by count (descending) and take top MAX_DISPLAY_ITEMS
|
|
151
|
-
const sortedTypes = targetTypes
|
|
152
|
-
.sort((a, b) => (state[b].count || 0) - (state[a].count || 0))
|
|
153
|
-
.slice(0, MAX_DISPLAY_ITEMS);
|
|
154
|
-
for (const type of sortedTypes) {
|
|
155
|
-
const info = state[type];
|
|
156
|
-
if (info.count === 1)
|
|
157
|
-
msgs.push(` ${type}`);
|
|
158
|
-
else
|
|
159
|
-
msgs.push(` ${type} (${info.count} instances)`);
|
|
160
|
-
}
|
|
161
|
-
// Add indicator if there are more types
|
|
162
|
-
if (targetTypes.length > MAX_DISPLAY_ITEMS)
|
|
163
|
-
msgs.push(` ... and ${targetTypes.length - MAX_DISPLAY_ITEMS} more type(s)`);
|
|
164
|
-
return msgs;
|
|
165
|
-
}
|
|
166
|
-
export const formatJsonLdEvent = (event) => {
|
|
167
|
-
const { state, changes, action } = event.data;
|
|
168
|
-
const messages = [];
|
|
169
|
-
if (action === "detected") {
|
|
170
|
-
messages.push("New JSON-LD types detected:");
|
|
171
|
-
messages.push(...buildStateMessages(state));
|
|
172
|
-
}
|
|
173
|
-
else if (action === "changed" && changes) {
|
|
174
|
-
messages.push("JSON-LD changes after action:");
|
|
175
|
-
messages.push(...changes.map((change) => ` ${change}`));
|
|
176
|
-
}
|
|
177
|
-
return messages.join("\n");
|
|
178
|
-
};
|
|
179
|
-
export const jsonLdDetectionHooks = {
|
|
180
|
-
pre: jsonLdDetectionPreHook,
|
|
181
|
-
post: jsonLdDetectionPostHook,
|
|
182
|
-
};
|