UNPKG

visilog

Version:

Stream browser console logs to files for LLM debugging. Zero-config setup with simple imports. No MCP required - just tell your LLM to read the log files.

2 lines โ€ข 17.9 kB
(()=>{"use strict";var e={3:e=>{e.exports=require("path")},86:e=>{e.exports=require("ws")},211:function(e,s,n){var t,o=this&&this.__createBinding||(Object.create?function(e,s,n,t){void 0===t&&(t=n);var o=Object.getOwnPropertyDescriptor(s,n);o&&!("get"in o?!s.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return s[n]}}),Object.defineProperty(e,t,o)}:function(e,s,n,t){void 0===t&&(t=n),e[t]=s[n]}),i=this&&this.__setModuleDefault||(Object.create?function(e,s){Object.defineProperty(e,"default",{enumerable:!0,value:s})}:function(e,s){e.default=s}),r=this&&this.__importStar||(t=function(e){return t=Object.getOwnPropertyNames||function(e){var s=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(s[s.length]=n);return s},t(e)},function(e){if(e&&e.__esModule)return e;var s={};if(null!=e)for(var n=t(e),r=0;r<n.length;r++)"default"!==n[r]&&o(s,e,n[r]);return i(s,e),s});Object.defineProperty(s,"__esModule",{value:!0}),s.WebSocketLoggerServer=void 0;const l=r(n(383)),a=r(n(3)),c=n(86);s.WebSocketLoggerServer=class{constructor(e={}){this.wss=null,this.sessions=new Map,this.cleanupTimer=null,this.sessionHandlers=[],this.logHandlers=[],this.errorHandlers=[],this.config={port:3001,host:"0.0.0.0",maxSessions:50,cleanupThreshold:75,cleanupAmount:25,logsDir:a.join(process.cwd(),"logs"),enableIndex:!0,enableSessionLogs:!0,cleanupInterval:18e5},this.config={...this.config,...e},this.indexFile=a.join(this.config.logsDir,"index.json")}async start(){if(this.wss)throw new Error("Server is already running");try{this.ensureDirectories(),this.wss=new c.WebSocketServer({port:this.config.port,host:this.config.host}),this.setupWebSocketHandlers(),this.startCleanupTimer(),console.log(`๐Ÿ”Œ WebSocket logging server started on ${this.config.host}:${this.config.port}`),console.log(`๐Ÿ“ Logs directory: ${this.config.logsDir}`),this.config.enableIndex&&this.initializeIndex()}catch(e){throw this.handleError(e,"Failed to start server"),e}}async stop(){if(this.wss)return console.log("๐Ÿ›‘ Shutting down logging server..."),this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=null),this.sessions.forEach(((e,s)=>{this.handleSessionClose(s)})),new Promise(((e,s)=>{this.wss.close((n=>{n?(this.handleError(n,"Error stopping server"),s(n)):(this.wss=null,console.log("โœ… Logging server stopped"),e())}))}))}ensureDirectories(){if(l.existsSync(this.config.logsDir)||l.mkdirSync(this.config.logsDir,{recursive:!0}),this.config.enableSessionLogs){const e=a.join(this.config.logsDir,"sessions");l.existsSync(e)||l.mkdirSync(e,{recursive:!0})}}setupWebSocketHandlers(){this.wss&&(this.wss.on("connection",((e,s)=>{const n=this.generateSessionId(),t=this.config.enableSessionLogs?a.join(this.config.logsDir,"sessions",`session-${n}.log`):"",o={id:n,startTime:new Date,lastActivity:new Date,messageCount:0},i={...o,ws:e,logFile:t};this.sessions.set(n,i);const r=s.headers["user-agent"]||"unknown",l=s.socket.remoteAddress||"unknown",c=`[SESSION-START] ${n} - ${l} - ${r}`;this.config.enableIndex&&this.updateIndex(),this.config.enableSessionLogs&&t&&this.logToFile(t,c),console.log(`๐Ÿ“ฑ New session: ${n} (${this.sessions.size} active)`),this.sessionHandlers.forEach((e=>{try{e(o)}catch(e){this.handleError(e,"Session handler error")}})),this.sendMessage(e,{type:"session-init",sessionId:n}),e.on("message",(e=>{try{const s=JSON.parse(e.toString());this.handleClientMessage(n,s)}catch(e){this.handleError(e,"Failed to parse client message")}})),e.on("close",(()=>{this.handleSessionClose(n)})),e.on("error",(e=>{this.handleError(e,`WebSocket error for session ${n}`),this.handleSessionClose(n)})),this.checkAndCleanupOldSessions()})),this.wss.on("error",(e=>{this.handleError(e,"WebSocket server error")})))}handleClientMessage(e,s){const n=this.sessions.get(e);if(n)switch(s.type){case"log":s.log&&this.handleLogMessage(e,s.log);break;case"ping":this.sendMessage(n.ws,{type:"pong"});break;case"pong":n.lastActivity=new Date}}sendMessage(e,s){if(e.readyState===c.WebSocket.OPEN)try{e.send(JSON.stringify(s))}catch(e){this.handleError(e,"Failed to send message to client")}}generateSessionId(){return Math.random().toString(36).substring(2,10)}handleLogMessage(e,s){const n=this.sessions.get(e);if(!n)return;n.lastActivity=new Date,n.messageCount++;const t=(new Date).toISOString(),o=this.formatLogMessage(s,t);this.config.enableSessionLogs&&n.logFile&&this.logToFile(n.logFile,o),this.config.enableIndex&&this.updateIndex();const i=`[${e.slice(-6)}] ${s.namespace?`[${s.namespace}] `:""}${s.level.toUpperCase()}: ${s.message}`;switch(s.level){case"error":console.error(`๐Ÿ”ด ${i}`);break;case"warn":console.warn(`๐ŸŸก ${i}`);break;case"info":console.info(`โ„น๏ธ ${i}`);break;case"debug":console.debug(`๐Ÿ” ${i}`);break;default:console.log(`๐Ÿ“ ${i}`)}this.logHandlers.forEach((e=>{try{e(s)}catch(e){this.handleError(e,"Log handler error")}}))}formatLogMessage(e,s){const n={timestamp:s,level:e.level,message:e.message,sessionId:e.sessionId,url:e.url,...e.namespace&&{namespace:e.namespace},...e.data&&{data:e.data}};try{return JSON.stringify(n)}catch(n){const t={timestamp:s,level:e.level,message:e.message,sessionId:e.sessionId,url:e.url,error:"Failed to serialize log data",originalError:n instanceof Error?n.message:String(n)};return JSON.stringify(t)}}logToFile(e,s){try{const n=`${s}\n`;l.appendFileSync(e,n)}catch(s){this.handleError(s,`Failed to write to log file: ${e}`)}}initializeIndex(){if(!this.config.enableIndex)return;const e={lastUpdated:(new Date).toISOString(),totalSessions:0,activeSessions:0,sessions:[]};this.writeIndex(e)}writeIndex(e){try{l.writeFileSync(this.indexFile,JSON.stringify(e,null,2))}catch(e){this.handleError(e,`Failed to write index file: ${this.indexFile}`)}}readIndex(){try{if(l.existsSync(this.indexFile)){const e=l.readFileSync(this.indexFile,"utf8");return JSON.parse(e)}}catch(e){this.handleError(e,`Failed to read index file: ${this.indexFile}`)}return{lastUpdated:(new Date).toISOString(),totalSessions:0,activeSessions:0,sessions:[]}}updateIndex(){if(!this.config.enableIndex)return;const e=this.readIndex(),s=Array.from(this.sessions.values());e.activeSessions=s.length,e.lastUpdated=(new Date).toISOString(),s.forEach((s=>{const n=e.sessions.find((e=>e.id===s.id));if(n)n.messageCount=s.messageCount,n.status="active";else{const n={id:s.id,startTime:s.startTime.toISOString(),messageCount:s.messageCount,namespace:s.namespace,logFile:a.relative(this.config.logsDir,s.logFile),status:"active"};e.sessions.push(n),e.totalSessions=Math.max(e.totalSessions,e.sessions.length)}})),this.writeIndex(e)}updateSessionInIndex(e,s,n,t){if(!this.config.enableIndex)return;const o=this.readIndex(),i=o.sessions.find((s=>s.id===e));i&&(i.status=s,n&&(i.endTime=n.toISOString()),t&&(i.duration=t),o.activeSessions=Array.from(this.sessions.values()).length,o.lastUpdated=(new Date).toISOString(),this.writeIndex(o))}handleSessionClose(e){const s=this.sessions.get(e);if(!s)return;const n=Date.now()-s.startTime.getTime(),t=`[SESSION-END] ${e} - Duration: ${Math.round(n/1e3)}s - Messages: ${s.messageCount}`;this.config.enableIndex&&this.updateSessionInIndex(e,"completed",new Date,Math.round(n/1e3)),this.config.enableSessionLogs&&s.logFile&&this.logToFile(s.logFile,t),console.log(`๐Ÿ‘‹ Session ended: ${e} (${s.messageCount} messages, ${Math.round(n/1e3)}s)`),this.sessions.delete(e)}checkAndCleanupOldSessions(){if(!this.config.enableSessionLogs)return;const e=a.join(this.config.logsDir,"sessions");try{const s=l.readdirSync(e).filter((e=>e.startsWith("session-")&&e.endsWith(".log")));s.length>=this.config.cleanupThreshold&&(console.log(`๐Ÿงน Session cleanup triggered: ${s.length} files found (threshold: ${this.config.cleanupThreshold})`),this.cleanupOldSessionFiles(s))}catch(e){this.handleError(e,"Failed to check session files")}}cleanupOldSessionFiles(e){const s=a.join(this.config.logsDir,"sessions");try{const n=e.map((e=>{const n=a.join(s,e);return{file:e,path:n,mtime:l.statSync(n).mtime}})).sort(((e,s)=>e.mtime.getTime()-s.mtime.getTime())),t=n.slice(0,this.config.cleanupAmount),o=n.length-t.length;t.forEach((e=>{try{l.unlinkSync(e.path),console.log(`๐Ÿ—‘๏ธ Deleted old session log: ${e.file}`)}catch(s){this.handleError(s,`Failed to delete ${e.file}`)}})),console.log(`โœ… Cleanup complete: ${t.length} files deleted, ${o} remaining`)}catch(e){this.handleError(e,"Cleanup process failed")}}startCleanupTimer(){this.cleanupTimer=setInterval((()=>{console.log(`๐Ÿ” Periodic cleanup check: ${this.sessions.size} active sessions`),this.checkAndCleanupOldSessions()}),this.config.cleanupInterval)}handleError(e,s){console.error(`โŒ WebSocketLoggerServer Error${s?` (${s})`:""}:`,e),this.errorHandlers.forEach((n=>{try{const t=s?{operation:s,component:"WebSocketLoggerServer",timestamp:(new Date).toISOString()}:void 0;n(e,t)}catch(e){console.error("โŒ Error handler failed:",e)}}))}onSession(e){return this.sessionHandlers.push(e),()=>{const s=this.sessionHandlers.indexOf(e);s>-1&&this.sessionHandlers.splice(s,1)}}onLog(e){return this.logHandlers.push(e),()=>{const s=this.logHandlers.indexOf(e);s>-1&&this.logHandlers.splice(s,1)}}onError(e){return this.errorHandlers.push(e),()=>{const s=this.errorHandlers.indexOf(e);s>-1&&this.errorHandlers.splice(s,1)}}getStats(){return{activeSessions:this.sessions.size,logsDirectory:this.config.logsDir,indexFile:this.indexFile,maxSessions:this.config.maxSessions,cleanupThreshold:this.config.cleanupThreshold,config:this.config,isRunning:null!==this.wss}}getSessions(){return Array.from(this.sessions.values()).map((e=>({id:e.id,startTime:e.startTime,lastActivity:e.lastActivity,messageCount:e.messageCount,namespace:e.namespace})))}getSession(e){const s=this.sessions.get(e);return s?{id:s.id,startTime:s.startTime,lastActivity:s.lastActivity,messageCount:s.messageCount,namespace:s.namespace}:null}updateConfig(e){this.config={...this.config,...e},this.indexFile=a.join(this.config.logsDir,"index.json"),void 0!==e.cleanupInterval&&(this.cleanupTimer&&clearInterval(this.cleanupTimer),this.startCleanupTimer())}isRunning(){return null!==this.wss}setupGracefulShutdown(){const e=async()=>{console.log("\n๐Ÿ›‘ Received shutdown signal...");try{await this.stop(),process.exit(0)}catch(e){console.error("โŒ Error during shutdown:",e),process.exit(1)}};process.on("SIGINT",e),process.on("SIGTERM",e)}}},335:(e,s)=>{},383:e=>{e.exports=require("fs")},549:e=>{e.exports=require("html-webpack-plugin")}},s={};function n(t){var o=s[t];if(void 0!==o)return o.exports;var i=s[t]={exports:{}};return e[t].call(i.exports,i,i.exports,n),i.exports}var t={};(()=>{var e=t;Object.defineProperty(e,"__esModule",{value:!0}),e.WebSocketLoggerWebpackPlugin=void 0,e.createWebpackPlugin=function(e={}){return new o(e)};const s=n(211);class o{constructor(e={}){this.loggerServer=null,this.isDevelopment=!0,this.config={startServer:!0,injectClient:!0,development:!0,server:{port:3001,host:"0.0.0.0",logsDir:"logs",...e.server},client:{enableWebSocket:!0,enableConsole:!0,minLevel:0,websocketUrl:`ws://localhost:${e.server?.port||3001}`,maxRetries:5,retryInterval:2e3,autoConnect:!0,...e.client},...e}}apply(e){const n="WebSocketLoggerWebpackPlugin";e.hooks.environment.tap(n,(()=>{this.isDevelopment=this.config.development??"development"===e.options.mode})),e.hooks.beforeCompile.tapAsync(n,(async(e,n)=>{if(this.isDevelopment&&this.config.startServer&&!this.loggerServer)try{this.loggerServer=new s.WebSocketLoggerServer(this.config.server),await this.loggerServer.start(),console.log(`๐Ÿ”Œ Webpack WebSocket Logger Plugin: Server started on port ${this.config.server.port}`)}catch(e){console.error("โŒ Failed to start WebSocket logger server:",e)}n()})),this.config.injectClient&&(e.hooks.compilation.tap(n,(e=>{if(!this.isDevelopment)return;const s=this.getHtmlWebpackPlugin();if(s)try{const t=s.getHooks?s.getHooks(e):e.hooks.htmlWebpackPluginBeforeHtmlProcessing;t&&t.beforeEmit?t.beforeEmit.tapAsync(n,((e,s)=>{e.html=this.injectClientScript(e.html),s(null,e)})):e.hooks.processAssets?.tap({name:n,stage:e.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE},(e=>{Object.keys(e).forEach((s=>{if(s.endsWith(".html")){const n=e[s].source(),t=this.injectClientScript(n);e[s]={source:()=>t,size:()=>t.length}}}))}))}catch(e){console.warn("Failed to hook into HTML webpack plugin:",e)}})),e.hooks.normalModuleFactory.tap(n,(e=>{e.hooks.beforeResolve.tapAsync(n,((e,s)=>{"@websocket-logger/client"===e?.request&&(e.request=this.getClientModulePath()),s()}))}))),e.options.devServer&&e.hooks.afterEnvironment.tap(n,(()=>{const s=e.options.devServer;s.setupMiddlewares||(s.setupMiddlewares=[]),Array.isArray(s.setupMiddlewares)&&s.setupMiddlewares.push(((e,s)=>(s.app?.get("/__websocket-logger-client.js",((e,s)=>{s.setHeader("Content-Type","application/javascript"),s.send(this.generateClientScript())})),e)))})),e.hooks.done.tapAsync(n,(async(e,s)=>{this.loggerServer&&!this.isDevelopment&&(console.log("๐Ÿ”Œ Webpack WebSocket Logger Plugin: Shutting down logger server..."),await this.loggerServer.stop(),this.loggerServer=null),s()})),e.hooks.shutdown.tapAsync(n,(async e=>{this.loggerServer&&(await this.loggerServer.stop(),this.loggerServer=null),e()}))}injectClientScript(e){const s=`\n<script>\n ${this.generateClientScript()}\n<\/script>`;return e.includes("</head>")?e.replace("</head>",`${s}\n</head>`):e.includes("<body>")?e.replace("<body>",`<body>\n${s}`):s+e}generateClientScript(){return`\n(function() {\n // Check if WebSocket is available\n if (typeof WebSocket === 'undefined') {\n console.warn('WebSocket not available, skipping logger initialization');\n return;\n }\n\n // Simple WebSocket logger implementation\n class SimpleWebSocketLogger {\n constructor(config) {\n this.config = Object.assign({\n enableWebSocket: true,\n enableConsole: true,\n minLevel: 0,\n websocketUrl: 'ws://localhost:3001',\n maxRetries: 5,\n retryInterval: 2000\n }, config);\n \n this.ws = null;\n this.sessionId = null;\n this.messageQueue = [];\n this.reconnectAttempts = 0;\n this.originalConsole = {\n log: console.log.bind(console),\n info: console.info.bind(console),\n warn: console.warn.bind(console),\n error: console.error.bind(console),\n debug: console.debug.bind(console)\n };\n \n this.init();\n }\n \n init() {\n if (this.config.enableWebSocket) {\n this.connect();\n }\n this.overrideConsole();\n }\n \n connect() {\n try {\n this.ws = new WebSocket(this.config.websocketUrl);\n \n this.ws.onopen = () => {\n this.reconnectAttempts = 0;\n this.processQueue();\n };\n \n this.ws.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data);\n if (data.type === 'session-init') {\n this.sessionId = data.sessionId;\n }\n } catch {\n console.error('Failed to parse server message:', 'Parse error');\n }\n };\n \n this.ws.onclose = () => {\n this.scheduleReconnect();\n };\n \n this.ws.onerror = () => {\n this.scheduleReconnect();\n };\n } catch (connectError) {\n console.error('Failed to connect to logging server:', connectError);\n }\n }\n \n scheduleReconnect() {\n if (this.reconnectAttempts >= this.config.maxRetries) {\n this.config.enableWebSocket = false;\n return;\n }\n \n const delay = this.config.retryInterval * Math.pow(2, this.reconnectAttempts);\n setTimeout(() => {\n this.reconnectAttempts++;\n this.connect();\n }, delay);\n }\n \n processQueue() {\n while (this.messageQueue.length > 0) {\n const message = this.messageQueue.shift();\n this.sendMessage(message);\n }\n }\n \n sendMessage(logMessage) {\n if (!this.config.enableWebSocket) return;\n \n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n try {\n this.ws.send(JSON.stringify({ type: 'log', log: logMessage }));\n } catch (error) {\n this.messageQueue.push(logMessage);\n }\n } else {\n this.messageQueue.push(logMessage);\n if (this.messageQueue.length > 1000) {\n this.messageQueue.shift();\n }\n }\n }\n \n overrideConsole() {\n const levels = ['log', 'info', 'warn', 'error', 'debug'];\n \n levels.forEach(level => {\n console[level] = (...args) => {\n if (this.config.enableConsole) {\n this.originalConsole[level](...args);\n }\n \n const message = args.map(arg => \n typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)\n ).join(' ');\n \n const logMessage = {\n level: level,\n message: message,\n timestamp: new Date().toISOString(),\n sessionId: this.sessionId || 'unknown',\n url: window.location.href,\n userAgent: navigator.userAgent,\n data: args.filter(arg => typeof arg === 'object' && arg !== null)\n };\n \n this.sendMessage(logMessage);\n };\n });\n }\n }\n \n // Initialize logger\n const logger = new SimpleWebSocketLogger(${JSON.stringify(this.config.client)});\n \n // Make available globally\n window.__websocketLogger = logger;\n \n console.log('๐Ÿ”Œ WebSocket Logger initialized and ready');\n})();\n`}getClientModulePath(){return 335}getHtmlWebpackPlugin(){try{return n(549)}catch{return null}}}e.WebSocketLoggerWebpackPlugin=o,e.default=o})(),module.exports["visilog-webpack-plugin"]=t})(); //# sourceMappingURL=webpack-plugin.js.map