@narumitw/pi-chrome-devtools 0.53.2 → 0.53.3

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 (40) hide show
  1. package/README.md +1 -0
  2. package/dist/chunks/{browser-settings-menu-IMB6HXSU.js → browser-settings-menu-SR7KLZT6.js} +16 -42
  3. package/dist/chunks/browser-settings-menu-SR7KLZT6.js.map +7 -0
  4. package/dist/chunks/{chunk-GTTYS5GT.js → chunk-4KMN4SFF.js} +2 -2
  5. package/dist/chunks/chunk-4KMN4SFF.js.map +7 -0
  6. package/dist/chunks/{chunk-SRT3UUIP.js → chunk-F5VG35UI.js} +23 -74
  7. package/dist/chunks/chunk-F5VG35UI.js.map +7 -0
  8. package/dist/chunks/{chunk-LLXOCRNF.js → chunk-MZCGM6D3.js} +12 -33
  9. package/dist/chunks/chunk-MZCGM6D3.js.map +7 -0
  10. package/dist/chunks/{menu-UJETYMSB.js → menu-YR4VRT7D.js} +10 -23
  11. package/dist/chunks/menu-YR4VRT7D.js.map +7 -0
  12. package/dist/chunks/{tools-6P7EUX37.js → tools-6R7Z6HWJ.js} +17 -59
  13. package/dist/chunks/tools-6R7Z6HWJ.js.map +7 -0
  14. package/dist/index.ts +18 -42
  15. package/dist/index.ts.map +2 -2
  16. package/package.json +54 -55
  17. package/src/browser-manager.ts +872 -942
  18. package/src/browser-settings-menu.ts +315 -350
  19. package/src/cdp-client.ts +381 -398
  20. package/src/chrome-devtools.ts +205 -232
  21. package/src/display.ts +26 -29
  22. package/src/lazy-tools.ts +158 -181
  23. package/src/menu.ts +409 -446
  24. package/src/render.ts +72 -81
  25. package/src/runtime.ts +137 -140
  26. package/src/screenshot.ts +170 -186
  27. package/src/settings.ts +472 -523
  28. package/src/tool-names.ts +8 -14
  29. package/src/tool-selector.ts +242 -271
  30. package/src/tools.ts +218 -226
  31. package/src/webmcp/discovery.ts +184 -188
  32. package/src/webmcp/policy.ts +296 -329
  33. package/src/webmcp/protocol.ts +324 -337
  34. package/src/webmcp/tools.ts +136 -147
  35. package/dist/chunks/browser-settings-menu-IMB6HXSU.js.map +0 -7
  36. package/dist/chunks/chunk-GTTYS5GT.js.map +0 -7
  37. package/dist/chunks/chunk-LLXOCRNF.js.map +0 -7
  38. package/dist/chunks/chunk-SRT3UUIP.js.map +0 -7
  39. package/dist/chunks/menu-UJETYMSB.js.map +0 -7
  40. package/dist/chunks/tools-6P7EUX37.js.map +0 -7
package/src/tools.ts CHANGED
@@ -1,260 +1,252 @@
1
1
  import { defineTool } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import {
4
- formatPage,
5
- getPage,
6
- listPages,
7
- resolvePage,
8
- resolvePageForNavigation,
9
- setActivePageId,
10
- textResult,
11
- withCdp,
4
+ formatPage,
5
+ getPage,
6
+ listPages,
7
+ resolvePage,
8
+ resolvePageForNavigation,
9
+ setActivePageId,
10
+ textResult,
11
+ withCdp,
12
12
  } from "./cdp-client.js";
13
13
  import { renderScreenshotResult, renderTextResult, renderToolCall, withStatus } from "./render.js";
14
14
  import { formatScreenshotText, saveScreenshot, throwIfAborted } from "./screenshot.js";
15
15
  import { CHROME_DEVTOOLS_TOOL_NAMES, WEBMCP_TOOL_NAMES } from "./tool-names.js";
16
16
 
17
17
  export const listPagesTool = defineTool({
18
- name: CHROME_DEVTOOLS_TOOL_NAMES[0],
19
- label: "Chrome DevTools: List Pages",
20
- description: "List Chrome tabs/pages from a running Chrome DevTools Protocol endpoint.",
21
- parameters: Type.Object({}),
22
- renderCall: renderToolCall("list pages"),
23
- renderResult: renderTextResult,
24
- async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
25
- return withStatus(ctx, "list pages", async () => {
26
- const pages = await listPages({ sessionOwner: ctx.sessionManager, signal });
27
- return textResult(JSON.stringify(pages.map(formatPage), null, 2), { pages });
28
- });
29
- },
18
+ name: CHROME_DEVTOOLS_TOOL_NAMES[0],
19
+ label: "Chrome DevTools: List Pages",
20
+ description: "List Chrome tabs/pages from a running Chrome DevTools Protocol endpoint.",
21
+ parameters: Type.Object({}),
22
+ renderCall: renderToolCall("list pages"),
23
+ renderResult: renderTextResult,
24
+ async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
25
+ return withStatus(ctx, "list pages", async () => {
26
+ const pages = await listPages({ sessionOwner: ctx.sessionManager, signal });
27
+ return textResult(JSON.stringify(pages.map(formatPage), null, 2), { pages });
28
+ });
29
+ },
30
30
  });
31
31
 
32
32
  export const selectPageTool = defineTool({
33
- name: CHROME_DEVTOOLS_TOOL_NAMES[1],
34
- label: "Chrome DevTools: Select Page",
35
- description: "Select the active Chrome page for later chrome_devtools_* tool calls.",
36
- parameters: Type.Object({
37
- pageId: Type.String({ description: "Page id from chrome_devtools_list_pages." }),
38
- }),
39
- renderCall: renderToolCall("select page"),
40
- renderResult: renderTextResult,
41
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
42
- return withStatus(ctx, "select page", async () => {
43
- const page = await getPage(params.pageId, { sessionOwner: ctx.sessionManager, signal });
44
- setActivePageId(ctx.sessionManager, page.id);
45
- return textResult(`Selected page ${page.id}: ${page.title}\n${page.url}`, {
46
- page: formatPage(page),
47
- });
48
- });
49
- },
33
+ name: CHROME_DEVTOOLS_TOOL_NAMES[1],
34
+ label: "Chrome DevTools: Select Page",
35
+ description: "Select the active Chrome page for later chrome_devtools_* tool calls.",
36
+ parameters: Type.Object({
37
+ pageId: Type.String({ description: "Page id from chrome_devtools_list_pages." }),
38
+ }),
39
+ renderCall: renderToolCall("select page"),
40
+ renderResult: renderTextResult,
41
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
42
+ return withStatus(ctx, "select page", async () => {
43
+ const page = await getPage(params.pageId, { sessionOwner: ctx.sessionManager, signal });
44
+ setActivePageId(ctx.sessionManager, page.id);
45
+ return textResult(`Selected page ${page.id}: ${page.title}\n${page.url}`, {
46
+ page: formatPage(page),
47
+ });
48
+ });
49
+ },
50
50
  });
51
51
 
52
52
  export const navigateTool = defineTool({
53
- name: CHROME_DEVTOOLS_TOOL_NAMES[2],
54
- label: "Chrome DevTools: Navigate",
55
- description:
56
- "Navigate a Chrome page to a URL through Chrome DevTools Protocol, creating a page first if none is available.",
57
- parameters: Type.Object({
58
- url: Type.String({ description: "URL to navigate to." }),
59
- pageId: Type.Optional(
60
- Type.String({ description: "Optional page id. Defaults to selected or first page." }),
61
- ),
62
- }),
63
- renderCall: renderToolCall("navigate"),
64
- renderResult: renderTextResult,
65
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
66
- return withStatus(ctx, "navigate", async () => {
67
- const { created, page } = await resolvePageForNavigation(params.pageId, {
68
- sessionOwner: ctx.sessionManager,
69
- signal,
70
- });
71
- const result = await withCdp(page, async (client) => {
72
- await client.send("Page.enable");
73
- return client.send("Page.navigate", { url: params.url });
74
- });
53
+ name: CHROME_DEVTOOLS_TOOL_NAMES[2],
54
+ label: "Chrome DevTools: Navigate",
55
+ description:
56
+ "Navigate a Chrome page to a URL through Chrome DevTools Protocol, creating a page first if none is available.",
57
+ parameters: Type.Object({
58
+ url: Type.String({ description: "URL to navigate to." }),
59
+ pageId: Type.Optional(Type.String({ description: "Optional page id. Defaults to selected or first page." })),
60
+ }),
61
+ renderCall: renderToolCall("navigate"),
62
+ renderResult: renderTextResult,
63
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
64
+ return withStatus(ctx, "navigate", async () => {
65
+ const { created, page } = await resolvePageForNavigation(params.pageId, {
66
+ sessionOwner: ctx.sessionManager,
67
+ signal,
68
+ });
69
+ const result = await withCdp(page, async (client) => {
70
+ await client.send("Page.enable");
71
+ return client.send("Page.navigate", { url: params.url });
72
+ });
75
73
 
76
- setActivePageId(ctx.sessionManager, page.id);
77
- const action = created ? "Created page and navigated" : "Navigated";
78
- return textResult(`${action} ${page.id} to ${params.url}`, {
79
- created,
80
- page: formatPage(page),
81
- result,
82
- });
83
- });
84
- },
74
+ setActivePageId(ctx.sessionManager, page.id);
75
+ const action = created ? "Created page and navigated" : "Navigated";
76
+ return textResult(`${action} ${page.id} to ${params.url}`, {
77
+ created,
78
+ page: formatPage(page),
79
+ result,
80
+ });
81
+ });
82
+ },
85
83
  });
86
84
 
87
85
  export const evaluateTool = defineTool({
88
- name: CHROME_DEVTOOLS_TOOL_NAMES[3],
89
- label: "Chrome DevTools: Evaluate",
90
- description: "Evaluate JavaScript in a Chrome page through Chrome DevTools Protocol.",
91
- parameters: Type.Object({
92
- expression: Type.String({ description: "JavaScript expression to evaluate." }),
93
- pageId: Type.Optional(
94
- Type.String({ description: "Optional page id. Defaults to selected or first page." }),
95
- ),
96
- awaitPromise: Type.Optional(
97
- Type.Boolean({ description: "Whether to await a returned Promise. Defaults to true." }),
98
- ),
99
- }),
100
- renderCall: renderToolCall("evaluate"),
101
- renderResult: renderTextResult,
102
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
103
- return withStatus(ctx, "evaluate", async () => {
104
- const page = await resolvePage(params.pageId, {
105
- sessionOwner: ctx.sessionManager,
106
- signal,
107
- });
108
- const result = await withCdp(page, (client) =>
109
- client.send("Runtime.evaluate", {
110
- expression: params.expression,
111
- awaitPromise: params.awaitPromise ?? true,
112
- returnByValue: true,
113
- }),
114
- );
86
+ name: CHROME_DEVTOOLS_TOOL_NAMES[3],
87
+ label: "Chrome DevTools: Evaluate",
88
+ description: "Evaluate JavaScript in a Chrome page through Chrome DevTools Protocol.",
89
+ parameters: Type.Object({
90
+ expression: Type.String({ description: "JavaScript expression to evaluate." }),
91
+ pageId: Type.Optional(Type.String({ description: "Optional page id. Defaults to selected or first page." })),
92
+ awaitPromise: Type.Optional(
93
+ Type.Boolean({ description: "Whether to await a returned Promise. Defaults to true." }),
94
+ ),
95
+ }),
96
+ renderCall: renderToolCall("evaluate"),
97
+ renderResult: renderTextResult,
98
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
99
+ return withStatus(ctx, "evaluate", async () => {
100
+ const page = await resolvePage(params.pageId, {
101
+ sessionOwner: ctx.sessionManager,
102
+ signal,
103
+ });
104
+ const result = await withCdp(page, (client) =>
105
+ client.send("Runtime.evaluate", {
106
+ expression: params.expression,
107
+ awaitPromise: params.awaitPromise ?? true,
108
+ returnByValue: true,
109
+ }),
110
+ );
115
111
 
116
- setActivePageId(ctx.sessionManager, page.id);
117
- return textResult(JSON.stringify(result, null, 2), { page: formatPage(page), result });
118
- });
119
- },
112
+ setActivePageId(ctx.sessionManager, page.id);
113
+ return textResult(JSON.stringify(result, null, 2), { page: formatPage(page), result });
114
+ });
115
+ },
120
116
  });
121
117
 
122
118
  export const webMcpListToolsTool = defineTool({
123
- name: WEBMCP_TOOL_NAMES[0],
124
- label: "Chrome DevTools: List WebMCP Tools (Experimental)",
125
- description:
126
- "List bounded page-provided WebMCP tool descriptors from the selected Chrome page through the experimental CDP WebMCP domain. Definitions remain in this tool result and are never registered as dynamic Pi tools.",
127
- parameters: Type.Object({
128
- pageId: Type.Optional(
129
- Type.String({
130
- description: "Optional page id. Defaults to selected or first page.",
131
- maxLength: 512,
132
- }),
133
- ),
134
- }),
135
- renderCall: renderToolCall("list WebMCP tools"),
136
- renderResult: renderTextResult,
137
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
138
- return withStatus(ctx, "list WebMCP tools", async () => {
139
- const implementation = await import("./webmcp/tools.js");
140
- return implementation.executeWebMcpListTool(params, signal, ctx);
141
- });
142
- },
119
+ name: WEBMCP_TOOL_NAMES[0],
120
+ label: "Chrome DevTools: List WebMCP Tools (Experimental)",
121
+ description:
122
+ "List bounded page-provided WebMCP tool descriptors from the selected Chrome page through the experimental CDP WebMCP domain. Definitions remain in this tool result and are never registered as dynamic Pi tools.",
123
+ parameters: Type.Object({
124
+ pageId: Type.Optional(
125
+ Type.String({
126
+ description: "Optional page id. Defaults to selected or first page.",
127
+ maxLength: 512,
128
+ }),
129
+ ),
130
+ }),
131
+ renderCall: renderToolCall("list WebMCP tools"),
132
+ renderResult: renderTextResult,
133
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
134
+ return withStatus(ctx, "list WebMCP tools", async () => {
135
+ const implementation = await import("./webmcp/tools.js");
136
+ return implementation.executeWebMcpListTool(params, signal, ctx);
137
+ });
138
+ },
143
139
  });
144
140
 
145
141
  export const webMcpCallTool = defineTool({
146
- name: WEBMCP_TOOL_NAMES[1],
147
- label: "Chrome DevTools: Call WebMCP Tool (Experimental)",
148
- description:
149
- "Call one previously listed page-provided WebMCP tool after exact page, document, frame, origin, schema, annotation, and session revalidation. Every call requires observable user confirmation and output is bounded to Pi's 50 KB or 2,000-line limit.",
150
- parameters: Type.Object({
151
- sessionGeneration: Type.String({
152
- description: "Session generation token returned by chrome_devtools_webmcp_list_tools.",
153
- maxLength: 100,
154
- }),
155
- pageId: Type.String({ description: "Page id returned by the list tool.", maxLength: 512 }),
156
- documentId: Type.String({
157
- description: "Document loader id returned by the list tool.",
158
- maxLength: 512,
159
- }),
160
- frameId: Type.String({ description: "Frame id returned by the list tool.", maxLength: 512 }),
161
- frameOrigin: Type.String({
162
- description: "Exact frame origin returned by the list tool.",
163
- maxLength: 2_048,
164
- }),
165
- toolName: Type.String({
166
- description: "Exact tool name returned by the list tool.",
167
- maxLength: 512,
168
- }),
169
- schemaDigest: Type.String({
170
- description: "Schema and annotation digest returned by the list tool.",
171
- minLength: 64,
172
- maxLength: 64,
173
- pattern: "^[a-f0-9]{64}$",
174
- }),
175
- input: Type.Record(Type.String(), Type.Unknown(), {
176
- description: "JSON object matching the page-provided input schema.",
177
- }),
178
- }),
179
- renderCall: renderToolCall("call WebMCP tool"),
180
- renderResult: renderTextResult,
181
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
182
- return withStatus(ctx, "call WebMCP tool", async () => {
183
- const implementation = await import("./webmcp/tools.js");
184
- return implementation.executeWebMcpCallTool(params, signal, ctx);
185
- });
186
- },
142
+ name: WEBMCP_TOOL_NAMES[1],
143
+ label: "Chrome DevTools: Call WebMCP Tool (Experimental)",
144
+ description:
145
+ "Call one previously listed page-provided WebMCP tool after exact page, document, frame, origin, schema, annotation, and session revalidation. Every call requires observable user confirmation and output is bounded to Pi's 50 KB or 2,000-line limit.",
146
+ parameters: Type.Object({
147
+ sessionGeneration: Type.String({
148
+ description: "Session generation token returned by chrome_devtools_webmcp_list_tools.",
149
+ maxLength: 100,
150
+ }),
151
+ pageId: Type.String({ description: "Page id returned by the list tool.", maxLength: 512 }),
152
+ documentId: Type.String({
153
+ description: "Document loader id returned by the list tool.",
154
+ maxLength: 512,
155
+ }),
156
+ frameId: Type.String({ description: "Frame id returned by the list tool.", maxLength: 512 }),
157
+ frameOrigin: Type.String({
158
+ description: "Exact frame origin returned by the list tool.",
159
+ maxLength: 2_048,
160
+ }),
161
+ toolName: Type.String({
162
+ description: "Exact tool name returned by the list tool.",
163
+ maxLength: 512,
164
+ }),
165
+ schemaDigest: Type.String({
166
+ description: "Schema and annotation digest returned by the list tool.",
167
+ minLength: 64,
168
+ maxLength: 64,
169
+ pattern: "^[a-f0-9]{64}$",
170
+ }),
171
+ input: Type.Record(Type.String(), Type.Unknown(), {
172
+ description: "JSON object matching the page-provided input schema.",
173
+ }),
174
+ }),
175
+ renderCall: renderToolCall("call WebMCP tool"),
176
+ renderResult: renderTextResult,
177
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
178
+ return withStatus(ctx, "call WebMCP tool", async () => {
179
+ const implementation = await import("./webmcp/tools.js");
180
+ return implementation.executeWebMcpCallTool(params, signal, ctx);
181
+ });
182
+ },
187
183
  });
188
184
 
189
185
  export const screenshotTool = defineTool({
190
- name: CHROME_DEVTOOLS_TOOL_NAMES[4],
191
- label: "Chrome DevTools: Screenshot",
192
- description: "Capture a PNG screenshot from a Chrome page through Chrome DevTools Protocol.",
193
- parameters: Type.Object({
194
- pageId: Type.Optional(
195
- Type.String({ description: "Optional page id. Defaults to selected or first page." }),
196
- ),
197
- fullPage: Type.Optional(
198
- Type.Boolean({ description: "Capture the full document, not just the viewport." }),
199
- ),
200
- savePath: Type.Optional(
201
- Type.String({
202
- description:
203
- "Screenshot is always saved as a PNG file. Optional output path; omitted defaults to a unique temp file. Relative paths resolve from the current working directory. A single leading @ is stripped to match Pi file-mention paths. Existing regular files are replaced.",
204
- }),
205
- ),
206
- }),
207
- renderCall: renderToolCall("screenshot"),
208
- renderResult: renderScreenshotResult,
209
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
210
- return withStatus(ctx, "screenshot", async () => {
211
- const page = await resolvePage(params.pageId, {
212
- sessionOwner: ctx.sessionManager,
213
- signal,
214
- });
215
- const result = await withCdp(page, async (client) => {
216
- throwIfAborted(signal);
217
- await client.send("Page.enable");
186
+ name: CHROME_DEVTOOLS_TOOL_NAMES[4],
187
+ label: "Chrome DevTools: Screenshot",
188
+ description: "Capture a PNG screenshot from a Chrome page through Chrome DevTools Protocol.",
189
+ parameters: Type.Object({
190
+ pageId: Type.Optional(Type.String({ description: "Optional page id. Defaults to selected or first page." })),
191
+ fullPage: Type.Optional(Type.Boolean({ description: "Capture the full document, not just the viewport." })),
192
+ savePath: Type.Optional(
193
+ Type.String({
194
+ description:
195
+ "Screenshot is always saved as a PNG file. Optional output path; omitted defaults to a unique temp file. Relative paths resolve from the current working directory. A single leading @ is stripped to match Pi file-mention paths. Existing regular files are replaced.",
196
+ }),
197
+ ),
198
+ }),
199
+ renderCall: renderToolCall("screenshot"),
200
+ renderResult: renderScreenshotResult,
201
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
202
+ return withStatus(ctx, "screenshot", async () => {
203
+ const page = await resolvePage(params.pageId, {
204
+ sessionOwner: ctx.sessionManager,
205
+ signal,
206
+ });
207
+ const result = await withCdp(page, async (client) => {
208
+ throwIfAborted(signal);
209
+ await client.send("Page.enable");
218
210
 
219
- if (!params.fullPage) {
220
- return client.send<{ data: string }>("Page.captureScreenshot", { format: "png" });
221
- }
211
+ if (!params.fullPage) {
212
+ return client.send<{ data: string }>("Page.captureScreenshot", { format: "png" });
213
+ }
222
214
 
223
- const metrics = await client.send<{
224
- contentSize: { x: number; y: number; width: number; height: number };
225
- }>("Page.getLayoutMetrics");
215
+ const metrics = await client.send<{
216
+ contentSize: { x: number; y: number; width: number; height: number };
217
+ }>("Page.getLayoutMetrics");
226
218
 
227
- throwIfAborted(signal);
228
- return client.send<{ data: string }>("Page.captureScreenshot", {
229
- captureBeyondViewport: true,
230
- format: "png",
231
- clip: {
232
- x: metrics.contentSize.x,
233
- y: metrics.contentSize.y,
234
- width: metrics.contentSize.width,
235
- height: metrics.contentSize.height,
236
- scale: 1,
237
- },
238
- });
239
- });
219
+ throwIfAborted(signal);
220
+ return client.send<{ data: string }>("Page.captureScreenshot", {
221
+ captureBeyondViewport: true,
222
+ format: "png",
223
+ clip: {
224
+ x: metrics.contentSize.x,
225
+ y: metrics.contentSize.y,
226
+ width: metrics.contentSize.width,
227
+ height: metrics.contentSize.height,
228
+ scale: 1,
229
+ },
230
+ });
231
+ });
240
232
 
241
- setActivePageId(ctx.sessionManager, page.id);
242
- const savedScreenshot = await saveScreenshot(result.data, params.savePath, ctx.cwd, signal);
243
- return {
244
- content: [
245
- {
246
- type: "text",
247
- text: formatScreenshotText(page, savedScreenshot),
248
- },
249
- { type: "image", data: result.data, mimeType: "image/png" },
250
- ],
251
- details: {
252
- page: formatPage(page),
253
- bytes: savedScreenshot.bytes,
254
- savedPath: savedScreenshot.savedPath,
255
- isDefaultPath: savedScreenshot.isDefaultPath,
256
- },
257
- };
258
- });
259
- },
233
+ setActivePageId(ctx.sessionManager, page.id);
234
+ const savedScreenshot = await saveScreenshot(result.data, params.savePath, ctx.cwd, signal);
235
+ return {
236
+ content: [
237
+ {
238
+ type: "text",
239
+ text: formatScreenshotText(page, savedScreenshot),
240
+ },
241
+ { type: "image", data: result.data, mimeType: "image/png" },
242
+ ],
243
+ details: {
244
+ page: formatPage(page),
245
+ bytes: savedScreenshot.bytes,
246
+ savedPath: savedScreenshot.savedPath,
247
+ isDefaultPath: savedScreenshot.isDefaultPath,
248
+ },
249
+ };
250
+ });
251
+ },
260
252
  });