figma-console-mcp 0.1.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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/browser/base.d.ts +50 -0
  4. package/dist/browser/base.d.ts.map +1 -0
  5. package/dist/browser/base.js +6 -0
  6. package/dist/browser/base.js.map +1 -0
  7. package/dist/browser/local.d.ts +66 -0
  8. package/dist/browser/local.d.ts.map +1 -0
  9. package/dist/browser/local.js +223 -0
  10. package/dist/browser/local.js.map +1 -0
  11. package/dist/cloudflare/browser/base.js +5 -0
  12. package/dist/cloudflare/browser/cloudflare.js +156 -0
  13. package/dist/cloudflare/browser-manager.js +157 -0
  14. package/dist/cloudflare/core/config.js +161 -0
  15. package/dist/cloudflare/core/console-monitor.js +382 -0
  16. package/dist/cloudflare/core/enrichment/enrichment-service.js +272 -0
  17. package/dist/cloudflare/core/enrichment/index.js +7 -0
  18. package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
  19. package/dist/cloudflare/core/enrichment/style-resolver.js +326 -0
  20. package/dist/cloudflare/core/figma-api.js +273 -0
  21. package/dist/cloudflare/core/figma-desktop-connector.js +383 -0
  22. package/dist/cloudflare/core/figma-style-extractor.js +311 -0
  23. package/dist/cloudflare/core/figma-tools.js +2299 -0
  24. package/dist/cloudflare/core/logger.js +53 -0
  25. package/dist/cloudflare/core/snippet-injector.js +96 -0
  26. package/dist/cloudflare/core/types/enriched.js +5 -0
  27. package/dist/cloudflare/core/types/index.js +4 -0
  28. package/dist/cloudflare/index.js +1059 -0
  29. package/dist/cloudflare/test-browser.js +88 -0
  30. package/dist/config.d.ts +17 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +141 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/core/config.d.ts +17 -0
  35. package/dist/core/config.d.ts.map +1 -0
  36. package/dist/core/config.js +162 -0
  37. package/dist/core/config.js.map +1 -0
  38. package/dist/core/console-monitor.d.ts +81 -0
  39. package/dist/core/console-monitor.d.ts.map +1 -0
  40. package/dist/core/console-monitor.js +383 -0
  41. package/dist/core/console-monitor.js.map +1 -0
  42. package/dist/core/enrichment/enrichment-service.d.ts +52 -0
  43. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
  44. package/dist/core/enrichment/enrichment-service.js +273 -0
  45. package/dist/core/enrichment/enrichment-service.js.map +1 -0
  46. package/dist/core/enrichment/index.d.ts +8 -0
  47. package/dist/core/enrichment/index.d.ts.map +1 -0
  48. package/dist/core/enrichment/index.js +8 -0
  49. package/dist/core/enrichment/index.js.map +1 -0
  50. package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
  51. package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
  52. package/dist/core/enrichment/relationship-mapper.js +352 -0
  53. package/dist/core/enrichment/relationship-mapper.js.map +1 -0
  54. package/dist/core/enrichment/style-resolver.d.ts +80 -0
  55. package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
  56. package/dist/core/enrichment/style-resolver.js +327 -0
  57. package/dist/core/enrichment/style-resolver.js.map +1 -0
  58. package/dist/core/figma-api.d.ts +137 -0
  59. package/dist/core/figma-api.d.ts.map +1 -0
  60. package/dist/core/figma-api.js +274 -0
  61. package/dist/core/figma-api.js.map +1 -0
  62. package/dist/core/figma-desktop-connector.d.ts +52 -0
  63. package/dist/core/figma-desktop-connector.d.ts.map +1 -0
  64. package/dist/core/figma-desktop-connector.js +384 -0
  65. package/dist/core/figma-desktop-connector.js.map +1 -0
  66. package/dist/core/figma-style-extractor.d.ts +76 -0
  67. package/dist/core/figma-style-extractor.d.ts.map +1 -0
  68. package/dist/core/figma-style-extractor.js +312 -0
  69. package/dist/core/figma-style-extractor.js.map +1 -0
  70. package/dist/core/figma-tools.d.ts +15 -0
  71. package/dist/core/figma-tools.d.ts.map +1 -0
  72. package/dist/core/figma-tools.js +2300 -0
  73. package/dist/core/figma-tools.js.map +1 -0
  74. package/dist/core/logger.d.ts +22 -0
  75. package/dist/core/logger.d.ts.map +1 -0
  76. package/dist/core/logger.js +54 -0
  77. package/dist/core/logger.js.map +1 -0
  78. package/dist/core/snippet-injector.d.ts +24 -0
  79. package/dist/core/snippet-injector.d.ts.map +1 -0
  80. package/dist/core/snippet-injector.js +97 -0
  81. package/dist/core/snippet-injector.js.map +1 -0
  82. package/dist/core/types/enriched.d.ts +213 -0
  83. package/dist/core/types/enriched.d.ts.map +1 -0
  84. package/dist/core/types/enriched.js +6 -0
  85. package/dist/core/types/enriched.js.map +1 -0
  86. package/dist/core/types/index.d.ts +112 -0
  87. package/dist/core/types/index.d.ts.map +1 -0
  88. package/dist/core/types/index.js +5 -0
  89. package/dist/core/types/index.js.map +1 -0
  90. package/dist/index.d.ts +8 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +72 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/local.d.ts +57 -0
  95. package/dist/local.d.ts.map +1 -0
  96. package/dist/local.js +668 -0
  97. package/dist/local.js.map +1 -0
  98. package/dist/logger.d.ts +22 -0
  99. package/dist/logger.d.ts.map +1 -0
  100. package/dist/logger.js +45 -0
  101. package/dist/logger.js.map +1 -0
  102. package/dist/server.d.ts +40 -0
  103. package/dist/server.d.ts.map +1 -0
  104. package/dist/server.js +99 -0
  105. package/dist/server.js.map +1 -0
  106. package/dist/tools/index.d.ts +15 -0
  107. package/dist/tools/index.d.ts.map +1 -0
  108. package/dist/tools/index.js +184 -0
  109. package/dist/tools/index.js.map +1 -0
  110. package/dist/types/index.d.ts +102 -0
  111. package/dist/types/index.d.ts.map +1 -0
  112. package/dist/types/index.js +6 -0
  113. package/dist/types/index.js.map +1 -0
  114. package/figma-desktop-bridge/README.md +232 -0
  115. package/figma-desktop-bridge/code.js +133 -0
  116. package/figma-desktop-bridge/manifest.json +13 -0
  117. package/figma-desktop-bridge/ui.html +200 -0
  118. package/package.json +77 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Configuration management for Figma Console MCP server
3
+ */
4
+ import { readFileSync, existsSync } from 'fs';
5
+ import { homedir } from 'os';
6
+ import { join } from 'path';
7
+ /**
8
+ * Auto-detect server mode based on environment
9
+ */
10
+ function detectMode() {
11
+ // If running in Workers environment, return cloudflare
12
+ if (typeof globalThis !== 'undefined' && 'caches' in globalThis) {
13
+ return 'cloudflare';
14
+ }
15
+ // Explicit env var override
16
+ const modeEnv = process.env.FIGMA_MCP_MODE?.toLowerCase();
17
+ if (modeEnv === 'local' || modeEnv === 'cloudflare') {
18
+ return modeEnv;
19
+ }
20
+ // Default to local for Node.js environments
21
+ return 'local';
22
+ }
23
+ /**
24
+ * Default configuration values
25
+ */
26
+ const DEFAULT_CONFIG = {
27
+ mode: detectMode(),
28
+ browser: {
29
+ headless: false,
30
+ args: [
31
+ '--disable-blink-features=AutomationControlled',
32
+ '--disable-dev-shm-usage',
33
+ '--no-sandbox', // Note: Only use in trusted environments
34
+ ],
35
+ },
36
+ console: {
37
+ bufferSize: 1000,
38
+ filterLevels: ['log', 'info', 'warn', 'error', 'debug'],
39
+ truncation: {
40
+ maxStringLength: 500,
41
+ maxArrayLength: 10,
42
+ maxObjectDepth: 3,
43
+ removeDuplicates: true,
44
+ },
45
+ },
46
+ screenshots: {
47
+ defaultFormat: 'png',
48
+ quality: 90,
49
+ storePath: join(process.env.TMPDIR || '/tmp', 'figma-console-mcp', 'screenshots'),
50
+ },
51
+ local: {
52
+ debugHost: process.env.FIGMA_DEBUG_HOST || 'localhost',
53
+ debugPort: parseInt(process.env.FIGMA_DEBUG_PORT || '9222', 10),
54
+ },
55
+ };
56
+ /**
57
+ * Possible config file locations (checked in order)
58
+ */
59
+ const CONFIG_PATHS = [
60
+ // Environment variable override
61
+ process.env.FIGMA_CONSOLE_CONFIG,
62
+ // Project-local config
63
+ join(process.cwd(), '.figma-console-mcp.json'),
64
+ join(process.cwd(), 'figma-console-mcp.json'),
65
+ // User home config
66
+ join(homedir(), '.config', 'figma-console-mcp', 'config.json'),
67
+ join(homedir(), '.figma-console-mcp.json'),
68
+ ].filter((path) => path !== undefined);
69
+ /**
70
+ * Load configuration from file or use defaults
71
+ */
72
+ export function loadConfig() {
73
+ // Try to load from config file
74
+ for (const configPath of CONFIG_PATHS) {
75
+ if (existsSync(configPath)) {
76
+ try {
77
+ const fileContent = readFileSync(configPath, 'utf-8');
78
+ const userConfig = JSON.parse(fileContent);
79
+ // Deep merge with defaults
80
+ const config = mergeConfig(DEFAULT_CONFIG, userConfig);
81
+ return config;
82
+ }
83
+ catch (error) {
84
+ console.error(`Failed to load config from ${configPath}:`, error);
85
+ // Continue to next config path
86
+ }
87
+ }
88
+ }
89
+ // No config file found, use defaults
90
+ return DEFAULT_CONFIG;
91
+ }
92
+ /**
93
+ * Deep merge two configuration objects
94
+ */
95
+ function mergeConfig(defaults, overrides) {
96
+ return {
97
+ mode: overrides.mode || defaults.mode,
98
+ browser: {
99
+ ...defaults.browser,
100
+ ...(overrides.browser || {}),
101
+ },
102
+ console: {
103
+ ...defaults.console,
104
+ ...(overrides.console || {}),
105
+ truncation: {
106
+ ...defaults.console.truncation,
107
+ ...(overrides.console?.truncation || {}),
108
+ },
109
+ },
110
+ screenshots: {
111
+ ...defaults.screenshots,
112
+ ...(overrides.screenshots || {}),
113
+ },
114
+ local: {
115
+ ...defaults.local,
116
+ ...(overrides.local || {}),
117
+ },
118
+ };
119
+ }
120
+ /**
121
+ * Validate configuration
122
+ */
123
+ export function validateConfig(config) {
124
+ // Validate browser config
125
+ if (!Array.isArray(config.browser.args)) {
126
+ throw new Error('browser.args must be an array');
127
+ }
128
+ // Validate console config
129
+ if (config.console.bufferSize <= 0) {
130
+ throw new Error('console.bufferSize must be positive');
131
+ }
132
+ if (!Array.isArray(config.console.filterLevels)) {
133
+ throw new Error('console.filterLevels must be an array');
134
+ }
135
+ // Validate truncation config
136
+ const { truncation } = config.console;
137
+ if (truncation.maxStringLength <= 0) {
138
+ throw new Error('console.truncation.maxStringLength must be positive');
139
+ }
140
+ if (truncation.maxArrayLength <= 0) {
141
+ throw new Error('console.truncation.maxArrayLength must be positive');
142
+ }
143
+ if (truncation.maxObjectDepth <= 0) {
144
+ throw new Error('console.truncation.maxObjectDepth must be positive');
145
+ }
146
+ // Validate screenshot config
147
+ if (!['png', 'jpeg'].includes(config.screenshots.defaultFormat)) {
148
+ throw new Error('screenshots.defaultFormat must be "png" or "jpeg"');
149
+ }
150
+ if (config.screenshots.quality < 0 || config.screenshots.quality > 100) {
151
+ throw new Error('screenshots.quality must be between 0 and 100');
152
+ }
153
+ }
154
+ /**
155
+ * Get configuration with validation
156
+ */
157
+ export function getConfig() {
158
+ const config = loadConfig();
159
+ validateConfig(config);
160
+ return config;
161
+ }
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Console Monitor
3
+ * Captures and manages console logs from Figma plugins via Chrome DevTools Protocol
4
+ * Monitors both main page console AND Web Worker consoles (where Figma plugins run)
5
+ */
6
+ import { createChildLogger } from './logger.js';
7
+ const logger = createChildLogger({ component: 'console-monitor' });
8
+ /**
9
+ * Console Monitor
10
+ * Listens to page console events and maintains a circular buffer of logs
11
+ * Also monitors Web Workers to capture Figma plugin console logs
12
+ */
13
+ export class ConsoleMonitor {
14
+ constructor(config) {
15
+ this.logs = [];
16
+ this.isMonitoring = false;
17
+ this.page = null; // Supports both puppeteer-core and @cloudflare/puppeteer
18
+ this.workers = new Set();
19
+ this.config = config;
20
+ }
21
+ /**
22
+ * Start monitoring console logs on a page
23
+ * Accepts any puppeteer Page type (puppeteer-core or @cloudflare/puppeteer)
24
+ */
25
+ async startMonitoring(page) {
26
+ if (this.isMonitoring && this.page === page) {
27
+ logger.info('Already monitoring this page');
28
+ return;
29
+ }
30
+ this.page = page;
31
+ this.isMonitoring = true;
32
+ logger.info('Starting console monitoring (page + workers + frames)');
33
+ // DIAGNOSTIC: Log all frames on the page and add to console
34
+ const frames = page.frames();
35
+ logger.info({ frameCount: frames.length }, 'Frames detected on page');
36
+ // Add diagnostic marker to console logs
37
+ this.addLog({
38
+ timestamp: Date.now(),
39
+ level: 'info',
40
+ message: `[MCP DIAGNOSTIC] Monitoring started. Detected ${frames.length} frames and ${page.workers().length} workers.`,
41
+ args: [],
42
+ source: 'page',
43
+ });
44
+ for (const frame of frames) {
45
+ const frameUrl = frame.url();
46
+ const frameName = frame.name() || 'unnamed';
47
+ logger.info({
48
+ frameUrl,
49
+ isDetached: frame.isDetached(),
50
+ name: frameName
51
+ }, 'Frame details');
52
+ // Add frame detection to console logs
53
+ this.addLog({
54
+ timestamp: Date.now(),
55
+ level: 'info',
56
+ message: `[MCP DIAGNOSTIC] Frame detected: ${frameName} - ${frameUrl}`,
57
+ args: [],
58
+ source: 'page',
59
+ });
60
+ }
61
+ // Listen to ALL console events (includes main page, iframes, and workers)
62
+ page.on('console', async (msg) => {
63
+ try {
64
+ const location = msg.location();
65
+ const url = location?.url || 'unknown';
66
+ const text = msg.text();
67
+ const type = msg.type();
68
+ // DIAGNOSTIC: Log every console event with its source
69
+ logger.info({
70
+ type,
71
+ url,
72
+ textPreview: text.substring(0, 100),
73
+ location
74
+ }, 'Console event captured');
75
+ const entry = await this.processConsoleMessage(msg, 'page', url);
76
+ if (entry) {
77
+ this.addLog(entry);
78
+ }
79
+ }
80
+ catch (error) {
81
+ logger.error({ error }, 'Failed to process console message');
82
+ }
83
+ });
84
+ // Listen to page errors
85
+ page.on('pageerror', (error) => {
86
+ this.addLog({
87
+ timestamp: Date.now(),
88
+ level: 'error',
89
+ message: error.message,
90
+ args: [],
91
+ stackTrace: {
92
+ callFrames: error.stack
93
+ ? error.stack.split('\n').map((line) => ({
94
+ functionName: line.trim(),
95
+ url: '',
96
+ lineNumber: 0,
97
+ columnNumber: 0,
98
+ }))
99
+ : [],
100
+ },
101
+ source: 'page',
102
+ });
103
+ });
104
+ // Monitor existing workers (for Figma plugin console logs)
105
+ const existingWorkers = page.workers();
106
+ logger.info({ workerCount: existingWorkers.length }, 'Found existing workers');
107
+ for (const worker of existingWorkers) {
108
+ this.attachWorkerListeners(worker);
109
+ }
110
+ // Listen for new workers being created (e.g., when plugin starts)
111
+ page.on('workercreated', (worker) => {
112
+ logger.info({ workerUrl: worker.url() }, 'New worker created');
113
+ this.attachWorkerListeners(worker);
114
+ });
115
+ // Listen for workers being destroyed
116
+ page.on('workerdestroyed', (worker) => {
117
+ logger.info({ workerUrl: worker.url() }, 'Worker destroyed');
118
+ this.workers.delete(worker);
119
+ });
120
+ // Listen for new frames being attached (e.g., when plugin UI loads)
121
+ page.on('frameattached', (frame) => {
122
+ const frameUrl = frame.url();
123
+ const frameName = frame.name() || 'unnamed';
124
+ logger.info({ frameUrl, frameName }, 'New frame attached');
125
+ // Add diagnostic marker for new frame
126
+ this.addLog({
127
+ timestamp: Date.now(),
128
+ level: 'info',
129
+ message: `[MCP DIAGNOSTIC] New frame attached: ${frameName} - ${frameUrl}`,
130
+ args: [],
131
+ source: 'page',
132
+ });
133
+ });
134
+ // Listen for frames being detached
135
+ page.on('framedetached', (frame) => {
136
+ logger.info({ frameUrl: frame.url() }, 'Frame detached');
137
+ });
138
+ logger.info({
139
+ pageMonitoring: true,
140
+ workerMonitoring: true,
141
+ frameMonitoring: true,
142
+ initialWorkerCount: existingWorkers.length,
143
+ initialFrameCount: frames.length
144
+ }, 'Console monitoring started');
145
+ }
146
+ /**
147
+ * Attach console listeners to a Web Worker
148
+ * This captures Figma plugin console logs
149
+ */
150
+ attachWorkerListeners(worker) {
151
+ this.workers.add(worker);
152
+ const workerUrl = worker.url();
153
+ logger.info({ workerUrl }, 'Attaching console listener to worker');
154
+ // DIAGNOSTIC: Log a marker when worker listener is attached
155
+ this.addLog({
156
+ timestamp: Date.now(),
157
+ level: 'info',
158
+ message: `[MCP] Worker detected and monitored: ${workerUrl}`,
159
+ args: [],
160
+ source: 'plugin',
161
+ workerUrl,
162
+ });
163
+ // Listen to worker console events
164
+ worker.on('console', async (msg) => {
165
+ try {
166
+ logger.info({ workerUrl, type: msg.type(), text: msg.text() }, 'Worker console event received');
167
+ const entry = await this.processConsoleMessage(msg, 'worker', workerUrl);
168
+ if (entry) {
169
+ this.addLog(entry);
170
+ }
171
+ }
172
+ catch (error) {
173
+ logger.error({ error, workerUrl }, 'Failed to process worker console message');
174
+ }
175
+ });
176
+ // Worker doesn't have pageerror, but console.error will be captured above
177
+ }
178
+ /**
179
+ * Process console message from Puppeteer
180
+ * @param msg - Console message from page or worker
181
+ * @param context - Where the message came from ('page' or 'worker')
182
+ * @param workerUrl - URL of the worker (if context is 'worker')
183
+ */
184
+ async processConsoleMessage(msg, context, workerUrl) {
185
+ const level = msg.type();
186
+ // Filter by configured levels
187
+ if (this.config.filterLevels.length > 0 &&
188
+ !this.config.filterLevels.includes(level)) {
189
+ return null;
190
+ }
191
+ try {
192
+ // Extract message text
193
+ const message = msg.text();
194
+ // Extract arguments (with truncation)
195
+ const args = await Promise.all(msg.args().map(async (arg) => {
196
+ try {
197
+ const jsonValue = await arg.jsonValue();
198
+ return this.truncateValue(jsonValue);
199
+ }
200
+ catch {
201
+ return String(arg);
202
+ }
203
+ }));
204
+ // Determine source based on context
205
+ let source;
206
+ if (context === 'worker') {
207
+ // Workers are where Figma plugins run
208
+ source = 'plugin';
209
+ }
210
+ else {
211
+ // Page console - determine if plugin or figma
212
+ const location = msg.location();
213
+ source = this.determineSource(location?.url);
214
+ }
215
+ const entry = {
216
+ timestamp: Date.now(),
217
+ level,
218
+ message: this.truncateString(message),
219
+ args,
220
+ source,
221
+ };
222
+ // Add worker URL metadata for debugging
223
+ if (workerUrl && context === 'worker') {
224
+ entry.workerUrl = workerUrl;
225
+ }
226
+ // Add stack trace for errors
227
+ if (level === 'error' && msg.stackTrace) {
228
+ entry.stackTrace = {
229
+ callFrames: msg.stackTrace().callFrames || [],
230
+ };
231
+ }
232
+ return entry;
233
+ }
234
+ catch (error) {
235
+ logger.error({ error, context, workerUrl }, 'Failed to extract console message details');
236
+ return null;
237
+ }
238
+ }
239
+ /**
240
+ * Determine if log is from plugin or Figma based on URL
241
+ */
242
+ determineSource(url) {
243
+ if (!url)
244
+ return 'unknown';
245
+ // Check for plugin-related URLs
246
+ // Plugins might run in:
247
+ // - iframes with plugin-specific URLs
248
+ // - blob: URLs created by the plugin
249
+ // - chrome-extension: URLs (for dev mode)
250
+ // - any URL containing "plugin"
251
+ if (url.includes('plugin') ||
252
+ url.includes('iframe') ||
253
+ url.startsWith('blob:') ||
254
+ url.startsWith('chrome-extension:') ||
255
+ url.includes('figma.com/plugin') ||
256
+ url.includes('/plugin-')) {
257
+ return 'plugin';
258
+ }
259
+ // Main Figma application
260
+ if (url.includes('figma.com')) {
261
+ return 'figma';
262
+ }
263
+ return 'unknown';
264
+ }
265
+ /**
266
+ * Add log to circular buffer
267
+ */
268
+ addLog(entry) {
269
+ this.logs.push(entry);
270
+ // Maintain buffer size
271
+ if (this.logs.length > this.config.bufferSize) {
272
+ this.logs.shift();
273
+ }
274
+ logger.debug({ level: entry.level, source: entry.source }, 'Log captured');
275
+ }
276
+ /**
277
+ * Truncate string to max length
278
+ */
279
+ truncateString(str) {
280
+ const maxLength = this.config.truncation.maxStringLength;
281
+ if (str.length <= maxLength) {
282
+ return str;
283
+ }
284
+ return str.substring(0, maxLength) + '... (truncated)';
285
+ }
286
+ /**
287
+ * Truncate value (string, array, object) intelligently
288
+ * Based on AgentDesk pattern to prevent context overflow
289
+ */
290
+ truncateValue(value, depth = 0) {
291
+ const { maxStringLength, maxArrayLength, maxObjectDepth } = this.config.truncation;
292
+ // Max depth reached
293
+ if (depth >= maxObjectDepth) {
294
+ return '[Max depth reached]';
295
+ }
296
+ // Handle null/undefined
297
+ if (value === null || value === undefined) {
298
+ return value;
299
+ }
300
+ // Handle strings
301
+ if (typeof value === 'string') {
302
+ return this.truncateString(value);
303
+ }
304
+ // Handle arrays
305
+ if (Array.isArray(value)) {
306
+ const truncated = value.slice(0, maxArrayLength).map((item) => this.truncateValue(item, depth + 1));
307
+ if (value.length > maxArrayLength) {
308
+ truncated.push(`... (${value.length - maxArrayLength} more items)`);
309
+ }
310
+ return truncated;
311
+ }
312
+ // Handle objects
313
+ if (typeof value === 'object') {
314
+ const result = {};
315
+ let count = 0;
316
+ for (const [key, val] of Object.entries(value)) {
317
+ if (count >= 10) {
318
+ // Limit object properties
319
+ result['...'] = '(more properties)';
320
+ break;
321
+ }
322
+ result[key] = this.truncateValue(val, depth + 1);
323
+ count++;
324
+ }
325
+ return result;
326
+ }
327
+ // Primitives (number, boolean, etc.)
328
+ return value;
329
+ }
330
+ /**
331
+ * Get logs with optional filtering
332
+ */
333
+ getLogs(options) {
334
+ let filtered = [...this.logs];
335
+ // Filter by timestamp
336
+ if (options?.since) {
337
+ filtered = filtered.filter((log) => log.timestamp >= options.since);
338
+ }
339
+ // Filter by level
340
+ if (options?.level && options.level !== 'all') {
341
+ filtered = filtered.filter((log) => log.level === options.level);
342
+ }
343
+ // Limit count (get most recent)
344
+ if (options?.count) {
345
+ filtered = filtered.slice(-options.count);
346
+ }
347
+ return filtered;
348
+ }
349
+ /**
350
+ * Clear log buffer
351
+ */
352
+ clear() {
353
+ const count = this.logs.length;
354
+ this.logs = [];
355
+ logger.info({ clearedCount: count }, 'Console buffer cleared');
356
+ return count;
357
+ }
358
+ /**
359
+ * Stop monitoring
360
+ */
361
+ stopMonitoring() {
362
+ if (!this.isMonitoring) {
363
+ return;
364
+ }
365
+ this.isMonitoring = false;
366
+ this.page = null;
367
+ logger.info('Console monitoring stopped');
368
+ }
369
+ /**
370
+ * Get monitoring status
371
+ */
372
+ getStatus() {
373
+ return {
374
+ isMonitoring: this.isMonitoring,
375
+ logCount: this.logs.length,
376
+ bufferSize: this.config.bufferSize,
377
+ workerCount: this.workers.size,
378
+ oldestTimestamp: this.logs[0]?.timestamp,
379
+ newestTimestamp: this.logs[this.logs.length - 1]?.timestamp,
380
+ };
381
+ }
382
+ }