chrome-devtools-mcp 0.21.0 → 0.23.0

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 (51) hide show
  1. package/README.md +87 -21
  2. package/build/src/HeapSnapshotManager.js +94 -0
  3. package/build/src/McpContext.js +26 -181
  4. package/build/src/McpPage.js +214 -0
  5. package/build/src/McpResponse.js +151 -13
  6. package/build/src/PageCollector.js +10 -24
  7. package/build/src/TextSnapshot.js +230 -0
  8. package/build/src/WaitForHelper.js +31 -0
  9. package/build/src/bin/check-latest-version.js +25 -0
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +34 -10
  11. package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
  12. package/build/src/bin/chrome-devtools.js +25 -14
  13. package/build/src/bin/cliDefinitions.js +14 -8
  14. package/build/src/daemon/client.js +11 -11
  15. package/build/src/daemon/daemon.js +6 -9
  16. package/build/src/daemon/utils.js +19 -14
  17. package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
  18. package/build/src/formatters/NetworkFormatter.js +24 -7
  19. package/build/src/index.js +12 -1
  20. package/build/src/telemetry/ClearcutLogger.js +34 -12
  21. package/build/src/telemetry/flagUtils.js +46 -4
  22. package/build/src/telemetry/toolMetricsUtils.js +88 -0
  23. package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
  24. package/build/src/third_party/THIRD_PARTY_NOTICES +59 -32
  25. package/build/src/third_party/bundled-packages.json +6 -4
  26. package/build/src/third_party/devtools-formatter-worker.js +61 -64
  27. package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
  28. package/build/src/third_party/index.js +62661 -60590
  29. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3501 -2658
  30. package/build/src/tools/categories.js +3 -0
  31. package/build/src/tools/console.js +42 -39
  32. package/build/src/tools/emulation.js +1 -1
  33. package/build/src/tools/extensions.js +5 -11
  34. package/build/src/tools/inPage.js +3 -13
  35. package/build/src/tools/input.js +15 -16
  36. package/build/src/tools/lighthouse.js +2 -2
  37. package/build/src/tools/memory.js +48 -3
  38. package/build/src/tools/network.js +4 -4
  39. package/build/src/tools/pages.js +212 -146
  40. package/build/src/tools/performance.js +1 -1
  41. package/build/src/tools/screencast.js +20 -8
  42. package/build/src/tools/screenshot.js +3 -3
  43. package/build/src/tools/script.js +22 -16
  44. package/build/src/tools/tools.js +2 -0
  45. package/build/src/tools/webmcp.js +63 -0
  46. package/build/src/utils/check-for-updates.js +73 -0
  47. package/build/src/utils/files.js +4 -0
  48. package/build/src/utils/id.js +15 -0
  49. package/build/src/version.js +1 -1
  50. package/package.json +13 -8
  51. package/build/src/utils/ExtensionRegistry.js +0 -35
@@ -11,19 +11,20 @@ import process from 'node:process';
11
11
  import { logger } from '../logger.js';
12
12
  import { Client, PipeTransport, StdioClientTransport, } from '../third_party/index.js';
13
13
  import { VERSION } from '../version.js';
14
- import { DAEMON_CLIENT_NAME, getDaemonPid, getPidFilePath, getSocketPath, INDEX_SCRIPT_PATH, IS_WINDOWS, isDaemonRunning, } from './utils.js';
15
- const pid = getDaemonPid();
16
- if (isDaemonRunning(pid)) {
14
+ import { DAEMON_CLIENT_NAME, getPidFilePath, getSocketPath, INDEX_SCRIPT_PATH, IS_WINDOWS, isDaemonRunning, } from './utils.js';
15
+ const sessionId = process.env.CHROME_DEVTOOLS_MCP_SESSION_ID || '';
16
+ logger(`Daemon sessionId: ${sessionId}`);
17
+ if (isDaemonRunning(sessionId)) {
17
18
  logger('Another daemon process is running.');
18
19
  process.exit(1);
19
20
  }
20
- const pidFilePath = getPidFilePath();
21
+ const pidFilePath = getPidFilePath(sessionId);
21
22
  fs.mkdirSync(path.dirname(pidFilePath), {
22
23
  recursive: true,
23
24
  });
24
25
  fs.writeFileSync(pidFilePath, process.pid.toString());
25
26
  logger(`Writing ${process.pid.toString()} to ${pidFilePath}`);
26
- const socketPath = getSocketPath();
27
+ const socketPath = getSocketPath(sessionId);
27
28
  const startDate = new Date();
28
29
  const mcpServerArgs = process.argv.slice(2);
29
30
  let mcpClient = null;
@@ -32,10 +33,6 @@ let server = null;
32
33
  async function setupMCPClient() {
33
34
  console.log('Setting up MCP client connection...');
34
35
  // Create stdio transport for chrome-devtools-mcp
35
- // Workaround for https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/client/stdio.ts#L128
36
- // which causes the console window to show on Windows.
37
- // @ts-expect-error no types for type.
38
- process.type = 'mcp-client';
39
36
  mcpTransport = new StdioClientTransport({
40
37
  command: process.execPath,
41
38
  args: [INDEX_SCRIPT_PATH, ...mcpServerArgs],
@@ -13,46 +13,50 @@ export const INDEX_SCRIPT_PATH = path.join(import.meta.dirname, '..', 'bin', 'ch
13
13
  const APP_NAME = 'chrome-devtools-mcp';
14
14
  export const DAEMON_CLIENT_NAME = 'chrome-devtools-cli-daemon';
15
15
  // Using these paths due to strict limits on the POSIX socket path length.
16
- export function getSocketPath() {
16
+ export function getSocketPath(sessionId) {
17
17
  const uid = os.userInfo().uid;
18
+ const suffix = sessionId ? `-${sessionId}` : '';
19
+ const appName = APP_NAME + suffix;
18
20
  if (IS_WINDOWS) {
19
21
  // Windows uses Named Pipes, not file paths.
20
22
  // This format is required for server.listen()
21
- return path.join('\\\\.\\pipe', APP_NAME, 'server.sock');
23
+ return path.join('\\\\.\\pipe', appName, 'server.sock');
22
24
  }
23
25
  // 1. Try XDG_RUNTIME_DIR (Linux standard, sometimes macOS)
24
26
  if (process.env.XDG_RUNTIME_DIR) {
25
- return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME, 'server.sock');
27
+ return path.join(process.env.XDG_RUNTIME_DIR, appName, 'server.sock');
26
28
  }
27
29
  // 2. macOS/Unix Fallback: Use /tmp/
28
30
  // We use /tmp/ because it is much shorter than ~/Library/Application Support/
29
31
  // and keeps us well under the 104-character limit.
30
- return path.join('/tmp', `${APP_NAME}-${uid}.sock`);
32
+ return path.join('/tmp', `${appName}-${uid}.sock`);
31
33
  }
32
- export function getRuntimeHome() {
34
+ export function getRuntimeHome(sessionId) {
33
35
  const platform = os.platform();
34
36
  const uid = os.userInfo().uid;
37
+ const suffix = sessionId ? `-${sessionId}` : '';
38
+ const appName = APP_NAME + suffix;
35
39
  // 1. Check for the modern Unix standard
36
40
  if (process.env.XDG_RUNTIME_DIR) {
37
- return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME);
41
+ return path.join(process.env.XDG_RUNTIME_DIR, appName);
38
42
  }
39
43
  // 2. Fallback for macOS and older Linux
40
44
  if (platform === 'darwin' || platform === 'linux') {
41
45
  // /tmp is cleared on boot, making it perfect for PIDs
42
- return path.join('/tmp', `${APP_NAME}-${uid}`);
46
+ return path.join('/tmp', `${appName}-${uid}`);
43
47
  }
44
48
  // 3. Windows Fallback
45
- return path.join(os.tmpdir(), APP_NAME);
49
+ return path.join(os.tmpdir(), appName);
46
50
  }
47
51
  export const IS_WINDOWS = os.platform() === 'win32';
48
- export function getPidFilePath() {
49
- const runtimeDir = getRuntimeHome();
52
+ export function getPidFilePath(sessionId) {
53
+ const runtimeDir = getRuntimeHome(sessionId);
50
54
  return path.join(runtimeDir, 'daemon.pid');
51
55
  }
52
- export function getDaemonPid() {
56
+ export function getDaemonPid(sessionId) {
53
57
  try {
54
- const pidFile = getPidFilePath();
55
- logger(`Daemon pid file ${pidFile}`);
58
+ const pidFile = getPidFilePath(sessionId);
59
+ logger(`Daemon pid file ${pidFile} sessionId=${sessionId}`);
56
60
  if (!fs.existsSync(pidFile)) {
57
61
  return null;
58
62
  }
@@ -68,7 +72,8 @@ export function getDaemonPid() {
68
72
  return null;
69
73
  }
70
74
  }
71
- export function isDaemonRunning(pid = getDaemonPid()) {
75
+ export function isDaemonRunning(sessionId) {
76
+ const pid = getDaemonPid(sessionId);
72
77
  if (pid) {
73
78
  try {
74
79
  process.kill(pid, 0); // Throws if process doesn't exist
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { stableIdSymbol } from '../utils/id.js';
7
+ export class HeapSnapshotFormatter {
8
+ #aggregates;
9
+ constructor(aggregates) {
10
+ this.#aggregates = aggregates;
11
+ }
12
+ #getSortedAggregates() {
13
+ return Object.values(this.#aggregates).sort((a, b) => b.self - a.self);
14
+ }
15
+ toString() {
16
+ const sorted = this.#getSortedAggregates();
17
+ const lines = [];
18
+ lines.push('uid,className,count,selfSize,maxRetainedSize');
19
+ for (const info of sorted) {
20
+ const uid = info[stableIdSymbol] ?? '';
21
+ lines.push(`${uid},"${info.name}",${info.count},${info.self},${info.maxRet}`);
22
+ }
23
+ return lines.join('\n');
24
+ }
25
+ toJSON() {
26
+ const sorted = this.#getSortedAggregates();
27
+ return sorted.map(info => ({
28
+ uid: info[stableIdSymbol],
29
+ className: info.name,
30
+ count: info.count,
31
+ selfSize: info.self,
32
+ retainedSize: info.maxRet,
33
+ }));
34
+ }
35
+ static sort(aggregates) {
36
+ return Object.entries(aggregates).sort((a, b) => b[1].self - a[1].self);
37
+ }
38
+ }
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  * */
6
6
  import { isUtf8 } from 'node:buffer';
7
+ import { DevTools, } from '../third_party/index.js';
7
8
  const BODY_CONTEXT_SIZE_LIMIT = 10000;
8
9
  export class NetworkFormatter {
9
10
  #request;
@@ -40,8 +41,8 @@ export class NetworkFormatter {
40
41
  throw new Error('saveFile is not provided');
41
42
  }
42
43
  if (data) {
43
- await this.#options.saveFile(Buffer.from(data), this.#options.requestFilePath);
44
- this.#requestBodyFilePath = this.#options.requestFilePath;
44
+ const result = await this.#options.saveFile(Buffer.from(data), this.#options.requestFilePath, '.network-request');
45
+ this.#requestBodyFilePath = result.filename;
45
46
  }
46
47
  else {
47
48
  this.#requestBody = requestBodyNotAvailableMessage;
@@ -66,8 +67,8 @@ export class NetworkFormatter {
66
67
  if (!this.#options.saveFile) {
67
68
  throw new Error('saveFile is not provided');
68
69
  }
69
- await this.#options.saveFile(buffer, this.#options.responseFilePath);
70
- this.#responseBodyFilePath = this.#options.responseFilePath;
70
+ const result = await this.#options.saveFile(buffer, this.#options.responseFilePath, '.network-response');
71
+ this.#responseBodyFilePath = result.filename;
71
72
  }
72
73
  catch {
73
74
  // Flatten error handling for buffer() failure and save failure
@@ -96,6 +97,16 @@ export class NetworkFormatter {
96
97
  selectedInDevToolsUI: this.#options.selectedInDevToolsUI,
97
98
  };
98
99
  }
100
+ #redactNetworkHeaders(headers) {
101
+ const headersList = Object.entries(headers).map(item => {
102
+ return { name: item[0], value: item[1] };
103
+ });
104
+ const redacted = DevTools.NetworkRequestFormatter.sanitizeHeaders(headersList);
105
+ return redacted.reduce((acc, item) => {
106
+ acc[item.name] = item.value;
107
+ return acc;
108
+ }, {});
109
+ }
99
110
  toJSONDetailed() {
100
111
  const redirectChain = this.#request.redirectChain();
101
112
  const formattedRedirectChain = redirectChain.reverse().map(request => {
@@ -105,15 +116,21 @@ export class NetworkFormatter {
105
116
  const formatter = new NetworkFormatter(request, {
106
117
  requestId: id,
107
118
  saveFile: this.#options.saveFile,
119
+ redactNetworkHeaders: this.#options.redactNetworkHeaders,
108
120
  });
109
121
  return formatter.toJSON();
110
122
  });
123
+ const responseHeaders = this.#request.response()?.headers();
111
124
  return {
112
125
  ...this.toJSON(),
113
- requestHeaders: this.#request.headers(),
126
+ requestHeaders: this.#options.redactNetworkHeaders
127
+ ? this.#redactNetworkHeaders(this.#request.headers())
128
+ : this.#request.headers(),
114
129
  requestBody: this.#requestBody,
115
130
  requestBodyFilePath: this.#requestBodyFilePath,
116
- responseHeaders: this.#request.response()?.headers(),
131
+ responseHeaders: this.#options.redactNetworkHeaders && responseHeaders
132
+ ? this.#redactNetworkHeaders(responseHeaders)
133
+ : this.#request.response()?.headers(),
117
134
  responseBody: this.#responseBody,
118
135
  responseBodyFilePath: this.#responseBodyFilePath,
119
136
  failure: this.#request.failure()?.errorText,
@@ -210,7 +227,7 @@ function converNetworkRequestDetailedToStringDetailed(data) {
210
227
  response.push(`### Redirect chain`);
211
228
  let indent = 0;
212
229
  for (const request of redirectChain.reverse()) {
213
- response.push(`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)})}`);
230
+ response.push(`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)}`);
214
231
  indent++;
215
232
  }
216
233
  }
@@ -101,7 +101,7 @@ export async function createMcpServer(serverArgs, options) {
101
101
  return;
102
102
  }
103
103
  if (tool.annotations.category === ToolCategory.EXTENSIONS &&
104
- !serverArgs.categoryExtensions) {
104
+ serverArgs.categoryExtensions === false) {
105
105
  return;
106
106
  }
107
107
  if (tool.annotations.category === ToolCategory.IN_PAGE &&
@@ -112,6 +112,10 @@ export async function createMcpServer(serverArgs, options) {
112
112
  !serverArgs.experimentalVision) {
113
113
  return;
114
114
  }
115
+ if (tool.annotations.conditions?.includes('experimentalMemory') &&
116
+ !serverArgs.experimentalMemory) {
117
+ return;
118
+ }
115
119
  if (tool.annotations.conditions?.includes('experimentalInteropTools') &&
116
120
  !serverArgs.experimentalInteropTools) {
117
121
  return;
@@ -120,6 +124,10 @@ export async function createMcpServer(serverArgs, options) {
120
124
  !serverArgs.experimentalScreencast) {
121
125
  return;
122
126
  }
127
+ if (tool.annotations.conditions?.includes('experimentalWebmcp') &&
128
+ !serverArgs.experimentalWebmcp) {
129
+ return;
130
+ }
123
131
  const schema = 'pageScoped' in tool &&
124
132
  tool.pageScoped &&
125
133
  serverArgs.experimentalPageIdRouting &&
@@ -142,6 +150,7 @@ export async function createMcpServer(serverArgs, options) {
142
150
  const response = serverArgs.slim
143
151
  ? new SlimMcpResponse(serverArgs)
144
152
  : new McpResponse(serverArgs);
153
+ response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
145
154
  if ('pageScoped' in tool && tool.pageScoped) {
146
155
  const page = serverArgs.experimentalPageIdRouting &&
147
156
  params.pageId &&
@@ -190,6 +199,8 @@ export async function createMcpServer(serverArgs, options) {
190
199
  finally {
191
200
  void clearcutLogger?.logToolInvocation({
192
201
  toolName: tool.name,
202
+ params,
203
+ schema,
193
204
  success,
194
205
  latencyMs: bucketizeLatency(Date.now() - startTime),
195
206
  });
@@ -10,7 +10,7 @@ import { FilePersistence } from './persistence.js';
10
10
  import { McpClient, WatchdogMessageType, OsType, } from './types.js';
11
11
  import { WatchdogClient } from './WatchdogClient.js';
12
12
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
13
- const PARAM_BLOCKLIST = new Set(['uid']);
13
+ export const PARAM_BLOCKLIST = new Set(['uid', 'reqid', 'msgid']);
14
14
  const SUPPORTED_ZOD_TYPES = [
15
15
  'ZodString',
16
16
  'ZodNumber',
@@ -21,7 +21,7 @@ const SUPPORTED_ZOD_TYPES = [
21
21
  function isZodType(type) {
22
22
  return SUPPORTED_ZOD_TYPES.includes(type);
23
23
  }
24
- function getZodType(zodType) {
24
+ export function getZodType(zodType) {
25
25
  const def = zodType._def;
26
26
  const typeName = def.typeName;
27
27
  if (typeName === 'ZodOptional' ||
@@ -37,15 +37,31 @@ function getZodType(zodType) {
37
37
  }
38
38
  throw new Error(`Unsupported zod type for tool parameter: ${typeName}`);
39
39
  }
40
- function transformName(zodType, name) {
40
+ export function transformArgName(zodType, name) {
41
+ const snakeCaseName = name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
41
42
  if (zodType === 'ZodString') {
42
- return `${name}_length`;
43
+ return `${snakeCaseName}_length`;
43
44
  }
44
45
  else if (zodType === 'ZodArray') {
45
- return `${name}_count`;
46
+ return `${snakeCaseName}_count`;
46
47
  }
47
48
  else {
48
- return name;
49
+ return snakeCaseName;
50
+ }
51
+ }
52
+ export function transformArgType(zodType) {
53
+ if (zodType === 'ZodString' || zodType === 'ZodArray') {
54
+ return 'number';
55
+ }
56
+ switch (zodType) {
57
+ case 'ZodNumber':
58
+ return 'number';
59
+ case 'ZodBoolean':
60
+ return 'boolean';
61
+ case 'ZodEnum':
62
+ return 'enum';
63
+ default:
64
+ throw new Error(`Unsupported zod type for tool parameter: ${zodType}`);
49
65
  }
50
66
  }
51
67
  function transformValue(zodType, value) {
@@ -91,7 +107,7 @@ export function sanitizeParams(params, schema) {
91
107
  if (!hasEquivalentType(zodType, value)) {
92
108
  throw new Error(`parameter ${name} has type ${zodType} but value ${value} is not of equivalent type`);
93
109
  }
94
- const transformedName = transformName(zodType, name);
110
+ const transformedName = transformArgName(zodType, name);
95
111
  const transformedValue = transformValue(zodType, value);
96
112
  transformed[transformedName] = transformedValue;
97
113
  }
@@ -153,15 +169,21 @@ export class ClearcutLogger {
153
169
  }
154
170
  }
155
171
  async logToolInvocation(args) {
172
+ const tool_invocation = {
173
+ tool_name: args.toolName,
174
+ success: args.success,
175
+ latency_ms: args.latencyMs,
176
+ };
177
+ if (Object.keys(args.params).length > 0) {
178
+ tool_invocation.tool_params = {
179
+ [`${args.toolName}_params`]: sanitizeParams(args.params, args.schema),
180
+ };
181
+ }
156
182
  this.#watchdog.send({
157
183
  type: WatchdogMessageType.LOG_EVENT,
158
184
  payload: {
159
185
  mcp_client: this.#mcpClient,
160
- tool_invocation: {
161
- tool_name: args.toolName,
162
- success: args.success,
163
- latency_ms: args.latencyMs,
164
- },
186
+ tool_invocation: tool_invocation,
165
187
  },
166
188
  });
167
189
  }
@@ -4,6 +4,14 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { toSnakeCase } from '../utils/string.js';
7
+ /**
8
+ * For enums, log the value as uppercase.
9
+ * We're going to have an enum for such flags with choices represented
10
+ * as an `enum` where the keys of the enum will map to the uppercase `choice`.
11
+ */
12
+ function formatEnumChoice(snakeCaseName, choice) {
13
+ return `${snakeCaseName}_${choice}`.toUpperCase();
14
+ }
7
15
  /**
8
16
  * Computes telemetry flag usage from parsed arguments and CLI options.
9
17
  *
@@ -14,6 +22,8 @@ import { toSnakeCase } from '../utils/string.js';
14
22
  * - The provided value differs from the default value.
15
23
  * - Boolean flags are logged with their literal value.
16
24
  * - String flags with defined `choices` (Enums) are logged as their uppercase value.
25
+ *
26
+ * IMPORTANT: keep getPossibleFlagMetrics() in sync with this function.
17
27
  */
18
28
  export function computeFlagUsage(args, options) {
19
29
  const usage = {};
@@ -35,11 +45,43 @@ export function computeFlagUsage(args, options) {
35
45
  typeof value === 'string' &&
36
46
  'choices' in config &&
37
47
  config.choices) {
38
- // For enums, log the value as uppercase
39
- // We're going to have an enum for such flags with choices represented
40
- // as an `enum` where the keys of the enum will map to the uppercase `choice`.
41
- usage[snakeCaseName] = `${snakeCaseName}_${value}`.toUpperCase();
48
+ usage[snakeCaseName] = formatEnumChoice(snakeCaseName, value);
42
49
  }
43
50
  }
44
51
  return usage;
45
52
  }
53
+ /**
54
+ * Computes the list of possible flag metrics based on the CLI options.
55
+ *
56
+ * IMPORTANT: keep this function in sync with computeFlagUsage().
57
+ */
58
+ export function getPossibleFlagMetrics(options) {
59
+ const metrics = [];
60
+ for (const [flagName, config] of Object.entries(options)) {
61
+ const snakeCaseName = toSnakeCase(flagName);
62
+ // _present is always a possible metric
63
+ metrics.push({
64
+ name: `${snakeCaseName}_present`,
65
+ flagType: 'boolean',
66
+ });
67
+ if (config.type === 'boolean') {
68
+ metrics.push({
69
+ name: snakeCaseName,
70
+ flagType: 'boolean',
71
+ });
72
+ }
73
+ else if (config.type === 'string' &&
74
+ 'choices' in config &&
75
+ config.choices) {
76
+ metrics.push({
77
+ name: snakeCaseName,
78
+ flagType: 'enum',
79
+ choices: [
80
+ `${snakeCaseName.toUpperCase()}_UNSPECIFIED`,
81
+ ...config.choices.map(choice => formatEnumChoice(snakeCaseName, choice)),
82
+ ],
83
+ });
84
+ }
85
+ }
86
+ return metrics;
87
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { transformArgName, transformArgType, getZodType, PARAM_BLOCKLIST, } from './ClearcutLogger.js';
7
+ /**
8
+ * Validates that all values in an enum are of the homogeneous primitive type.
9
+ * Returns the primitive type string. Throws an error if heterogeneous.
10
+ */
11
+ export function validateEnumHomogeneity(values) {
12
+ const firstType = typeof values[0];
13
+ for (const val of values) {
14
+ if (typeof val !== firstType) {
15
+ throw new Error('Heterogeneous enum types found');
16
+ }
17
+ }
18
+ return firstType;
19
+ }
20
+ export function applyToExistingMetrics(existing, update) {
21
+ const updated = applyToExisting(existing, update);
22
+ const existingByName = new Map(existing.map(tool => [tool.name, tool]));
23
+ const updatedByName = new Map(update.map(tool => [tool.name, tool]));
24
+ return updated.map(tool => {
25
+ const existingTool = existingByName.get(tool.name);
26
+ const updatedTool = updatedByName.get(tool.name);
27
+ // If the tool still exists in the update, we will update the args.
28
+ if (existingTool && updatedTool) {
29
+ const updatedArgs = applyToExisting(existingTool.args, updatedTool.args);
30
+ return { ...tool, args: updatedArgs };
31
+ }
32
+ return tool;
33
+ });
34
+ }
35
+ function applyToExisting(existing, update) {
36
+ const existingNames = new Set(existing.map(item => item.name));
37
+ const updatedNames = new Set(update.map(item => item.name));
38
+ const result = [];
39
+ // Keep the original ordering.
40
+ for (const entry of existing) {
41
+ const toAdd = { ...entry };
42
+ if (!updatedNames.has(entry.name)) {
43
+ toAdd.isDeprecated = true;
44
+ }
45
+ result.push(toAdd);
46
+ }
47
+ // New entries must be added to the very back of the list.
48
+ for (const entry of update) {
49
+ if (!existingNames.has(entry.name)) {
50
+ result.push({ ...entry });
51
+ }
52
+ }
53
+ return result;
54
+ }
55
+ /**
56
+ * Generates tool metrics from tool definitions.
57
+ */
58
+ export function generateToolMetrics(tools) {
59
+ return tools.map(tool => {
60
+ const args = [];
61
+ for (const [name, schema] of Object.entries(tool.schema)) {
62
+ if (PARAM_BLOCKLIST.has(name)) {
63
+ continue;
64
+ }
65
+ const zodType = getZodType(schema);
66
+ const transformedName = transformArgName(zodType, name);
67
+ let argType = transformArgType(zodType);
68
+ if (argType === 'enum') {
69
+ let values;
70
+ if (schema._def.values?.length > 0) {
71
+ values = schema._def.values;
72
+ }
73
+ else {
74
+ values = schema._def.innerType._def.values;
75
+ }
76
+ argType = validateEnumHomogeneity(values);
77
+ }
78
+ args.push({
79
+ name: transformedName,
80
+ argType,
81
+ });
82
+ }
83
+ return {
84
+ name: tool.name,
85
+ args,
86
+ };
87
+ });
88
+ }
@@ -42,13 +42,14 @@ export class ClearcutSender {
42
42
  this.#sessionId = crypto.randomUUID();
43
43
  this.#sessionCreated = Date.now();
44
44
  }
45
- logger('Enqueing telemetry event', JSON.stringify(event, null, 2));
46
- this.#addToBuffer({
45
+ const eventToSend = {
47
46
  ...event,
48
47
  session_id: this.#sessionId,
49
48
  app_version: this.#appVersion,
50
49
  os_type: this.#osType,
51
- });
50
+ };
51
+ logger('Enqueing telemetry event', JSON.stringify(eventToSend, null, 2));
52
+ this.#addToBuffer(eventToSend);
52
53
  if (!this.#timerStarted) {
53
54
  this.#timerStarted = true;
54
55
  this.#scheduleFlush(this.#flushIntervalMs);