@wordbricks/playwright-mcp 0.1.25 → 0.1.27

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.
Files changed (82) hide show
  1. package/lib/browserContextFactory.js +616 -0
  2. package/lib/browserServerBackend.js +86 -0
  3. package/lib/config.js +302 -0
  4. package/lib/context.js +320 -0
  5. package/lib/extension/cdpRelay.js +352 -0
  6. package/lib/extension/extensionContextFactory.js +56 -0
  7. package/lib/frameworkPatterns.js +35 -0
  8. package/lib/hooks/antiBotDetectionHook.js +178 -0
  9. package/lib/hooks/core.js +145 -0
  10. package/lib/hooks/eventConsumer.js +52 -0
  11. package/lib/hooks/events.js +42 -0
  12. package/lib/hooks/formatToolCallEvent.js +12 -0
  13. package/lib/hooks/frameworkStateHook.js +182 -0
  14. package/lib/hooks/grouping.js +72 -0
  15. package/lib/hooks/jsonLdDetectionHook.js +182 -0
  16. package/lib/hooks/networkFilters.js +82 -0
  17. package/lib/hooks/networkSetup.js +61 -0
  18. package/lib/hooks/networkTrackingHook.js +67 -0
  19. package/lib/hooks/pageHeightHook.js +75 -0
  20. package/lib/hooks/registry.js +41 -0
  21. package/lib/hooks/requireTabHook.js +26 -0
  22. package/lib/hooks/schema.js +89 -0
  23. package/lib/hooks/waitHook.js +33 -0
  24. package/lib/index.js +41 -0
  25. package/lib/mcp/inProcessTransport.js +71 -0
  26. package/lib/mcp/proxyBackend.js +130 -0
  27. package/lib/mcp/server.js +91 -0
  28. package/lib/mcp/tool.js +44 -0
  29. package/lib/mcp/transport.js +188 -0
  30. package/lib/playwrightTransformer.js +520 -0
  31. package/lib/program.js +112 -0
  32. package/lib/response.js +192 -0
  33. package/lib/sessionLog.js +123 -0
  34. package/lib/tab.js +251 -0
  35. package/lib/tools/common.js +55 -0
  36. package/lib/tools/console.js +33 -0
  37. package/lib/tools/dialogs.js +50 -0
  38. package/lib/tools/evaluate.js +62 -0
  39. package/lib/tools/extractFrameworkState.js +225 -0
  40. package/lib/tools/files.js +48 -0
  41. package/lib/tools/form.js +66 -0
  42. package/lib/tools/getSnapshot.js +36 -0
  43. package/lib/tools/getVisibleHtml.js +68 -0
  44. package/lib/tools/install.js +51 -0
  45. package/lib/tools/keyboard.js +83 -0
  46. package/lib/tools/mouse.js +97 -0
  47. package/lib/tools/navigate.js +66 -0
  48. package/lib/tools/network.js +121 -0
  49. package/lib/tools/networkDetail.js +238 -0
  50. package/lib/tools/networkSearch/bodySearch.js +161 -0
  51. package/lib/tools/networkSearch/grouping.js +37 -0
  52. package/lib/tools/networkSearch/helpers.js +32 -0
  53. package/lib/tools/networkSearch/searchHtml.js +76 -0
  54. package/lib/tools/networkSearch/types.js +1 -0
  55. package/lib/tools/networkSearch/urlSearch.js +124 -0
  56. package/lib/tools/networkSearch.js +278 -0
  57. package/lib/tools/pdf.js +41 -0
  58. package/lib/tools/repl.js +414 -0
  59. package/lib/tools/screenshot.js +103 -0
  60. package/lib/tools/scroll.js +131 -0
  61. package/lib/tools/snapshot.js +161 -0
  62. package/lib/tools/tabs.js +62 -0
  63. package/lib/tools/tool.js +35 -0
  64. package/lib/tools/utils.js +78 -0
  65. package/lib/tools/wait.js +60 -0
  66. package/lib/tools.js +68 -0
  67. package/lib/utils/adBlockFilter.js +90 -0
  68. package/lib/utils/codegen.js +55 -0
  69. package/lib/utils/extensionPath.js +10 -0
  70. package/lib/utils/fileUtils.js +40 -0
  71. package/lib/utils/graphql.js +269 -0
  72. package/lib/utils/guid.js +22 -0
  73. package/lib/utils/httpServer.js +39 -0
  74. package/lib/utils/log.js +21 -0
  75. package/lib/utils/manualPromise.js +111 -0
  76. package/lib/utils/networkFormat.js +14 -0
  77. package/lib/utils/package.js +20 -0
  78. package/lib/utils/result.js +2 -0
  79. package/lib/utils/sanitizeHtml.js +130 -0
  80. package/lib/utils/truncate.js +103 -0
  81. package/lib/utils/withTimeout.js +7 -0
  82. package/package.json +11 -1
@@ -0,0 +1,56 @@
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
+ }
@@ -0,0 +1,35 @@
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;
@@ -0,0 +1,178 @@
1
+ import ms from "ms";
2
+ import { Ok } from "../utils/result.js";
3
+ import { getEventsAfter, isEventType, trackEvent } from "./events.js";
4
+ import { hookNameSchema } from "./schema.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 || "")
27
+ .slice(0, 2000)
28
+ .toLowerCase();
29
+ if (title.includes("just a moment") || bodyText.includes("just a moment"))
30
+ return false;
31
+ if (title.includes("checking your browser") ||
32
+ bodyText.includes("checking your browser"))
33
+ return false;
34
+ return true;
35
+ });
36
+ };
37
+ const getDetectedProviders = (context) => {
38
+ const detectedProviders = detectedProvidersByContext.get(context);
39
+ if (detectedProviders)
40
+ return detectedProviders;
41
+ const newSet = new Set();
42
+ detectedProvidersByContext.set(context, newSet);
43
+ return newSet;
44
+ };
45
+ const updateLastProcessedEventId = (context, events) => {
46
+ const lastEvent = events[events.length - 1];
47
+ if (!lastEvent)
48
+ return;
49
+ lastProcessedEventIdByContext.set(context, lastEvent.id);
50
+ };
51
+ const isStatusOk = (status) => status >= 200 && status < 400;
52
+ const providerConfigs = [
53
+ {
54
+ provider: "cloudflare-turnstile",
55
+ match: (event) => isStatusOk(event.data.status) &&
56
+ event.data.url.includes("challenges.cloudflare.com") &&
57
+ event.data.url.includes("/turnstile/"),
58
+ },
59
+ {
60
+ provider: "aws-waf",
61
+ match: (event) => isStatusOk(event.data.status) &&
62
+ event.data.method.toUpperCase() === "POST" &&
63
+ event.data.url.includes(".awswaf.com") &&
64
+ event.data.url.includes("/telemetry"),
65
+ },
66
+ ];
67
+ export const getAntiBotProviderConfigs = () => providerConfigs;
68
+ const waitForResolution = async (ctx) => {
69
+ const start = Date.now();
70
+ if (!ctx.tab?.page)
71
+ return { resolved: false, waitedMs: 0 };
72
+ await ctx.tab.page.waitForTimeout(RESOLUTION_WAIT_MS);
73
+ const resolved = await isLikelyResolved(ctx);
74
+ return { resolved, waitedMs: Date.now() - start };
75
+ };
76
+ const detectAntiBot = (ctx, events) => {
77
+ const detectedProviders = getDetectedProviders(ctx.context);
78
+ const networkEvents = events.filter(isEventType("network-request"));
79
+ return providerConfigs.reduce((acc, config) => {
80
+ if (detectedProviders.has(config.provider))
81
+ return acc;
82
+ const match = networkEvents.find(config.match);
83
+ if (!match)
84
+ return acc;
85
+ detectedProviders.add(config.provider);
86
+ return {
87
+ hits: [
88
+ ...acc.hits,
89
+ {
90
+ provider: config.provider,
91
+ url: match.data.url,
92
+ status: match.data.status,
93
+ },
94
+ ],
95
+ };
96
+ }, { hits: [] });
97
+ };
98
+ export const antiBotDetectionPreHook = {
99
+ name: hookNameSchema.enum["anti-bot-detection-pre"],
100
+ handler: async (ctx) => {
101
+ if (lastProcessedEventIdByContext.has(ctx.context))
102
+ return Ok(undefined);
103
+ if (typeof ctx.eventStore.lastSeenEventId === "number")
104
+ lastProcessedEventIdByContext.set(ctx.context, ctx.eventStore.lastSeenEventId);
105
+ return Ok(undefined);
106
+ },
107
+ };
108
+ export const antiBotDetectionPostHook = {
109
+ name: hookNameSchema.enum["anti-bot-detection-post"],
110
+ handler: async (ctx) => {
111
+ const newEvents = getEventsAfter(ctx.eventStore, lastProcessedEventIdByContext.get(ctx.context));
112
+ if (newEvents.length === 0)
113
+ return Ok(undefined);
114
+ const detection = detectAntiBot(ctx, newEvents);
115
+ if (detection.hits.length > 0) {
116
+ detection.hits.forEach((hit) => {
117
+ trackEvent(ctx.context, {
118
+ type: "anti-bot",
119
+ data: {
120
+ provider: hit.provider,
121
+ detectionMethod: "network-request",
122
+ url: hit.url,
123
+ status: hit.status,
124
+ action: "detected",
125
+ waitMs: RESOLUTION_WAIT_MS,
126
+ },
127
+ });
128
+ });
129
+ const waitResult = await waitForResolution(ctx);
130
+ detection.hits.forEach((hit) => {
131
+ trackEvent(ctx.context, {
132
+ type: "anti-bot",
133
+ data: {
134
+ provider: hit.provider,
135
+ detectionMethod: "network-request",
136
+ url: hit.url,
137
+ status: hit.status,
138
+ action: waitResult.resolved ? "resolved" : "still-blocked",
139
+ waitMs: waitResult.waitedMs,
140
+ },
141
+ });
142
+ });
143
+ }
144
+ updateLastProcessedEventId(ctx.context, newEvents);
145
+ return Ok(undefined);
146
+ },
147
+ };
148
+ export const antiBotDetectionHooks = {
149
+ pre: antiBotDetectionPreHook,
150
+ post: antiBotDetectionPostHook,
151
+ };
152
+ export const formatAntiBotEvent = (event) => {
153
+ if (event.data.action === "still-blocked") {
154
+ const waitSeconds = event.data.waitMs
155
+ ? Math.round(event.data.waitMs / 1000)
156
+ : 0;
157
+ if (event.data.provider === "cloudflare-turnstile")
158
+ return `Anti-bot still active: Cloudflare Turnstile after ${waitSeconds || "<1"}s wait`;
159
+ if (event.data.provider === "aws-waf")
160
+ return `Anti-bot still active: AWS WAF after ${waitSeconds || "<1"}s wait`;
161
+ return "Anti-bot mechanism still active";
162
+ }
163
+ if (event.data.action === "resolved") {
164
+ const waitSeconds = event.data.waitMs
165
+ ? Math.round(event.data.waitMs / 1000)
166
+ : 0;
167
+ if (event.data.provider === "cloudflare-turnstile")
168
+ return `Anti-bot resolved: Cloudflare Turnstile after ${waitSeconds || "<1"}s wait`;
169
+ if (event.data.provider === "aws-waf")
170
+ return `Anti-bot resolved: AWS WAF after ${waitSeconds || "<1"}s wait`;
171
+ return "Anti-bot mechanism resolved";
172
+ }
173
+ if (event.data.provider === "cloudflare-turnstile")
174
+ return `Anti-bot detected: Cloudflare Turnstile (${event.data.status})`;
175
+ if (event.data.provider === "aws-waf")
176
+ return `Anti-bot detected: AWS WAF telemetry request (${event.data.status})`;
177
+ return "Anti-bot mechanism detected";
178
+ };
@@ -0,0 +1,145 @@
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
+ };
@@ -0,0 +1,52 @@
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
+ };
@@ -0,0 +1,42 @@
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
+ };
@@ -0,0 +1,12 @@
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
+ };