@playwright/mcp 0.0.22 → 0.0.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/README.md +13 -9
- package/config.d.ts +1 -1
- package/index.js +1 -1
- package/lib/config.js +2 -2
- package/lib/context.js +2 -2
- package/lib/index.js +2 -1
- package/lib/pageSnapshot.js +8 -55
- package/lib/tab.js +6 -9
- package/lib/tools/common.js +25 -6
- package/lib/tools/console.js +1 -1
- package/lib/tools/install.js +1 -1
- package/lib/tools/pdf.js +6 -3
- package/lib/tools/snapshot.js +2 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -207,7 +207,7 @@ And then in MCP client config, set the `url` to the SSE endpoint:
|
|
|
207
207
|
"mcpServers": {
|
|
208
208
|
"playwright": {
|
|
209
209
|
"command": "docker",
|
|
210
|
-
"args": ["run", "-i", "--rm", "--init", "
|
|
210
|
+
"args": ["run", "-i", "--rm", "--init", "--pull=always", "mcr.microsoft.com/playwright/mcp"]
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
}
|
|
@@ -216,7 +216,7 @@ And then in MCP client config, set the `url` to the SSE endpoint:
|
|
|
216
216
|
You can build the Docker image yourself.
|
|
217
217
|
|
|
218
218
|
```
|
|
219
|
-
docker build -t
|
|
219
|
+
docker build -t mcr.microsoft.com/playwright/mcp .
|
|
220
220
|
```
|
|
221
221
|
|
|
222
222
|
### Programmatic usage
|
|
@@ -224,14 +224,14 @@ docker build -t mcp/playwright .
|
|
|
224
224
|
```js
|
|
225
225
|
import http from 'http';
|
|
226
226
|
|
|
227
|
-
import {
|
|
227
|
+
import { createConnection } from '@playwright/mcp';
|
|
228
228
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
229
229
|
|
|
230
230
|
http.createServer(async (req, res) => {
|
|
231
231
|
// ...
|
|
232
232
|
|
|
233
233
|
// Creates a headless Playwright MCP server with SSE transport
|
|
234
|
-
const connection = await createConnection({ headless: true });
|
|
234
|
+
const connection = await createConnection({ browser: { launchOptions: { headless: true } } });
|
|
235
235
|
const transport = new SSEServerTransport('/messages', res);
|
|
236
236
|
await connection.connect(transport);
|
|
237
237
|
|
|
@@ -341,6 +341,7 @@ X Y coordinate space, based on the provided screenshot.
|
|
|
341
341
|
- Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
|
|
342
342
|
- Parameters:
|
|
343
343
|
- `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
|
|
344
|
+
- `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.
|
|
344
345
|
- `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.
|
|
345
346
|
- `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.
|
|
346
347
|
- Read-only: **true**
|
|
@@ -501,7 +502,8 @@ X Y coordinate space, based on the provided screenshot.
|
|
|
501
502
|
- **browser_pdf_save**
|
|
502
503
|
- Title: Save as PDF
|
|
503
504
|
- Description: Save page as PDF
|
|
504
|
-
- Parameters:
|
|
505
|
+
- Parameters:
|
|
506
|
+
- `filename` (string, optional): File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.
|
|
505
507
|
- Read-only: **true**
|
|
506
508
|
|
|
507
509
|
### Utilities
|
|
@@ -516,11 +518,13 @@ X Y coordinate space, based on the provided screenshot.
|
|
|
516
518
|
|
|
517
519
|
<!-- NOTE: This has been generated via update-readme.js -->
|
|
518
520
|
|
|
519
|
-
- **
|
|
520
|
-
- Title: Wait
|
|
521
|
-
- Description: Wait for a specified time
|
|
521
|
+
- **browser_wait_for**
|
|
522
|
+
- Title: Wait for
|
|
523
|
+
- Description: Wait for text to appear or disappear or a specified time to pass
|
|
522
524
|
- Parameters:
|
|
523
|
-
- `time` (number): The time to wait in seconds
|
|
525
|
+
- `time` (number, optional): The time to wait in seconds
|
|
526
|
+
- `text` (string, optional): The text to wait for
|
|
527
|
+
- `textGone` (string, optional): The text to wait for to disappear
|
|
524
528
|
- Read-only: **true**
|
|
525
529
|
|
|
526
530
|
<!-- NOTE: This has been generated via update-readme.js -->
|
package/config.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type Config = {
|
|
|
40
40
|
*
|
|
41
41
|
* This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
|
|
42
42
|
*/
|
|
43
|
-
launchOptions?: playwright.
|
|
43
|
+
launchOptions?: playwright.LaunchOptions;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Context options for the browser context.
|
package/index.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -72,7 +72,7 @@ export async function configFromCLIOptions(cliOptions) {
|
|
|
72
72
|
headless: cliOptions.headless,
|
|
73
73
|
};
|
|
74
74
|
if (browserName === 'chromium')
|
|
75
|
-
launchOptions.
|
|
75
|
+
launchOptions.cdpPort = await findFreePort();
|
|
76
76
|
const contextOptions = cliOptions.device ? devices[cliOptions.device] : undefined;
|
|
77
77
|
return {
|
|
78
78
|
browser: {
|
|
@@ -138,7 +138,7 @@ function mergeConfig(base, overrides) {
|
|
|
138
138
|
...pickDefined(overrides.browser?.contextOptions),
|
|
139
139
|
},
|
|
140
140
|
};
|
|
141
|
-
if (browser.browserName !== 'chromium')
|
|
141
|
+
if (browser.browserName !== 'chromium' && browser.launchOptions)
|
|
142
142
|
delete browser.launchOptions.channel;
|
|
143
143
|
return {
|
|
144
144
|
...pickDefined(base),
|
package/lib/context.js
CHANGED
|
@@ -100,7 +100,7 @@ export class Context {
|
|
|
100
100
|
}
|
|
101
101
|
async run(tool, params) {
|
|
102
102
|
// Tab management is done outside of the action() call.
|
|
103
|
-
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params));
|
|
103
|
+
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
|
104
104
|
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
|
105
105
|
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
|
|
106
106
|
if (resultOverride)
|
|
@@ -316,7 +316,7 @@ async function createUserDataDir(browserConfig) {
|
|
|
316
316
|
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
317
317
|
else
|
|
318
318
|
throw new Error('Unsupported platform: ' + process.platform);
|
|
319
|
-
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions
|
|
319
|
+
const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserConfig?.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
320
320
|
await fs.promises.mkdir(result, { recursive: true });
|
|
321
321
|
return result;
|
|
322
322
|
}
|
package/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import { createConnection as createConnectionImpl } from './connection.js';
|
|
16
17
|
export async function createConnection(config = {}) {
|
|
17
|
-
return
|
|
18
|
+
return createConnectionImpl(config);
|
|
18
19
|
}
|
package/lib/pageSnapshot.js
CHANGED
|
@@ -13,22 +13,22 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import yaml from 'yaml';
|
|
17
16
|
export class PageSnapshot {
|
|
18
|
-
|
|
17
|
+
_page;
|
|
19
18
|
_text;
|
|
20
|
-
constructor() {
|
|
19
|
+
constructor(page) {
|
|
20
|
+
this._page = page;
|
|
21
21
|
}
|
|
22
22
|
static async create(page) {
|
|
23
|
-
const snapshot = new PageSnapshot();
|
|
24
|
-
await snapshot._build(
|
|
23
|
+
const snapshot = new PageSnapshot(page);
|
|
24
|
+
await snapshot._build();
|
|
25
25
|
return snapshot;
|
|
26
26
|
}
|
|
27
27
|
text() {
|
|
28
28
|
return this._text;
|
|
29
29
|
}
|
|
30
|
-
async _build(
|
|
31
|
-
const yamlDocument = await this.
|
|
30
|
+
async _build() {
|
|
31
|
+
const yamlDocument = await this._page._snapshotForAI();
|
|
32
32
|
this._text = [
|
|
33
33
|
`- Page Snapshot`,
|
|
34
34
|
'```yaml',
|
|
@@ -36,54 +36,7 @@ export class PageSnapshot {
|
|
|
36
36
|
'```',
|
|
37
37
|
].join('\n');
|
|
38
38
|
}
|
|
39
|
-
async _snapshotFrame(frame) {
|
|
40
|
-
const frameIndex = this._frameLocators.push(frame) - 1;
|
|
41
|
-
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true });
|
|
42
|
-
const snapshot = yaml.parseDocument(snapshotString);
|
|
43
|
-
const visit = async (node) => {
|
|
44
|
-
if (yaml.isPair(node)) {
|
|
45
|
-
await Promise.all([
|
|
46
|
-
visit(node.key).then(k => node.key = k),
|
|
47
|
-
visit(node.value).then(v => node.value = v)
|
|
48
|
-
]);
|
|
49
|
-
}
|
|
50
|
-
else if (yaml.isSeq(node) || yaml.isMap(node)) {
|
|
51
|
-
node.items = await Promise.all(node.items.map(visit));
|
|
52
|
-
}
|
|
53
|
-
else if (yaml.isScalar(node)) {
|
|
54
|
-
if (typeof node.value === 'string') {
|
|
55
|
-
const value = node.value;
|
|
56
|
-
if (frameIndex > 0)
|
|
57
|
-
node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
|
|
58
|
-
if (value.startsWith('iframe ')) {
|
|
59
|
-
const ref = value.match(/\[ref=(.*)\]/)?.[1];
|
|
60
|
-
if (ref) {
|
|
61
|
-
try {
|
|
62
|
-
const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`));
|
|
63
|
-
return snapshot.createPair(node.value, childSnapshot);
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
return snapshot.createPair(node.value, '<could not take iframe snapshot>');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return node;
|
|
73
|
-
};
|
|
74
|
-
await visit(snapshot.contents);
|
|
75
|
-
return snapshot;
|
|
76
|
-
}
|
|
77
39
|
refLocator(ref) {
|
|
78
|
-
|
|
79
|
-
const match = ref.match(/^f(\d+)(.*)/);
|
|
80
|
-
if (match) {
|
|
81
|
-
const frameIndex = parseInt(match[1], 10);
|
|
82
|
-
frame = this._frameLocators[frameIndex];
|
|
83
|
-
ref = match[2];
|
|
84
|
-
}
|
|
85
|
-
if (!frame)
|
|
86
|
-
throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
|
|
87
|
-
return frame.locator(`aria-ref=${ref}`);
|
|
40
|
+
return this._page.locator(`aria-ref=${ref}`);
|
|
88
41
|
}
|
|
89
42
|
}
|
package/lib/tab.js
CHANGED
|
@@ -17,7 +17,7 @@ import { PageSnapshot } from './pageSnapshot.js';
|
|
|
17
17
|
export class Tab {
|
|
18
18
|
context;
|
|
19
19
|
page;
|
|
20
|
-
|
|
20
|
+
_consoleMessages = [];
|
|
21
21
|
_requests = new Map();
|
|
22
22
|
_snapshot;
|
|
23
23
|
_onPageClose;
|
|
@@ -25,13 +25,9 @@ export class Tab {
|
|
|
25
25
|
this.context = context;
|
|
26
26
|
this.page = page;
|
|
27
27
|
this._onPageClose = onPageClose;
|
|
28
|
-
page.on('console', event => this.
|
|
28
|
+
page.on('console', event => this._consoleMessages.push(event));
|
|
29
29
|
page.on('request', request => this._requests.set(request, null));
|
|
30
30
|
page.on('response', response => this._requests.set(response.request(), response));
|
|
31
|
-
page.on('framenavigated', frame => {
|
|
32
|
-
if (!frame.parentFrame())
|
|
33
|
-
this._clearCollectedArtifacts();
|
|
34
|
-
});
|
|
35
31
|
page.on('close', () => this._onClose());
|
|
36
32
|
page.on('filechooser', chooser => {
|
|
37
33
|
this.context.setModalState({
|
|
@@ -48,7 +44,7 @@ export class Tab {
|
|
|
48
44
|
page.setDefaultTimeout(5000);
|
|
49
45
|
}
|
|
50
46
|
_clearCollectedArtifacts() {
|
|
51
|
-
this.
|
|
47
|
+
this._consoleMessages.length = 0;
|
|
52
48
|
this._requests.clear();
|
|
53
49
|
}
|
|
54
50
|
_onClose() {
|
|
@@ -56,6 +52,7 @@ export class Tab {
|
|
|
56
52
|
this._onPageClose(this);
|
|
57
53
|
}
|
|
58
54
|
async navigate(url) {
|
|
55
|
+
this._clearCollectedArtifacts();
|
|
59
56
|
const downloadEvent = this.page.waitForEvent('download').catch(() => { });
|
|
60
57
|
try {
|
|
61
58
|
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
@@ -85,8 +82,8 @@ export class Tab {
|
|
|
85
82
|
throw new Error('No snapshot available');
|
|
86
83
|
return this._snapshot;
|
|
87
84
|
}
|
|
88
|
-
|
|
89
|
-
return this.
|
|
85
|
+
consoleMessages() {
|
|
86
|
+
return this._consoleMessages;
|
|
90
87
|
}
|
|
91
88
|
requests() {
|
|
92
89
|
return this._requests;
|
package/lib/tools/common.js
CHANGED
|
@@ -18,18 +18,37 @@ import { defineTool } from './tool.js';
|
|
|
18
18
|
const wait = captureSnapshot => defineTool({
|
|
19
19
|
capability: 'wait',
|
|
20
20
|
schema: {
|
|
21
|
-
name: '
|
|
22
|
-
title: 'Wait',
|
|
23
|
-
description: 'Wait for a specified time
|
|
21
|
+
name: 'browser_wait_for',
|
|
22
|
+
title: 'Wait for',
|
|
23
|
+
description: 'Wait for text to appear or disappear or a specified time to pass',
|
|
24
24
|
inputSchema: z.object({
|
|
25
|
-
time: z.number().describe('The time to wait in seconds'),
|
|
25
|
+
time: z.number().optional().describe('The time to wait in seconds'),
|
|
26
|
+
text: z.string().optional().describe('The text to wait for'),
|
|
27
|
+
textGone: z.string().optional().describe('The text to wait for to disappear'),
|
|
26
28
|
}),
|
|
27
29
|
type: 'readOnly',
|
|
28
30
|
},
|
|
29
31
|
handle: async (context, params) => {
|
|
30
|
-
|
|
32
|
+
if (!params.text && !params.textGone && !params.time)
|
|
33
|
+
throw new Error('Either time, text or textGone must be provided');
|
|
34
|
+
const code = [];
|
|
35
|
+
if (params.time) {
|
|
36
|
+
code.push(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
|
|
37
|
+
await new Promise(f => setTimeout(f, Math.min(10000, params.time * 1000)));
|
|
38
|
+
}
|
|
39
|
+
const tab = context.currentTabOrDie();
|
|
40
|
+
const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
|
|
41
|
+
const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;
|
|
42
|
+
if (goneLocator) {
|
|
43
|
+
code.push(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
|
|
44
|
+
await goneLocator.waitFor({ state: 'hidden' });
|
|
45
|
+
}
|
|
46
|
+
if (locator) {
|
|
47
|
+
code.push(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
|
|
48
|
+
await locator.waitFor({ state: 'visible' });
|
|
49
|
+
}
|
|
31
50
|
return {
|
|
32
|
-
code
|
|
51
|
+
code,
|
|
33
52
|
captureSnapshot,
|
|
34
53
|
waitForNetwork: false,
|
|
35
54
|
};
|
package/lib/tools/console.js
CHANGED
|
@@ -25,7 +25,7 @@ const console = defineTool({
|
|
|
25
25
|
type: 'readOnly',
|
|
26
26
|
},
|
|
27
27
|
handle: async (context) => {
|
|
28
|
-
const messages = context.currentTabOrDie().
|
|
28
|
+
const messages = context.currentTabOrDie().consoleMessages();
|
|
29
29
|
const log = messages.map(message => `[${message.type().toUpperCase()}] ${message.text()}`).join('\n');
|
|
30
30
|
return {
|
|
31
31
|
code: [`// <internal code to get console messages>`],
|
package/lib/tools/install.js
CHANGED
|
@@ -28,7 +28,7 @@ const install = defineTool({
|
|
|
28
28
|
type: 'destructive',
|
|
29
29
|
},
|
|
30
30
|
handle: async (context) => {
|
|
31
|
-
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.
|
|
31
|
+
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
|
|
32
32
|
const cliUrl = import.meta.resolve('playwright/package.json');
|
|
33
33
|
const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
|
|
34
34
|
const child = fork(cliPath, ['install', channel], {
|
package/lib/tools/pdf.js
CHANGED
|
@@ -17,18 +17,21 @@ import { z } from 'zod';
|
|
|
17
17
|
import { defineTool } from './tool.js';
|
|
18
18
|
import * as javascript from '../javascript.js';
|
|
19
19
|
import { outputFile } from '../config.js';
|
|
20
|
+
const pdfSchema = z.object({
|
|
21
|
+
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
|
|
22
|
+
});
|
|
20
23
|
const pdf = defineTool({
|
|
21
24
|
capability: 'pdf',
|
|
22
25
|
schema: {
|
|
23
26
|
name: 'browser_pdf_save',
|
|
24
27
|
title: 'Save as PDF',
|
|
25
28
|
description: 'Save page as PDF',
|
|
26
|
-
inputSchema:
|
|
29
|
+
inputSchema: pdfSchema,
|
|
27
30
|
type: 'readOnly',
|
|
28
31
|
},
|
|
29
|
-
handle: async (context) => {
|
|
32
|
+
handle: async (context, params) => {
|
|
30
33
|
const tab = context.currentTabOrDie();
|
|
31
|
-
const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.pdf`);
|
|
34
|
+
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
|
|
32
35
|
const code = [
|
|
33
36
|
`// Save page as ${fileName}`,
|
|
34
37
|
`await page.pdf(${javascript.formatObject({ path: fileName })});`,
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -188,6 +188,7 @@ const selectOption = defineTool({
|
|
|
188
188
|
});
|
|
189
189
|
const screenshotSchema = z.object({
|
|
190
190
|
raw: z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
|
|
191
|
+
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
|
|
191
192
|
element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
|
|
192
193
|
ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
|
|
193
194
|
}).refine(data => {
|
|
@@ -209,7 +210,7 @@ const screenshot = defineTool({
|
|
|
209
210
|
const tab = context.currentTabOrDie();
|
|
210
211
|
const snapshot = tab.snapshotOrDie();
|
|
211
212
|
const fileType = params.raw ? 'png' : 'jpeg';
|
|
212
|
-
const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.${fileType}`);
|
|
213
|
+
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
|
213
214
|
const options = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName };
|
|
214
215
|
const isElementScreenshot = params.element && params.ref;
|
|
215
216
|
const code = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playwright/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "Playwright Tools for MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -37,14 +37,13 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
39
39
|
"commander": "^13.1.0",
|
|
40
|
-
"playwright": "1.53.0-alpha-
|
|
41
|
-
"yaml": "^2.7.1",
|
|
40
|
+
"playwright": "1.53.0-alpha-1746832516000",
|
|
42
41
|
"zod-to-json-schema": "^3.24.4"
|
|
43
42
|
},
|
|
44
43
|
"devDependencies": {
|
|
45
44
|
"@eslint/eslintrc": "^3.2.0",
|
|
46
45
|
"@eslint/js": "^9.19.0",
|
|
47
|
-
"@playwright/test": "1.53.0-alpha-
|
|
46
|
+
"@playwright/test": "1.53.0-alpha-1746832516000",
|
|
48
47
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
49
48
|
"@types/node": "^22.13.10",
|
|
50
49
|
"@typescript-eslint/eslint-plugin": "^8.26.1",
|