@@ -16,12 +16,41 @@ export interface McpToolCaller {
1616}
1717
1818const STATE_MAX_AGE_MS = 24 * 60 * 60 * 1000 ;
19+ const DEFAULT_MAX_STDIN_BYTES = 1024 * 1024 ;
20+ const READ_CHUNK_BYTES = 64 * 1024 ;
21+ const TRANSPORT_ERROR_CODES = new Set ( [ "ECONNREFUSED" , "ECONNRESET" , "EPIPE" , "ENOENT" ] ) ;
1922
2023export function parseHookInput ( raw : string ) : HookInput {
2124 const normalized = raw . replace ( / ^ \uFEFF / , "" ) . trim ( ) ;
2225 return normalized ? JSON . parse ( normalized ) as HookInput : { } ;
2326}
2427
28+ export function readHookInput ( descriptor = 0 , maxBytes = DEFAULT_MAX_STDIN_BYTES ) : HookInput {
29+ const limit = Number . isSafeInteger ( maxBytes ) && maxBytes > 0 ? maxBytes : DEFAULT_MAX_STDIN_BYTES ;
30+ const chunks : Buffer [ ] = [ ] ;
31+ let totalBytes = 0 ;
32+
33+ while ( true ) {
34+ const chunk = Buffer . alloc ( Math . min ( READ_CHUNK_BYTES , limit - totalBytes + 1 ) ) ;
35+ const bytesRead = fs . readSync ( descriptor , chunk , 0 , chunk . length , null ) ;
36+ if ( bytesRead === 0 ) break ;
37+ totalBytes += bytesRead ;
38+ if ( totalBytes > limit ) throw Object . assign ( new Error ( "Hook input exceeds the configured limit." ) , { code : "INPUT_TOO_LARGE" } ) ;
39+ chunks . push ( chunk . subarray ( 0 , bytesRead ) ) ;
40+ }
41+
42+ return parseHookInput ( Buffer . concat ( chunks , totalBytes ) . toString ( "utf8" ) ) ;
43+ }
44+
45+ export function sanitizeError ( error : unknown ) : string {
46+ const code = errorCode ( error ) ;
47+ if ( code === "INPUT_TOO_LARGE" ) return "input-too-large" ;
48+ if ( code === - 32001 || code === "ETIMEDOUT" ) return "timeout" ;
49+ if ( code === - 32000 || ( typeof code === "string" && TRANSPORT_ERROR_CODES . has ( code ) ) ) return "transport-error" ;
50+ if ( error instanceof SyntaxError ) return "invalid-input" ;
51+ return "hook-error" ;
52+ }
53+
2554export function detectClient ( input : HookInput ) : string {
2655 const explicit = stringField ( input , "client" ) ;
2756 if ( explicit ) return explicit . toLowerCase ( ) ;
@@ -96,6 +125,13 @@ export function statePath(input: HookInput): string {
96125 detectClient ( input ) ,
97126 stringField ( input , "session_id" ) ?? stringField ( input , "sessionId" ) ?? "no-session" ,
98127 stringField ( input , "cwd" ) ?? process . cwd ( ) ,
128+ firstString ( input , [
129+ "request_id" , "requestId" ,
130+ "invocation_id" , "invocationId" ,
131+ "turn_id" , "turnId" ,
132+ "hook_id" , "hookId" ,
133+ "transcript_path" , "transcriptPath" ,
134+ ] ) ?? "no-hook-id" ,
99135 ] . join ( "|" ) ;
100136 const hash = createHash ( "sha256" ) . update ( key ) . digest ( "hex" ) ;
101137 return path . join ( os . tmpdir ( ) , "promptimprover-hooks" , `${ hash } .json` ) ;
@@ -152,18 +188,21 @@ export async function runPostExecution(input: HookInput, callTool: McpToolCaller
152188
153189 const client = state ?. client ?? detectClient ( input ) ;
154190 const outputLength = extractOutputLength ( input ) ;
155- await callTool ( "record_agent_output" , {
156- prompt_id : promptId ,
157- output_summary : `${ client } completed the tracked turn; output_length=${ outputLength } .` ,
158- artifacts_json : JSON . stringify ( {
159- client,
160- hook_event : stringField ( input , "hook_event_name" ) ?? "manual" ,
161- output_length : outputLength ,
162- } ) ,
163- status : stringField ( input , "status" ) === "failed" ? "failed" : "completed" ,
164- } ) ;
165- clearState ( input ) ;
166- return allowOutput ( input ) ;
191+ try {
192+ await callTool ( "record_agent_output" , {
193+ prompt_id : promptId ,
194+ output_summary : `${ client } completed the tracked turn; output_length=${ outputLength } .` ,
195+ artifacts_json : JSON . stringify ( {
196+ client,
197+ hook_event : stringField ( input , "hook_event_name" ) ?? "manual" ,
198+ output_length : outputLength ,
199+ } ) ,
200+ status : stringField ( input , "status" ) === "failed" ? "failed" : "completed" ,
201+ } ) ;
202+ return allowOutput ( input ) ;
203+ } finally {
204+ clearState ( input ) ;
205+ }
167206}
168207
169208function firstString ( input : HookInput , fields : string [ ] ) : string | undefined {
@@ -178,3 +217,9 @@ function stringField(input: HookInput, field: string): string | undefined {
178217 const value = input [ field ] ;
179218 return typeof value === "string" ? value : undefined ;
180219}
220+
221+ function errorCode ( error : unknown ) : unknown {
222+ return typeof error === "object" && error !== null && "code" in error
223+ ? ( error as { code ?: unknown } ) . code
224+ : undefined ;
225+ }
0 commit comments