@@ -51,21 +51,64 @@ const DEDUPE_TTL_MS = clampInt(
5151const MAX_BACKOFF_MS = 30_000 ;
5252const INBOX_PROTOCOL_VERSION = "2026-02-1" ;
5353const BROKER_HEALTH_PATH = path . join ( homedir ( ) , ".pi" , "agent" , "broker-health.json" ) ;
54+ const LOG_BUFFER_MAX_LINES = 1000 ;
55+
56+ const logLineBuffer = [ ] ;
5457
5558function ts ( ) {
5659 return new Date ( ) . toISOString ( ) ;
5760}
5861
62+ function formatLogArg ( arg ) {
63+ if ( typeof arg === "string" ) return arg ;
64+ if ( arg instanceof Error ) return arg . stack || arg . message ;
65+ try {
66+ return JSON . stringify ( arg ) ;
67+ } catch {
68+ return String ( arg ) ;
69+ }
70+ }
71+
72+ function pushLogLine ( line ) {
73+ const lines = String ( line ) . split ( / \r ? \n / ) ;
74+ for ( const rawLine of lines ) {
75+ const normalizedLine = rawLine . trimEnd ( ) ;
76+ if ( ! normalizedLine ) continue ;
77+ logLineBuffer . push ( normalizedLine ) ;
78+ }
79+
80+ const overflow = logLineBuffer . length - LOG_BUFFER_MAX_LINES ;
81+ if ( overflow > 0 ) {
82+ logLineBuffer . splice ( 0 , overflow ) ;
83+ }
84+ }
85+
86+ function logWithLevel ( level , ...args ) {
87+ const timestampPrefix = `[${ ts ( ) } ]` ;
88+ const line = [ timestampPrefix , ...args . map ( formatLogArg ) ] . join ( " " ) ;
89+ pushLogLine ( line ) ;
90+
91+ if ( level === "error" ) {
92+ console . error ( timestampPrefix , ...args ) ;
93+ return ;
94+ }
95+ if ( level === "warn" ) {
96+ console . warn ( timestampPrefix , ...args ) ;
97+ return ;
98+ }
99+ console . log ( timestampPrefix , ...args ) ;
100+ }
101+
59102function logInfo ( ...args ) {
60- console . log ( `[ ${ ts ( ) } ]` , ...args ) ;
103+ logWithLevel ( "info" , ...args ) ;
61104}
62105
63106function logError ( ...args ) {
64- console . error ( `[ ${ ts ( ) } ]` , ...args ) ;
107+ logWithLevel ( "error" , ...args ) ;
65108}
66109
67110function logWarn ( ...args ) {
68- console . warn ( `[ ${ ts ( ) } ]` , ...args ) ;
111+ logWithLevel ( "warn" , ...args ) ;
69112}
70113
71114for ( const key of [
@@ -669,13 +712,37 @@ async function processPulledMessage(message) {
669712 return true ;
670713}
671714
715+ function getLogLinesForResponse ( url ) {
716+ const nParam = url . searchParams . get ( "n" ) ;
717+ const filterParam = url . searchParams . get ( "filter" ) ;
718+
719+ let requestedLineCount = null ;
720+ if ( nParam !== null ) {
721+ const parsedN = Number . parseInt ( nParam , 10 ) ;
722+ if ( ! Number . isFinite ( parsedN ) || parsedN < 1 ) {
723+ throw new Error ( "n must be a positive integer" ) ;
724+ }
725+ requestedLineCount = Math . min ( parsedN , LOG_BUFFER_MAX_LINES ) ;
726+ }
727+
728+ let lines = logLineBuffer ;
729+
730+ const normalizedFilter = filterParam ?. trim ( ) . toLowerCase ( ) ;
731+ if ( normalizedFilter ) {
732+ lines = lines . filter ( ( line ) => line . toLowerCase ( ) . includes ( normalizedFilter ) ) ;
733+ }
734+
735+ if ( requestedLineCount !== null ) {
736+ lines = lines . slice ( - requestedLineCount ) ;
737+ }
738+
739+ return lines ;
740+ }
741+
672742function startApiServer ( ) {
673743 const server = createServer ( async ( req , res ) => {
674- if ( req . method !== "POST" ) {
675- res . writeHead ( 405 , { "Content-Type" : "application/json" } ) ;
676- res . end ( JSON . stringify ( { error : "Method not allowed" } ) ) ;
677- return ;
678- }
744+ const url = new URL ( req . url , `http://localhost:${ API_PORT } ` ) ;
745+ const pathname = url . pathname ;
679746
680747 const remoteAddr = req . socket . remoteAddress ;
681748 if ( remoteAddr !== "127.0.0.1" && remoteAddr !== "::1" && remoteAddr !== "::ffff:127.0.0.1" ) {
@@ -690,6 +757,31 @@ function startApiServer() {
690757 return ;
691758 }
692759
760+ if ( pathname === "/logs" ) {
761+ if ( req . method !== "GET" ) {
762+ res . writeHead ( 405 , { "Content-Type" : "application/json" } ) ;
763+ res . end ( JSON . stringify ( { error : "Method not allowed" } ) ) ;
764+ return ;
765+ }
766+
767+ try {
768+ const lines = getLogLinesForResponse ( url ) ;
769+ res . writeHead ( 200 , { "Content-Type" : "text/plain; charset=utf-8" } ) ;
770+ res . end ( lines . length > 0 ? `${ lines . join ( "\n" ) } \n` : "" ) ;
771+ return ;
772+ } catch ( err ) {
773+ res . writeHead ( 400 , { "Content-Type" : "application/json" } ) ;
774+ res . end ( JSON . stringify ( { error : err instanceof Error ? err . message : "invalid query params" } ) ) ;
775+ return ;
776+ }
777+ }
778+
779+ if ( req . method !== "POST" ) {
780+ res . writeHead ( 405 , { "Content-Type" : "application/json" } ) ;
781+ res . end ( JSON . stringify ( { error : "Method not allowed" } ) ) ;
782+ return ;
783+ }
784+
693785 let rawApiRequestBody = "" ;
694786 for await ( const chunk of req ) rawApiRequestBody += chunk ;
695787
@@ -703,9 +795,6 @@ function startApiServer() {
703795 }
704796
705797 try {
706- const url = new URL ( req . url , `http://localhost:${ API_PORT } ` ) ;
707- const pathname = url . pathname ;
708-
709798 if ( pathname === "/send" ) {
710799 const validationError = validateSendParams ( apiRequestBody ) ;
711800 if ( validationError ) {
@@ -715,7 +804,7 @@ function startApiServer() {
715804 }
716805
717806 const { channel, text, thread_ts } = apiRequestBody ;
718-
807+
719808 const result = await sendViaBroker ( {
720809 action : "chat.postMessage" ,
721810 routing : { channel, ...( thread_ts ? { thread_ts } : { } ) } ,
@@ -767,7 +856,7 @@ function startApiServer() {
767856 }
768857
769858 const { channel, timestamp, emoji } = apiRequestBody ;
770-
859+
771860 await sendViaBroker ( {
772861 action : "reactions.add" ,
773862 routing : { channel, timestamp, emoji } ,
@@ -780,7 +869,7 @@ function startApiServer() {
780869 }
781870
782871 res . writeHead ( 404 , { "Content-Type" : "application/json" } ) ;
783- res . end ( JSON . stringify ( { error : "Not found. Endpoints: POST /send, POST /reply, POST /react" } ) ) ;
872+ res . end ( JSON . stringify ( { error : "Not found. Endpoints: POST /send, POST /reply, POST /react, GET /logs " } ) ) ;
784873 } catch ( err ) {
785874 res . writeHead ( 500 , { "Content-Type" : "application/json" } ) ;
786875 res . end ( JSON . stringify ( { error : err instanceof Error ? err . message : "unknown error" } ) ) ;
0 commit comments