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 โข 12.1 kB
JavaScript
(()=>{"use strict";var e={3:e=>{e.exports=require("path")},86:e=>{e.exports=require("ws")},211:function(e,s,t){var n,i=this&&this.__createBinding||(Object.create?function(e,s,t,n){void 0===n&&(n=t);var i=Object.getOwnPropertyDescriptor(s,t);i&&!("get"in i?!s.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return s[t]}}),Object.defineProperty(e,n,i)}:function(e,s,t,n){void 0===n&&(n=t),e[n]=s[t]}),o=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||(n=function(e){return n=Object.getOwnPropertyNames||function(e){var s=[];for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(s[s.length]=t);return s},n(e)},function(e){if(e&&e.__esModule)return e;var s={};if(null!=e)for(var t=n(e),r=0;r<t.length;r++)"default"!==t[r]&&i(s,e,t[r]);return o(s,e),s});Object.defineProperty(s,"__esModule",{value:!0}),s.WebSocketLoggerServer=void 0;const a=r(t(383)),l=r(t(3)),c=t(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:l.join(process.cwd(),"logs"),enableIndex:!0,enableSessionLogs:!0,cleanupInterval:18e5},this.config={...this.config,...e},this.indexFile=l.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((t=>{t?(this.handleError(t,"Error stopping server"),s(t)):(this.wss=null,console.log("โ
Logging server stopped"),e())}))}))}ensureDirectories(){if(a.existsSync(this.config.logsDir)||a.mkdirSync(this.config.logsDir,{recursive:!0}),this.config.enableSessionLogs){const e=l.join(this.config.logsDir,"sessions");a.existsSync(e)||a.mkdirSync(e,{recursive:!0})}}setupWebSocketHandlers(){this.wss&&(this.wss.on("connection",((e,s)=>{const t=this.generateSessionId(),n=this.config.enableSessionLogs?l.join(this.config.logsDir,"sessions",`session-${t}.log`):"",i={id:t,startTime:new Date,lastActivity:new Date,messageCount:0},o={...i,ws:e,logFile:n};this.sessions.set(t,o);const r=s.headers["user-agent"]||"unknown",a=s.socket.remoteAddress||"unknown",c=`[SESSION-START] ${t} - ${a} - ${r}`;this.config.enableIndex&&this.updateIndex(),this.config.enableSessionLogs&&n&&this.logToFile(n,c),console.log(`๐ฑ New session: ${t} (${this.sessions.size} active)`),this.sessionHandlers.forEach((e=>{try{e(i)}catch(e){this.handleError(e,"Session handler error")}})),this.sendMessage(e,{type:"session-init",sessionId:t}),e.on("message",(e=>{try{const s=JSON.parse(e.toString());this.handleClientMessage(t,s)}catch(e){this.handleError(e,"Failed to parse client message")}})),e.on("close",(()=>{this.handleSessionClose(t)})),e.on("error",(e=>{this.handleError(e,`WebSocket error for session ${t}`),this.handleSessionClose(t)})),this.checkAndCleanupOldSessions()})),this.wss.on("error",(e=>{this.handleError(e,"WebSocket server error")})))}handleClientMessage(e,s){const t=this.sessions.get(e);if(t)switch(s.type){case"log":s.log&&this.handleLogMessage(e,s.log);break;case"ping":this.sendMessage(t.ws,{type:"pong"});break;case"pong":t.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 t=this.sessions.get(e);if(!t)return;t.lastActivity=new Date,t.messageCount++;const n=(new Date).toISOString(),i=this.formatLogMessage(s,n);this.config.enableSessionLogs&&t.logFile&&this.logToFile(t.logFile,i),this.config.enableIndex&&this.updateIndex();const o=`[${e.slice(-6)}] ${s.namespace?`[${s.namespace}] `:""}${s.level.toUpperCase()}: ${s.message}`;switch(s.level){case"error":console.error(`๐ด ${o}`);break;case"warn":console.warn(`๐ก ${o}`);break;case"info":console.info(`โน๏ธ ${o}`);break;case"debug":console.debug(`๐ ${o}`);break;default:console.log(`๐ ${o}`)}this.logHandlers.forEach((e=>{try{e(s)}catch(e){this.handleError(e,"Log handler error")}}))}formatLogMessage(e,s){const t={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(t)}catch(t){const n={timestamp:s,level:e.level,message:e.message,sessionId:e.sessionId,url:e.url,error:"Failed to serialize log data",originalError:t instanceof Error?t.message:String(t)};return JSON.stringify(n)}}logToFile(e,s){try{const t=`${s}\n`;a.appendFileSync(e,t)}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{a.writeFileSync(this.indexFile,JSON.stringify(e,null,2))}catch(e){this.handleError(e,`Failed to write index file: ${this.indexFile}`)}}readIndex(){try{if(a.existsSync(this.indexFile)){const e=a.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 t=e.sessions.find((e=>e.id===s.id));if(t)t.messageCount=s.messageCount,t.status="active";else{const t={id:s.id,startTime:s.startTime.toISOString(),messageCount:s.messageCount,namespace:s.namespace,logFile:l.relative(this.config.logsDir,s.logFile),status:"active"};e.sessions.push(t),e.totalSessions=Math.max(e.totalSessions,e.sessions.length)}})),this.writeIndex(e)}updateSessionInIndex(e,s,t,n){if(!this.config.enableIndex)return;const i=this.readIndex(),o=i.sessions.find((s=>s.id===e));o&&(o.status=s,t&&(o.endTime=t.toISOString()),n&&(o.duration=n),i.activeSessions=Array.from(this.sessions.values()).length,i.lastUpdated=(new Date).toISOString(),this.writeIndex(i))}handleSessionClose(e){const s=this.sessions.get(e);if(!s)return;const t=Date.now()-s.startTime.getTime(),n=`[SESSION-END] ${e} - Duration: ${Math.round(t/1e3)}s - Messages: ${s.messageCount}`;this.config.enableIndex&&this.updateSessionInIndex(e,"completed",new Date,Math.round(t/1e3)),this.config.enableSessionLogs&&s.logFile&&this.logToFile(s.logFile,n),console.log(`๐ Session ended: ${e} (${s.messageCount} messages, ${Math.round(t/1e3)}s)`),this.sessions.delete(e)}checkAndCleanupOldSessions(){if(!this.config.enableSessionLogs)return;const e=l.join(this.config.logsDir,"sessions");try{const s=a.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=l.join(this.config.logsDir,"sessions");try{const t=e.map((e=>{const t=l.join(s,e);return{file:e,path:t,mtime:a.statSync(t).mtime}})).sort(((e,s)=>e.mtime.getTime()-s.mtime.getTime())),n=t.slice(0,this.config.cleanupAmount),i=t.length-n.length;n.forEach((e=>{try{a.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: ${n.length} files deleted, ${i} 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((t=>{try{const n=s?{operation:s,component:"WebSocketLoggerServer",timestamp:(new Date).toISOString()}:void 0;t(e,n)}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=l.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)}}},383:e=>{e.exports=require("fs")}},s={};function t(n){var i=s[n];if(void 0!==i)return i.exports;var o=s[n]={exports:{}};return e[n].call(o.exports,o,o.exports,t),o.exports}var n={};(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.fastify=e.koa=e.express=void 0,e.createDevMiddleware=i;const s=t(211);function i(e={}){const{port:t=3001,host:n="0.0.0.0",injectScript:i=!0,logsDir:o="logs",enabled:r=!1}=e;let a=null;return r&&(a=new s.WebSocketLoggerServer({port:t,host:n,logsDir:o}),a.start().then((()=>{console.log(`๐ VisiLog middleware: WebSocket server started on ${n}:${t}`)})).catch((e=>{console.error("โ VisiLog middleware: Failed to start server:",e)})),process.on("SIGTERM",(()=>a?.stop())),process.on("SIGINT",(()=>a?.stop()))),(e,s,n)=>{if(!r||!i)return n();const o=s.send;s.send=function(e){if("string"==typeof e&&e.includes("<html")){const s=`\n<script>\n // VisiLog auto-injected client\n (function() {\n if (typeof window === 'undefined') return;\n \n const script = document.createElement('script');\n script.src = 'https://unpkg.com/visilog/dist/browser.js';\n script.onload = function() {\n if (window.VisiLog && window.VisiLog.isDevEnvironment()) {\n window.VisiLog.connect('ws://' + window.location.hostname + ':${t}');\n }\n };\n document.head.appendChild(script);\n })();\n<\/script>`;e.includes("</head>")?e=e.replace("</head>",s+"\n</head>"):e.includes("<head>")&&(e=e.replace("<head>","<head>\n"+s))}return o.call(this,e)},n()}}e.express=i,e.koa=(e={})=>{const s=i(e);return(e,t)=>new Promise(((n,i)=>{s(e.request,e.response,(e=>{e?i(e):n(t())}))}))},e.fastify=(e={})=>{const s=i(e);return(e,t,n)=>{s(e,t,n)}}})(),module.exports["visilog-middleware"]=n})();
//# sourceMappingURL=middleware.js.map