@mcp-b/chrome-devtools-mcp 1.6.2 → 1.7.0-canary.20260214192802

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 CHANGED
@@ -8,7 +8,7 @@
8
8
  [![27 Tools](https://img.shields.io/badge/MCP_Tools-27-green?style=flat-square)](./docs/tool-reference.md)
9
9
  [![Chrome](https://img.shields.io/badge/Chrome-DevTools-4285F4?style=flat-square&logo=googlechrome)](https://developer.chrome.com/docs/devtools/)
10
10
 
11
- 📖 **[WebMCP Documentation](https://docs.mcp-b.ai)** | 🚀 **[Quick Start](https://docs.mcp-b.ai/quickstart)** | 🔌 **[Connecting Agents](https://docs.mcp-b.ai/connecting-agents)** | 🎯 **[Chrome DevTools Quickstart](https://github.com/WebMCP-org/chrome-devtools-quickstart)**
11
+ **[WebMCP Documentation](https://docs.mcp-b.ai)** | **[Quick Start](https://docs.mcp-b.ai/quickstart)** | **[Connecting Agents](https://docs.mcp-b.ai/connecting-agents)** | **[Chrome DevTools Quickstart](https://github.com/WebMCP-org/chrome-devtools-quickstart)**
12
12
 
13
13
  **@mcp-b/chrome-devtools-mcp** lets AI coding agents like Claude, Gemini, Cursor, and Copilot control and inspect a live Chrome browser via the Model Context Protocol (MCP). Get performance insights, debug network requests, take screenshots, and interact with website-specific MCP tools through WebMCP integration.
14
14
 
@@ -48,13 +48,13 @@ This fork adds **WebMCP integration** - the ability to call MCP tools that are r
48
48
 
49
49
  | Feature | Chrome DevTools MCP | @mcp-b/chrome-devtools-mcp |
50
50
  |---------|--------------------|-----------------------------|
51
- | Browser automation | | |
52
- | Performance analysis | | |
53
- | Network inspection | | |
54
- | Screenshot/snapshot | | |
55
- | **Call website MCP tools** | | |
56
- | **List website MCP tools** | | |
57
- | **AI-driven tool development** | | |
51
+ | Browser automation | Yes | Yes |
52
+ | Performance analysis | Yes | Yes |
53
+ | Network inspection | Yes | Yes |
54
+ | Screenshot/snapshot | Yes | Yes |
55
+ | **Call website MCP tools** | No | Yes |
56
+ | **List website MCP tools** | No | Yes |
57
+ | **AI-driven tool development** | No | Yes |
58
58
 
59
59
  The key addition is automatic WebMCP tool discovery and registration. When you visit a page with [@mcp-b/global](https://www.npmjs.com/package/@mcp-b/global), its tools are automatically registered as first-class MCP tools that your AI agent can call directly.
60
60
 
@@ -167,8 +167,10 @@ export class McpContext {
167
167
  }
168
168
  await page.evaluate(WEB_MCP_BRIDGE_SCRIPT);
169
169
  }
170
- catch {
171
- // Page might not be ready or accessible, ignore
170
+ catch (err) {
171
+ // Page might not be ready or accessible - log for debugging
172
+ const message = err instanceof Error ? err.message : String(err);
173
+ this.logger(`Bridge injection skipped for ${page.url()}: ${message}`);
172
174
  }
173
175
  }
174
176
  this.logger('WebMCP bridge injected into existing pages');
@@ -191,7 +193,10 @@ export class McpContext {
191
193
  this.#toolHub = hub;
192
194
  // Trigger auto-detection for all existing pages asynchronously
193
195
  this.#setupWebMCPAutoDetectionForAllPages().catch(err => {
194
- this.logger('Error setting up WebMCP auto-detection:', err);
196
+ const message = err instanceof Error ? err.message : String(err);
197
+ this.logger(`WebMCP auto-detection setup failed: ${message}`);
198
+ // Note: This is non-fatal - individual page connections may still work
199
+ // when explicitly requested via getWebMCPClient()
195
200
  });
196
201
  }
197
202
  /**
@@ -202,11 +207,23 @@ export class McpContext {
202
207
  }
203
208
  /**
204
209
  * Get or create a browser-level CDP session for window operations.
210
+ * Recreates the session if it has become invalid.
205
211
  */
206
212
  async #getBrowserCdpSession() {
207
- if (!this.#browserCdpSession) {
208
- this.#browserCdpSession = await this.browser.target().createCDPSession();
213
+ if (this.#browserCdpSession) {
214
+ // Verify session is still valid by attempting a simple operation
215
+ try {
216
+ await this.#browserCdpSession.send('Browser.getVersion');
217
+ return this.#browserCdpSession;
218
+ }
219
+ catch (err) {
220
+ // Session is stale, recreate it
221
+ const message = err instanceof Error ? err.message : String(err);
222
+ this.logger(`Browser CDP session stale (${message}), recreating...`);
223
+ this.#browserCdpSession = undefined;
224
+ }
209
225
  }
226
+ this.#browserCdpSession = await this.browser.target().createCDPSession();
210
227
  return this.#browserCdpSession;
211
228
  }
212
229
  /**
@@ -235,6 +252,44 @@ export class McpContext {
235
252
  getSessionWindowId() {
236
253
  return this.#sessionWindowId;
237
254
  }
255
+ /**
256
+ * Close all pages in the session's window.
257
+ * Called during cleanup when the MCP server is shutting down.
258
+ */
259
+ async closeSessionWindow() {
260
+ if (this.#sessionWindowId === undefined) {
261
+ this.logger('No session window to close');
262
+ return;
263
+ }
264
+ this.logger(`Closing session window ${this.#sessionWindowId}...`);
265
+ // Get all pages in this session's window and close them
266
+ const pagesToClose = [...this.#pages];
267
+ for (const page of pagesToClose) {
268
+ try {
269
+ if (!page.isClosed()) {
270
+ await page.close();
271
+ }
272
+ }
273
+ catch (err) {
274
+ // Ignore errors during cleanup - page might already be closed
275
+ const message = err instanceof Error ? err.message : String(err);
276
+ this.logger(`Error closing page during cleanup: ${message}`);
277
+ }
278
+ }
279
+ // Clean up the CDP session
280
+ if (this.#browserCdpSession) {
281
+ try {
282
+ await this.#browserCdpSession.detach();
283
+ }
284
+ catch (err) {
285
+ // Detach errors during cleanup are expected - log for debugging
286
+ const message = err instanceof Error ? err.message : String(err);
287
+ this.logger(`CDP detach during cleanup: ${message}`);
288
+ }
289
+ this.#browserCdpSession = undefined;
290
+ }
291
+ this.logger('Session window closed');
292
+ }
238
293
  /**
239
294
  * Set up automatic WebMCP detection for a page.
240
295
  * This installs listeners that detect WebMCP after navigation and sync tools.
@@ -270,7 +325,17 @@ export class McpContext {
270
325
  // Immediately try to connect - no polling needed
271
326
  // If no WebMCP, connection will timeout gracefully
272
327
  this.#tryConnectWebMCP(page).catch(err => {
273
- this.logger('WebMCP connection attempt failed (expected if page has no WebMCP):', err);
328
+ const message = err instanceof Error ? err.message : String(err);
329
+ // Differentiate expected timeouts from unexpected errors
330
+ const isExpected = message.includes('timeout') ||
331
+ message.includes('WebMCP not detected') ||
332
+ message.includes('server did not respond');
333
+ if (isExpected) {
334
+ this.logger(`No WebMCP detected after navigation: ${page.url()}`);
335
+ }
336
+ else {
337
+ this.logger(`Unexpected WebMCP connection error for ${page.url()}: ${message}`);
338
+ }
274
339
  });
275
340
  };
276
341
  page.on('framenavigated', onFrameNavigated);
@@ -324,7 +389,17 @@ export class McpContext {
324
389
  this.#setupWebMCPAutoDetection(page);
325
390
  // Try to connect immediately (don't await - run in parallel for all pages)
326
391
  this.#tryConnectWebMCP(page).catch(err => {
327
- this.logger('Initial WebMCP connection attempt failed (expected):', err);
392
+ const message = err instanceof Error ? err.message : String(err);
393
+ // Differentiate expected timeouts from unexpected errors
394
+ const isExpected = message.includes('timeout') ||
395
+ message.includes('WebMCP not detected') ||
396
+ message.includes('server did not respond');
397
+ if (isExpected) {
398
+ this.logger(`No WebMCP on page during initial scan: ${page.url()}`);
399
+ }
400
+ else {
401
+ this.logger(`Unexpected error during initial WebMCP scan for ${page.url()}: ${message}`);
402
+ }
328
403
  });
329
404
  }
330
405
  }
@@ -412,8 +487,10 @@ export class McpContext {
412
487
  await existingPage.bringToFront();
413
488
  }
414
489
  }
415
- catch {
490
+ catch (err) {
416
491
  // Best effort - focus might fail if page is closing
492
+ const message = err instanceof Error ? err.message : String(err);
493
+ this.logger(`Window focus failed (non-critical): ${message}`);
417
494
  }
418
495
  }
419
496
  const page = await this.browser.newPage();
@@ -427,8 +504,10 @@ export class McpContext {
427
504
  `Tab may not be visible in list_pages.`);
428
505
  }
429
506
  }
430
- catch {
507
+ catch (err) {
431
508
  // Failed to get windowId - page might be in an unexpected state
509
+ const message = err instanceof Error ? err.message : String(err);
510
+ this.logger(`Window ID check failed (non-critical): ${message}`);
432
511
  }
433
512
  }
434
513
  await this.createPagesSnapshot();
@@ -631,6 +710,10 @@ export class McpContext {
631
710
  */
632
711
  async createPagesSnapshot() {
633
712
  const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
713
+ this.logger(`createPagesSnapshot: found ${allPages.length} total pages`);
714
+ for (const page of allPages) {
715
+ this.logger(` - ${page.url()}`);
716
+ }
634
717
  // First filter: DevTools pages (unless experimental mode is enabled)
635
718
  let filteredPages = allPages.filter(page => {
636
719
  return (this.#options.experimentalDevToolsDebugging ||
@@ -642,23 +725,50 @@ export class McpContext {
642
725
  const windowFilteredPages = [];
643
726
  // Check window IDs in parallel for better performance
644
727
  const windowIdResults = await Promise.allSettled(filteredPages.map(async (page) => {
645
- try {
646
- const windowId = await this.getWindowIdForPage(page);
647
- return { page, windowId };
728
+ // Try to get windowId with retry for pages that might be in transitional state
729
+ let windowId = null;
730
+ let lastError = null;
731
+ for (let attempt = 0; attempt < 3; attempt++) {
732
+ try {
733
+ windowId = await this.getWindowIdForPage(page);
734
+ break; // Success, exit retry loop
735
+ }
736
+ catch (err) {
737
+ lastError = err instanceof Error ? err.message : String(err);
738
+ if (attempt < 2) {
739
+ // Wait before retry (50ms, then 100ms)
740
+ await new Promise(r => setTimeout(r, 50 * (attempt + 1)));
741
+ }
742
+ }
648
743
  }
649
- catch {
650
- // Page might be closing, exclude it
651
- return null;
744
+ if (windowId !== null) {
745
+ return { page, windowId, url: page.url() };
652
746
  }
747
+ // All retries failed
748
+ this.logger(`Failed to get windowId for page ${page.url()} after 3 attempts: ${lastError}`);
749
+ return null;
653
750
  }));
654
751
  for (const result of windowIdResults) {
655
- if (result.status === 'fulfilled' &&
656
- result.value &&
657
- result.value.windowId === this.#sessionWindowId) {
658
- windowFilteredPages.push(result.value.page);
752
+ if (result.status === 'fulfilled' && result.value) {
753
+ if (result.value.windowId === this.#sessionWindowId) {
754
+ windowFilteredPages.push(result.value.page);
755
+ }
756
+ else {
757
+ this.logger(`Excluding page ${result.value.url} (windowId ${result.value.windowId} != session ${this.#sessionWindowId})`);
758
+ }
659
759
  }
660
760
  }
661
761
  filteredPages = windowFilteredPages;
762
+ this.logger(`Window filter: ${windowFilteredPages.length} pages in session window ${this.#sessionWindowId}`);
763
+ // Always include the explicitly selected page even if window filtering excluded it
764
+ // This handles edge cases where windowId lookup fails after cross-origin navigation
765
+ if (this.#pageExplicitlySelected &&
766
+ this.#selectedPage &&
767
+ !this.#selectedPage.isClosed() &&
768
+ !filteredPages.includes(this.#selectedPage)) {
769
+ this.logger(`Re-adding explicitly selected page that was filtered out: ${this.#selectedPage.url()}`);
770
+ filteredPages.unshift(this.#selectedPage);
771
+ }
662
772
  }
663
773
  this.#pages = filteredPages;
664
774
  // Only auto-select pages[0] if:
@@ -709,7 +819,8 @@ export class McpContext {
709
819
  }
710
820
  }
711
821
  catch (error) {
712
- this.logger('Issue occurred while trying to find DevTools', error);
822
+ const message = error instanceof Error ? error.message : String(error);
823
+ this.logger(`DevTools detection failed for ${devToolsPage.url()}: ${message}`);
713
824
  }
714
825
  }
715
826
  }
@@ -805,7 +916,8 @@ export class McpContext {
805
916
  try {
806
917
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
807
918
  const filename = path.join(dir, `screenshot.${getExtensionFromMimeType(mimeType)}`);
808
- await fs.writeFile(filename, data);
919
+ // Use mode 0o600 (owner read/write only) for secure temp file creation
920
+ await fs.writeFile(filename, data, { mode: 0o600 });
809
921
  return { filename };
810
922
  }
811
923
  catch (err) {
@@ -816,7 +928,8 @@ export class McpContext {
816
928
  async saveFile(data, filename) {
817
929
  try {
818
930
  const filePath = path.resolve(filename);
819
- await fs.writeFile(filePath, data);
931
+ // Use mode 0o644 (owner read/write, others read) for user-specified paths
932
+ await fs.writeFile(filePath, data, { mode: 0o644 });
820
933
  return { filename };
821
934
  }
822
935
  catch (err) {
@@ -879,8 +992,10 @@ export class McpContext {
879
992
  try {
880
993
  await conn.client.close();
881
994
  }
882
- catch {
883
- // Ignore close errors
995
+ catch (err) {
996
+ // Close errors during reconnection are expected - log for debugging
997
+ const message = err instanceof Error ? err.message : String(err);
998
+ this.logger(`WebMCP client close during reconnection: ${message}`);
884
999
  }
885
1000
  this.#webMCPConnections.delete(targetPage);
886
1001
  }
package/build/src/cli.js CHANGED
@@ -170,10 +170,11 @@ export function parseArguments(version, argv = process.argv) {
170
170
  .check(args => {
171
171
  // We can't set default in the options else
172
172
  // Yargs will complain
173
- if (!args.channel &&
174
- !args.browserUrl &&
175
- !args.wsEndpoint &&
176
- !args.executablePath) {
173
+ // Note: Use explicit undefined checks since empty strings are valid falsy values
174
+ if (args.channel === undefined &&
175
+ args.browserUrl === undefined &&
176
+ args.wsEndpoint === undefined &&
177
+ args.executablePath === undefined) {
177
178
  args.channel = 'dev';
178
179
  }
179
180
  return true;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { ISSUE_UTILS } from '../issue-descriptions.js';
7
+ import { logger } from '../logger.js';
8
+ import { DevTools } from '../third_party/index.js';
9
+ export class IssueFormatter {
10
+ #issue;
11
+ #options;
12
+ constructor(issue, options) {
13
+ this.#issue = issue;
14
+ this.#options = options;
15
+ }
16
+ toString() {
17
+ const title = this.#getTitle();
18
+ const count = this.#issue.getAggregatedIssuesCount();
19
+ const idPart = this.#options.id !== undefined ? `msgid=${this.#options.id} ` : '';
20
+ return `${idPart}[issue] ${title} (count: ${count})`;
21
+ }
22
+ toStringDetailed() {
23
+ const result = [];
24
+ if (this.#options.id !== undefined) {
25
+ result.push(`ID: ${this.#options.id}`);
26
+ }
27
+ const bodyParts = [];
28
+ const description = this.#getDescription();
29
+ let processedMarkdown = description?.trim();
30
+ // Remove heading in order not to conflict with the whole console message response markdown
31
+ if (processedMarkdown?.startsWith('# ')) {
32
+ processedMarkdown = processedMarkdown.substring(2).trimStart();
33
+ }
34
+ if (processedMarkdown) {
35
+ bodyParts.push(processedMarkdown);
36
+ }
37
+ else {
38
+ bodyParts.push(this.#getTitle() ?? 'Unknown Issue');
39
+ }
40
+ const links = this.#issue.getDescription()?.links;
41
+ if (links && links.length > 0) {
42
+ bodyParts.push('Learn more:');
43
+ for (const link of links) {
44
+ bodyParts.push(`[${link.linkTitle}](${link.link})`);
45
+ }
46
+ }
47
+ const affectedResources = this.#getAffectedResources();
48
+ if (affectedResources.length) {
49
+ bodyParts.push('### Affected resources');
50
+ bodyParts.push(...affectedResources.map(item => {
51
+ const details = [];
52
+ if (item.uid) {
53
+ details.push(`uid=${item.uid}`);
54
+ }
55
+ if (item.request) {
56
+ details.push((typeof item.request === 'number' ? `reqid=` : 'url=') +
57
+ item.request);
58
+ }
59
+ if (item.data) {
60
+ details.push(`data=${JSON.stringify(item.data)}`);
61
+ }
62
+ return details.join(' ');
63
+ }));
64
+ }
65
+ result.push(`Message: issue> ${bodyParts.join('\n')}`);
66
+ return result.join('\n');
67
+ }
68
+ toJSON() {
69
+ return {
70
+ type: 'issue',
71
+ title: this.#getTitle(),
72
+ count: this.#issue.getAggregatedIssuesCount(),
73
+ id: this.#options.id,
74
+ };
75
+ }
76
+ toJSONDetailed() {
77
+ return {
78
+ id: this.#options.id,
79
+ type: 'issue',
80
+ title: this.#getTitle(),
81
+ description: this.#getDescription(),
82
+ links: this.#issue.getDescription()?.links,
83
+ affectedResources: this.#getAffectedResources(),
84
+ };
85
+ }
86
+ #getAffectedResources() {
87
+ const issues = this.#issue.getAllIssues();
88
+ const affectedResources = [];
89
+ for (const singleIssue of issues) {
90
+ const details = singleIssue.details();
91
+ if (!details) {
92
+ continue;
93
+ }
94
+ // We send the remaining details as untyped JSON because the DevTools
95
+ // frontend code is currently not re-usable.
96
+ const data = structuredClone(details);
97
+ let uid;
98
+ let request;
99
+ if ('violatingNodeId' in details &&
100
+ details.violatingNodeId &&
101
+ this.#options.elementIdResolver) {
102
+ uid = this.#options.elementIdResolver(details.violatingNodeId);
103
+ delete data.violatingNodeId;
104
+ }
105
+ if ('nodeId' in details &&
106
+ details.nodeId &&
107
+ this.#options.elementIdResolver) {
108
+ uid = this.#options.elementIdResolver(details.nodeId);
109
+ delete data.nodeId;
110
+ }
111
+ if ('documentNodeId' in details &&
112
+ details.documentNodeId &&
113
+ this.#options.elementIdResolver) {
114
+ uid = this.#options.elementIdResolver(details.documentNodeId);
115
+ delete data.documentNodeId;
116
+ }
117
+ if ('request' in details && details.request) {
118
+ request = details.request.url;
119
+ if (details.request.requestId && this.#options.requestIdResolver) {
120
+ const resolvedId = this.#options.requestIdResolver(details.request.requestId);
121
+ if (resolvedId) {
122
+ request = resolvedId;
123
+ const requestData = data.request;
124
+ delete requestData.requestId;
125
+ }
126
+ }
127
+ }
128
+ // These fields has no use for the MCP client (redundant or irrelevant).
129
+ delete data.errorType;
130
+ delete data.frameId;
131
+ affectedResources.push({
132
+ uid,
133
+ data: data,
134
+ request,
135
+ });
136
+ }
137
+ return affectedResources;
138
+ }
139
+ isValid() {
140
+ return this.#getTitle() !== undefined;
141
+ }
142
+ // Helper to extract title
143
+ #getTitle() {
144
+ const markdownDescription = this.#issue.getDescription();
145
+ const filename = markdownDescription?.file;
146
+ if (!filename) {
147
+ logger(`no description found for issue:` + this.#issue.code());
148
+ return undefined;
149
+ }
150
+ // We already have the description logic in #getDescription, but title extraction is separate
151
+ // We can reuse the logic or cache it.
152
+ // Ideally we should process markdown once.
153
+ const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
154
+ if (!rawMarkdown) {
155
+ logger(`no markdown ${filename} found for issue:` + this.#issue.code());
156
+ return undefined;
157
+ }
158
+ try {
159
+ const processedMarkdown = DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription?.substitutions);
160
+ const markdownAst = DevTools.Marked.Marked.lexer(processedMarkdown);
161
+ const title = DevTools.MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
162
+ if (!title) {
163
+ logger('cannot read issue title from ' + filename);
164
+ return undefined;
165
+ }
166
+ return title;
167
+ }
168
+ catch {
169
+ logger('error parsing markdown for issue ' + this.#issue.code());
170
+ return undefined;
171
+ }
172
+ }
173
+ #getDescription() {
174
+ const markdownDescription = this.#issue.getDescription();
175
+ const filename = markdownDescription?.file;
176
+ if (!filename) {
177
+ return undefined;
178
+ }
179
+ const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
180
+ if (!rawMarkdown) {
181
+ return undefined;
182
+ }
183
+ try {
184
+ return DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription?.substitutions);
185
+ }
186
+ catch {
187
+ return undefined;
188
+ }
189
+ }
190
+ }
package/build/src/main.js CHANGED
@@ -27,6 +27,39 @@ const VERSION = '1.5.6';
27
27
  process.on('unhandledRejection', (reason, promise) => {
28
28
  logger('Unhandled promise rejection', promise, reason);
29
29
  });
30
+ /**
31
+ * Cleanup handler for graceful shutdown.
32
+ * Closes the session window when the MCP server is killed.
33
+ */
34
+ let isCleaningUp = false;
35
+ async function cleanup() {
36
+ if (isCleaningUp)
37
+ return;
38
+ isCleaningUp = true;
39
+ logger('Shutting down MCP server...');
40
+ if (context) {
41
+ try {
42
+ await context.closeSessionWindow();
43
+ }
44
+ catch (err) {
45
+ logger('Error during cleanup:', err);
46
+ }
47
+ }
48
+ process.exit(0);
49
+ }
50
+ // Handle various termination signals
51
+ process.on('SIGINT', () => {
52
+ logger('Received SIGINT');
53
+ cleanup();
54
+ });
55
+ process.on('SIGTERM', () => {
56
+ logger('Received SIGTERM');
57
+ cleanup();
58
+ });
59
+ process.on('SIGHUP', () => {
60
+ logger('Received SIGHUP');
61
+ cleanup();
62
+ });
30
63
  export const args = parseArguments(VERSION);
31
64
  const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
32
65
  logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import process from 'node:process';
7
+ import { logger } from '../logger.js';
8
+ import { FilePersistence } from './persistence.js';
9
+ import { WatchdogMessageType, OsType } from './types.js';
10
+ import { WatchdogClient } from './watchdog-client.js';
11
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
12
+ function detectOsType() {
13
+ switch (process.platform) {
14
+ case 'win32':
15
+ return OsType.OS_TYPE_WINDOWS;
16
+ case 'darwin':
17
+ return OsType.OS_TYPE_MACOS;
18
+ case 'linux':
19
+ return OsType.OS_TYPE_LINUX;
20
+ default:
21
+ return OsType.OS_TYPE_UNSPECIFIED;
22
+ }
23
+ }
24
+ export class ClearcutLogger {
25
+ #persistence;
26
+ #watchdog;
27
+ constructor(options) {
28
+ this.#persistence = options.persistence ?? new FilePersistence();
29
+ this.#watchdog =
30
+ options.watchdogClient ??
31
+ new WatchdogClient({
32
+ parentPid: process.pid,
33
+ appVersion: options.appVersion,
34
+ osType: detectOsType(),
35
+ logFile: options.logFile,
36
+ clearcutEndpoint: options.clearcutEndpoint,
37
+ clearcutForceFlushIntervalMs: options.clearcutForceFlushIntervalMs,
38
+ clearcutIncludePidHeader: options.clearcutIncludePidHeader,
39
+ });
40
+ }
41
+ async logToolInvocation(args) {
42
+ this.#watchdog.send({
43
+ type: WatchdogMessageType.LOG_EVENT,
44
+ payload: {
45
+ tool_invocation: {
46
+ tool_name: args.toolName,
47
+ success: args.success,
48
+ latency_ms: args.latencyMs,
49
+ },
50
+ },
51
+ });
52
+ }
53
+ async logServerStart(flagUsage) {
54
+ this.#watchdog.send({
55
+ type: WatchdogMessageType.LOG_EVENT,
56
+ payload: {
57
+ server_start: {
58
+ flag_usage: flagUsage,
59
+ },
60
+ },
61
+ });
62
+ }
63
+ async logDailyActiveIfNeeded() {
64
+ try {
65
+ const state = await this.#persistence.loadState();
66
+ if (this.#shouldLogDailyActive(state)) {
67
+ let daysSince = -1;
68
+ if (state.lastActive) {
69
+ const lastActiveDate = new Date(state.lastActive);
70
+ const now = new Date();
71
+ const diffTime = Math.abs(now.getTime() - lastActiveDate.getTime());
72
+ daysSince = Math.ceil(diffTime / MS_PER_DAY);
73
+ }
74
+ this.#watchdog.send({
75
+ type: WatchdogMessageType.LOG_EVENT,
76
+ payload: {
77
+ daily_active: {
78
+ days_since_last_active: daysSince,
79
+ },
80
+ },
81
+ });
82
+ state.lastActive = new Date().toISOString();
83
+ await this.#persistence.saveState(state);
84
+ }
85
+ }
86
+ catch (err) {
87
+ logger('Error in logDailyActiveIfNeeded:', err);
88
+ }
89
+ }
90
+ #shouldLogDailyActive(state) {
91
+ if (!state.lastActive) {
92
+ return true;
93
+ }
94
+ const lastActiveDate = new Date(state.lastActive);
95
+ const now = new Date();
96
+ // Compare UTC dates
97
+ const isSameDay = lastActiveDate.getUTCFullYear() === now.getUTCFullYear() &&
98
+ lastActiveDate.getUTCMonth() === now.getUTCMonth() &&
99
+ lastActiveDate.getUTCDate() === now.getUTCDate();
100
+ return !isSameDay;
101
+ }
102
+ }