@wordbricks/playwright-mcp 0.1.20 → 0.1.23
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/cli-wrapper.js +15 -14
- package/cli.js +1 -1
- package/config.d.ts +11 -6
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/package.json +34 -57
- package/LICENSE +0 -202
- package/lib/browserContextFactory.js +0 -326
- package/lib/browserServerBackend.js +0 -84
- package/lib/config.js +0 -286
- package/lib/context.js +0 -309
- package/lib/extension/cdpRelay.js +0 -346
- package/lib/extension/extensionContextFactory.js +0 -56
- package/lib/frameworkPatterns.js +0 -35
- package/lib/hooks/antiBotDetectionHook.js +0 -171
- package/lib/hooks/core.js +0 -144
- package/lib/hooks/eventConsumer.js +0 -52
- package/lib/hooks/events.js +0 -42
- package/lib/hooks/formatToolCallEvent.js +0 -16
- package/lib/hooks/frameworkStateHook.js +0 -182
- package/lib/hooks/grouping.js +0 -72
- package/lib/hooks/jsonLdDetectionHook.js +0 -175
- package/lib/hooks/networkFilters.js +0 -82
- package/lib/hooks/networkSetup.js +0 -59
- package/lib/hooks/networkTrackingHook.js +0 -67
- package/lib/hooks/pageHeightHook.js +0 -75
- package/lib/hooks/registry.js +0 -42
- 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 -39
- package/lib/mcp/inProcessTransport.js +0 -72
- package/lib/mcp/proxyBackend.js +0 -115
- package/lib/mcp/server.js +0 -86
- package/lib/mcp/tool.js +0 -38
- package/lib/mcp/transport.js +0 -181
- package/lib/playwrightTransformer.js +0 -497
- package/lib/program.js +0 -110
- package/lib/response.js +0 -186
- package/lib/sessionLog.js +0 -121
- package/lib/tab.js +0 -249
- package/lib/tools/common.js +0 -55
- package/lib/tools/console.js +0 -33
- package/lib/tools/dialogs.js +0 -47
- package/lib/tools/evaluate.js +0 -53
- package/lib/tools/extractFrameworkState.js +0 -214
- package/lib/tools/files.js +0 -45
- package/lib/tools/form.js +0 -57
- package/lib/tools/getSnapshot.js +0 -37
- package/lib/tools/getVisibleHtml.js +0 -52
- package/lib/tools/install.js +0 -51
- package/lib/tools/keyboard.js +0 -78
- package/lib/tools/mouse.js +0 -99
- package/lib/tools/navigate.js +0 -70
- package/lib/tools/network.js +0 -123
- package/lib/tools/networkDetail.js +0 -229
- package/lib/tools/networkSearch/bodySearch.js +0 -147
- package/lib/tools/networkSearch/grouping.js +0 -28
- package/lib/tools/networkSearch/helpers.js +0 -32
- package/lib/tools/networkSearch/searchHtml.js +0 -67
- package/lib/tools/networkSearch/types.js +0 -1
- package/lib/tools/networkSearch/urlSearch.js +0 -82
- package/lib/tools/networkSearch.js +0 -268
- package/lib/tools/pdf.js +0 -40
- package/lib/tools/repl.js +0 -402
- package/lib/tools/screenshot.js +0 -79
- package/lib/tools/scroll.js +0 -126
- package/lib/tools/snapshot.js +0 -144
- package/lib/tools/tabs.js +0 -59
- package/lib/tools/tool.js +0 -33
- package/lib/tools/utils.js +0 -74
- package/lib/tools/wait.js +0 -55
- package/lib/tools.js +0 -67
- package/lib/utils/adBlockFilter.js +0 -87
- package/lib/utils/codegen.js +0 -51
- package/lib/utils/extensionPath.js +0 -10
- package/lib/utils/fileUtils.js +0 -36
- package/lib/utils/graphql.js +0 -258
- 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 -12
- package/lib/utils/package.js +0 -20
- package/lib/utils/result.js +0 -2
- package/lib/utils/sanitizeHtml.js +0 -98
- package/lib/utils/truncate.js +0 -103
- package/lib/utils/withTimeout.js +0 -7
- package/src/index.ts +0 -50
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import debug from 'debug';
|
|
17
|
-
import * as playwright from 'playwright-core';
|
|
18
|
-
import { startHttpServer } from '../utils/httpServer.js';
|
|
19
|
-
import { CDPRelayServer } from './cdpRelay.js';
|
|
20
|
-
const debugLogger = debug('pw:mcp:relay');
|
|
21
|
-
export class ExtensionContextFactory {
|
|
22
|
-
name = 'extension';
|
|
23
|
-
description = 'Connect to a browser using the Playwright MCP extension';
|
|
24
|
-
_browserChannel;
|
|
25
|
-
_userDataDir;
|
|
26
|
-
constructor(browserChannel, userDataDir) {
|
|
27
|
-
this._browserChannel = browserChannel;
|
|
28
|
-
this._userDataDir = userDataDir;
|
|
29
|
-
}
|
|
30
|
-
async createContext(clientInfo, abortSignal) {
|
|
31
|
-
const browser = await this._obtainBrowser(clientInfo, abortSignal);
|
|
32
|
-
return {
|
|
33
|
-
browserContext: browser.contexts()[0],
|
|
34
|
-
close: async () => {
|
|
35
|
-
debugLogger('close() called for browser context');
|
|
36
|
-
await browser.close();
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
async _obtainBrowser(clientInfo, abortSignal) {
|
|
41
|
-
const relay = await this._startRelay(abortSignal);
|
|
42
|
-
await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal);
|
|
43
|
-
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
|
|
44
|
-
}
|
|
45
|
-
async _startRelay(abortSignal) {
|
|
46
|
-
const httpServer = await startHttpServer({});
|
|
47
|
-
if (abortSignal.aborted) {
|
|
48
|
-
httpServer.close();
|
|
49
|
-
throw new Error(abortSignal.reason);
|
|
50
|
-
}
|
|
51
|
-
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir);
|
|
52
|
-
abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
|
|
53
|
-
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
|
|
54
|
-
return cdpRelayServer;
|
|
55
|
-
}
|
|
56
|
-
}
|
package/lib/frameworkPatterns.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared framework state patterns used by framework state detection hook
|
|
3
|
-
*/
|
|
4
|
-
export const FRAMEWORK_STATE_PATTERNS = [
|
|
5
|
-
// React/Next.js
|
|
6
|
-
'__NEXT_DATA__',
|
|
7
|
-
'__reactServerState',
|
|
8
|
-
// Remix
|
|
9
|
-
'__remixContext',
|
|
10
|
-
'__remixManifest',
|
|
11
|
-
'__remixRouteModules',
|
|
12
|
-
// Apollo GraphQL
|
|
13
|
-
'__APOLLO_STATE__',
|
|
14
|
-
'__APOLLO_CLIENT__',
|
|
15
|
-
// Redux/State Management
|
|
16
|
-
'__PRELOADED_STATE__',
|
|
17
|
-
'__INITIAL_STATE__',
|
|
18
|
-
'__REDUX_STATE__',
|
|
19
|
-
// Vue/Nuxt
|
|
20
|
-
'__NUXT__',
|
|
21
|
-
// Gatsby
|
|
22
|
-
'___gatsby',
|
|
23
|
-
'___loader',
|
|
24
|
-
// Generic SSR
|
|
25
|
-
'__SSR_DATA__',
|
|
26
|
-
'__APP_STATE__',
|
|
27
|
-
'__SERVER_STATE__',
|
|
28
|
-
// Others
|
|
29
|
-
'__QWIK_STATE__',
|
|
30
|
-
'__SVELTE__',
|
|
31
|
-
'__ANGULAR__',
|
|
32
|
-
'__SOLID__',
|
|
33
|
-
'__ASTRO_DATA__',
|
|
34
|
-
];
|
|
35
|
-
export const MAX_DISPLAY_ITEMS = 5;
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import ms from 'ms';
|
|
2
|
-
import { Ok } from '../utils/result.js';
|
|
3
|
-
import { hookNameSchema } from './schema.js';
|
|
4
|
-
import { getEventsAfter, isEventType, trackEvent } from './events.js';
|
|
5
|
-
const RESOLUTION_WAIT_MS = ms('10s');
|
|
6
|
-
const lastProcessedEventIdByContext = new WeakMap();
|
|
7
|
-
const detectedProvidersByContext = new WeakMap();
|
|
8
|
-
const isLikelyResolved = async (ctx) => {
|
|
9
|
-
if (!ctx.tab?.page)
|
|
10
|
-
return false;
|
|
11
|
-
const host = ctx.tab.page.url();
|
|
12
|
-
if (host.includes('challenges.cloudflare.com'))
|
|
13
|
-
return false;
|
|
14
|
-
return ctx.tab.page.evaluate(() => {
|
|
15
|
-
const challengeSelectors = [
|
|
16
|
-
'#challenge-stage',
|
|
17
|
-
'#cf-challenge-running',
|
|
18
|
-
'iframe[src*="turnstile"]',
|
|
19
|
-
'form[action*="/cdn-cgi/challenge-platform"]',
|
|
20
|
-
'[data-cf-challenge]'
|
|
21
|
-
];
|
|
22
|
-
const hasChallengeDom = challengeSelectors.some(selector => document.querySelector(selector));
|
|
23
|
-
if (hasChallengeDom)
|
|
24
|
-
return false;
|
|
25
|
-
const title = document.title.toLowerCase();
|
|
26
|
-
const bodyText = (document.body?.innerText || '').slice(0, 2000).toLowerCase();
|
|
27
|
-
if (title.includes('just a moment') || bodyText.includes('just a moment'))
|
|
28
|
-
return false;
|
|
29
|
-
if (title.includes('checking your browser') || bodyText.includes('checking your browser'))
|
|
30
|
-
return false;
|
|
31
|
-
return true;
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
const getDetectedProviders = (context) => {
|
|
35
|
-
const detectedProviders = detectedProvidersByContext.get(context);
|
|
36
|
-
if (detectedProviders)
|
|
37
|
-
return detectedProviders;
|
|
38
|
-
const newSet = new Set();
|
|
39
|
-
detectedProvidersByContext.set(context, newSet);
|
|
40
|
-
return newSet;
|
|
41
|
-
};
|
|
42
|
-
const updateLastProcessedEventId = (context, events) => {
|
|
43
|
-
const lastEvent = events[events.length - 1];
|
|
44
|
-
if (!lastEvent)
|
|
45
|
-
return;
|
|
46
|
-
lastProcessedEventIdByContext.set(context, lastEvent.id);
|
|
47
|
-
};
|
|
48
|
-
const isStatusOk = (status) => status >= 200 && status < 400;
|
|
49
|
-
const providerConfigs = [
|
|
50
|
-
{
|
|
51
|
-
provider: 'cloudflare-turnstile',
|
|
52
|
-
match: event => isStatusOk(event.data.status) &&
|
|
53
|
-
event.data.url.includes('challenges.cloudflare.com') &&
|
|
54
|
-
event.data.url.includes('/turnstile/'),
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
provider: 'aws-waf',
|
|
58
|
-
match: event => isStatusOk(event.data.status) &&
|
|
59
|
-
event.data.method.toUpperCase() === 'POST' &&
|
|
60
|
-
event.data.url.includes('.awswaf.com') &&
|
|
61
|
-
event.data.url.includes('/telemetry'),
|
|
62
|
-
},
|
|
63
|
-
];
|
|
64
|
-
export const getAntiBotProviderConfigs = () => providerConfigs;
|
|
65
|
-
const waitForResolution = async (ctx) => {
|
|
66
|
-
const start = Date.now();
|
|
67
|
-
if (!ctx.tab?.page)
|
|
68
|
-
return { resolved: false, waitedMs: 0 };
|
|
69
|
-
await ctx.tab.page.waitForTimeout(RESOLUTION_WAIT_MS);
|
|
70
|
-
const resolved = await isLikelyResolved(ctx);
|
|
71
|
-
return { resolved, waitedMs: Date.now() - start };
|
|
72
|
-
};
|
|
73
|
-
const detectAntiBot = (ctx, events) => {
|
|
74
|
-
const detectedProviders = getDetectedProviders(ctx.context);
|
|
75
|
-
const networkEvents = events.filter(isEventType('network-request'));
|
|
76
|
-
return providerConfigs.reduce((acc, config) => {
|
|
77
|
-
if (detectedProviders.has(config.provider))
|
|
78
|
-
return acc;
|
|
79
|
-
const match = networkEvents.find(config.match);
|
|
80
|
-
if (!match)
|
|
81
|
-
return acc;
|
|
82
|
-
detectedProviders.add(config.provider);
|
|
83
|
-
return {
|
|
84
|
-
hits: [
|
|
85
|
-
...acc.hits,
|
|
86
|
-
{
|
|
87
|
-
provider: config.provider,
|
|
88
|
-
url: match.data.url,
|
|
89
|
-
status: match.data.status,
|
|
90
|
-
}
|
|
91
|
-
],
|
|
92
|
-
};
|
|
93
|
-
}, { hits: [] });
|
|
94
|
-
};
|
|
95
|
-
export const antiBotDetectionPreHook = {
|
|
96
|
-
name: hookNameSchema.enum['anti-bot-detection-pre'],
|
|
97
|
-
handler: async (ctx) => {
|
|
98
|
-
if (lastProcessedEventIdByContext.has(ctx.context))
|
|
99
|
-
return Ok(undefined);
|
|
100
|
-
if (typeof ctx.eventStore.lastSeenEventId === 'number')
|
|
101
|
-
lastProcessedEventIdByContext.set(ctx.context, ctx.eventStore.lastSeenEventId);
|
|
102
|
-
return Ok(undefined);
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
export const antiBotDetectionPostHook = {
|
|
106
|
-
name: hookNameSchema.enum['anti-bot-detection-post'],
|
|
107
|
-
handler: async (ctx) => {
|
|
108
|
-
const newEvents = getEventsAfter(ctx.eventStore, lastProcessedEventIdByContext.get(ctx.context));
|
|
109
|
-
if (newEvents.length === 0)
|
|
110
|
-
return Ok(undefined);
|
|
111
|
-
const detection = detectAntiBot(ctx, newEvents);
|
|
112
|
-
if (detection.hits.length > 0) {
|
|
113
|
-
detection.hits.forEach(hit => {
|
|
114
|
-
trackEvent(ctx.context, {
|
|
115
|
-
type: 'anti-bot',
|
|
116
|
-
data: {
|
|
117
|
-
provider: hit.provider,
|
|
118
|
-
detectionMethod: 'network-request',
|
|
119
|
-
url: hit.url,
|
|
120
|
-
status: hit.status,
|
|
121
|
-
action: 'detected',
|
|
122
|
-
waitMs: RESOLUTION_WAIT_MS,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
const waitResult = await waitForResolution(ctx);
|
|
127
|
-
detection.hits.forEach(hit => {
|
|
128
|
-
trackEvent(ctx.context, {
|
|
129
|
-
type: 'anti-bot',
|
|
130
|
-
data: {
|
|
131
|
-
provider: hit.provider,
|
|
132
|
-
detectionMethod: 'network-request',
|
|
133
|
-
url: hit.url,
|
|
134
|
-
status: hit.status,
|
|
135
|
-
action: waitResult.resolved ? 'resolved' : 'still-blocked',
|
|
136
|
-
waitMs: waitResult.waitedMs,
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
updateLastProcessedEventId(ctx.context, newEvents);
|
|
142
|
-
return Ok(undefined);
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
export const antiBotDetectionHooks = {
|
|
146
|
-
pre: antiBotDetectionPreHook,
|
|
147
|
-
post: antiBotDetectionPostHook,
|
|
148
|
-
};
|
|
149
|
-
export const formatAntiBotEvent = (event) => {
|
|
150
|
-
if (event.data.action === 'still-blocked') {
|
|
151
|
-
const waitSeconds = event.data.waitMs ? Math.round(event.data.waitMs / 1000) : 0;
|
|
152
|
-
if (event.data.provider === 'cloudflare-turnstile')
|
|
153
|
-
return `Anti-bot still active: Cloudflare Turnstile after ${waitSeconds || '<1'}s wait`;
|
|
154
|
-
if (event.data.provider === 'aws-waf')
|
|
155
|
-
return `Anti-bot still active: AWS WAF after ${waitSeconds || '<1'}s wait`;
|
|
156
|
-
return 'Anti-bot mechanism still active';
|
|
157
|
-
}
|
|
158
|
-
if (event.data.action === 'resolved') {
|
|
159
|
-
const waitSeconds = event.data.waitMs ? Math.round(event.data.waitMs / 1000) : 0;
|
|
160
|
-
if (event.data.provider === 'cloudflare-turnstile')
|
|
161
|
-
return `Anti-bot resolved: Cloudflare Turnstile after ${waitSeconds || '<1'}s wait`;
|
|
162
|
-
if (event.data.provider === 'aws-waf')
|
|
163
|
-
return `Anti-bot resolved: AWS WAF after ${waitSeconds || '<1'}s wait`;
|
|
164
|
-
return 'Anti-bot mechanism resolved';
|
|
165
|
-
}
|
|
166
|
-
if (event.data.provider === 'cloudflare-turnstile')
|
|
167
|
-
return `Anti-bot detected: Cloudflare Turnstile (${event.data.status})`;
|
|
168
|
-
if (event.data.provider === 'aws-waf')
|
|
169
|
-
return `Anti-bot detected: AWS WAF telemetry request (${event.data.status})`;
|
|
170
|
-
return 'Anti-bot mechanism detected';
|
|
171
|
-
};
|
package/lib/hooks/core.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { reduce } from '@fxts/core';
|
|
2
|
-
import { getEventStore, trackEvent } from './events.js';
|
|
3
|
-
import { consumeEvents } from './eventConsumer.js';
|
|
4
|
-
import { toolNameSchema } from './schema.js';
|
|
5
|
-
export { Ok, Err } 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 || (toolHooks.preHooks.length === 0 && toolHooks.postHooks.length === 0)) {
|
|
32
|
-
return {
|
|
33
|
-
...tool,
|
|
34
|
-
handle: async (context, params, response) => {
|
|
35
|
-
const eventStore = getEventStore(context);
|
|
36
|
-
// Consume pre-tool events
|
|
37
|
-
consumeEvents(context, eventStore, response);
|
|
38
|
-
// Track tool execution
|
|
39
|
-
const startTime = Date.now();
|
|
40
|
-
let success = true;
|
|
41
|
-
try {
|
|
42
|
-
await tool.handle(context, params, response);
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
success = false;
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
finally {
|
|
49
|
-
// Record tool call completion
|
|
50
|
-
const executionTime = Date.now() - startTime;
|
|
51
|
-
trackEvent(context, {
|
|
52
|
-
type: 'tool-call',
|
|
53
|
-
data: {
|
|
54
|
-
toolName,
|
|
55
|
-
params: params,
|
|
56
|
-
executionTime,
|
|
57
|
-
success,
|
|
58
|
-
},
|
|
59
|
-
timestamp: startTime,
|
|
60
|
-
});
|
|
61
|
-
// Consume post-tool events
|
|
62
|
-
consumeEvents(context, eventStore, response);
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
...tool,
|
|
69
|
-
handle: async (context, params, response) => {
|
|
70
|
-
const eventStore = getEventStore(context);
|
|
71
|
-
// Consume pre-tool events
|
|
72
|
-
consumeEvents(context, eventStore, response);
|
|
73
|
-
// Track tool execution
|
|
74
|
-
const startTime = Date.now();
|
|
75
|
-
// Run pre-hooks
|
|
76
|
-
const hookContext = {
|
|
77
|
-
context,
|
|
78
|
-
tab: context.currentTab(),
|
|
79
|
-
params,
|
|
80
|
-
toolName,
|
|
81
|
-
response,
|
|
82
|
-
eventStore,
|
|
83
|
-
};
|
|
84
|
-
try {
|
|
85
|
-
await reduce(async (ctx, hook) => runHook(hook, await ctx), Promise.resolve(hookContext), toolHooks.preHooks);
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
// Pre-hook already handled messaging (e.g., require-tab pre-hook sets tabs section)
|
|
89
|
-
// Avoid adding a duplicate error line in the Result section.
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
// Run original tool
|
|
93
|
-
let success = true;
|
|
94
|
-
try {
|
|
95
|
-
await tool.handle(context, params, response);
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
success = false;
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
// Record tool call completion
|
|
103
|
-
const executionTime = Date.now() - startTime;
|
|
104
|
-
trackEvent(context, {
|
|
105
|
-
type: 'tool-call',
|
|
106
|
-
data: {
|
|
107
|
-
toolName,
|
|
108
|
-
params: params,
|
|
109
|
-
executionTime,
|
|
110
|
-
success,
|
|
111
|
-
},
|
|
112
|
-
timestamp: startTime,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
// Run post-hooks
|
|
116
|
-
const postHookContext = {
|
|
117
|
-
context,
|
|
118
|
-
tab: context.currentTab(),
|
|
119
|
-
params,
|
|
120
|
-
toolName,
|
|
121
|
-
response,
|
|
122
|
-
eventStore: getEventStore(context),
|
|
123
|
-
};
|
|
124
|
-
try {
|
|
125
|
-
await reduce(async (ctx, hook) => runHook(hook, await ctx), Promise.resolve(postHookContext), toolHooks.postHooks);
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
response.addError(error instanceof Error ? error.message : 'Post-hook failed');
|
|
129
|
-
}
|
|
130
|
-
// Consume post-tool events
|
|
131
|
-
consumeEvents(context, eventStore, response);
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
};
|
|
135
|
-
export const getHookRegistry = (context) => {
|
|
136
|
-
const registry = hookRegistryMap.get(context);
|
|
137
|
-
return registry || createHookRegistry();
|
|
138
|
-
};
|
|
139
|
-
export const applyHooksToTools = (tools, context) => {
|
|
140
|
-
const registry = getHookRegistry(context);
|
|
141
|
-
if (registry.tools.size === 0)
|
|
142
|
-
return tools;
|
|
143
|
-
return tools.map(tool => wrapToolWithHooks(tool, registry));
|
|
144
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { getEventsAfter, isEventType, updateLastSeenId } from './events.js';
|
|
2
|
-
import { formatWaitEvent } from './waitHook.js';
|
|
3
|
-
import { formatPageHeightEvent } from './pageHeightHook.js';
|
|
4
|
-
import { formatNetworkEvent } from './networkTrackingHook.js';
|
|
5
|
-
import { planGroupedMessages } from './grouping.js';
|
|
6
|
-
import { formatToolCallEvent } from './formatToolCallEvent.js';
|
|
7
|
-
import { formatFrameworkStateEvent } from './frameworkStateHook.js';
|
|
8
|
-
import { formatJsonLdEvent } from './jsonLdDetectionHook.js';
|
|
9
|
-
import { formatAntiBotEvent, getAntiBotProviderConfigs } from './antiBotDetectionHook.js';
|
|
10
|
-
import { isAntiBotUrl } from './networkFilters.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 { pipe, filter, 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,16 +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
|
|
9
|
-
? ` (${executionTime}ms)`
|
|
10
|
-
: '';
|
|
11
|
-
// Format success status if available
|
|
12
|
-
const statusStr = success !== undefined
|
|
13
|
-
? success ? ' ✓' : ' ✗'
|
|
14
|
-
: '';
|
|
15
|
-
return `Tool ${toolName}${paramStr}${timeStr}${statusStr}`;
|
|
16
|
-
};
|