11import { pathToFileURL } from "node:url"
2- import { normalizeSpokenWord , validateCaptureDescriptor } from "./captures.js"
2+ import { boundedString , record , utf8Length , validateCaptureDescriptor } from "./captures.js"
33import { Cause , Deferred , Effect , Exit , Fiber } from "effect"
44import { Hex , ToolCallError } from "./effect.js"
55import type { HexService } from "./effect.js"
@@ -13,6 +13,7 @@ import {
1313} from "./model.js"
1414import type {
1515 HandlerArguments ,
16+ HandlerContext ,
1617 CaptureDescriptor ,
1718 CaptureSchema ,
1819 HexCapabilities ,
@@ -54,20 +55,6 @@ type HostHandlerFunction = (arguments_: HandlerArguments<UntypedCaptures>) =>
5455 void | Promise < void > | Effect . Effect < void , unknown , Hex >
5556type HostHandler = HostHandlerFunction | Effect . Effect < void , unknown , Hex >
5657
57- const utf8Length = ( value : string ) : number => Buffer . byteLength ( value , "utf8" )
58-
59- const boundedString = ( value : unknown , label : string , maxBytes : number ) : string => {
60- if ( typeof value !== "string" || value . length === 0 || utf8Length ( value ) > maxBytes ) {
61- throw new Error ( `${ label } must be a non-empty string no longer than ${ maxBytes } UTF-8 bytes` )
62- }
63- return value
64- }
65-
66- const record = ( value : unknown ) : Record < string , unknown > | undefined =>
67- typeof value === "object" && value !== null && ! Array . isArray ( value )
68- ? Object . fromEntries ( Object . entries ( value ) )
69- : undefined
70-
7158const isFunction = ( value : unknown ) : value is HostHandlerFunction =>
7259 typeof value === "function"
7360
@@ -356,6 +343,33 @@ const errorMessage = (error: unknown): string => {
356343 : Buffer . from ( message ) . subarray ( 0 , MAX_ERROR_BYTES ) . toString ( "utf8" )
357344}
358345
346+ const decodeContext = (
347+ value : unknown ,
348+ label : "invoke.context" | "transform.context" ,
349+ deriveBrowserHost : boolean ,
350+ ) : HandlerContext => {
351+ const context = record ( value )
352+ if ( context === undefined ) throw new Error ( `${ label } must be an object` )
353+ const application = context . application === undefined
354+ ? undefined
355+ : boundedString ( context . application , `${ label } .application` , MAX_VALUE_BYTES )
356+ const browserUrl = context . browserUrl === undefined
357+ ? undefined
358+ : boundedString ( context . browserUrl , `${ label } .browserUrl` , MAX_VALUE_BYTES )
359+ const windowTitle = context . windowTitle === undefined
360+ ? undefined
361+ : boundedString ( context . windowTitle , `${ label } .windowTitle` , MAX_VALUE_BYTES )
362+ const browserHost = context . browserHost === undefined
363+ ? deriveBrowserHost && browserUrl !== undefined ? new URL ( browserUrl ) . hostname : undefined
364+ : boundedString ( context . browserHost , `${ label } .browserHost` , 253 )
365+ return {
366+ ...( application === undefined ? { } : { application } ) ,
367+ ...( browserHost === undefined ? { } : { browserHost } ) ,
368+ ...( browserUrl === undefined ? { } : { browserUrl } ) ,
369+ ...( windowTitle === undefined ? { } : { windowTitle } ) ,
370+ }
371+ }
372+
359373const decodeInput = ( line : string ) : HostInput => {
360374 let value : unknown
361375 try {
@@ -365,21 +379,8 @@ const decodeInput = (line: string): HostInput => {
365379 }
366380 const input = record ( value )
367381 switch ( input ?. type ) {
368- case "invoke" :
369- const context = record ( input . context )
370- if ( context === undefined ) throw new Error ( "invoke.context must be an object" )
371- const application = context . application === undefined
372- ? undefined
373- : boundedString ( context . application , "invoke.context.application" , MAX_VALUE_BYTES )
374- const browserUrl = context . browserUrl === undefined
375- ? undefined
376- : boundedString ( context . browserUrl , "invoke.context.browserUrl" , MAX_VALUE_BYTES )
377- const windowTitle = context . windowTitle === undefined
378- ? undefined
379- : boundedString ( context . windowTitle , "invoke.context.windowTitle" , MAX_VALUE_BYTES )
380- const browserHost = context . browserHost === undefined
381- ? browserUrl === undefined ? undefined : new URL ( browserUrl ) . hostname
382- : boundedString ( context . browserHost , "invoke.context.browserHost" , 253 )
382+ case "invoke" : {
383+ const context = decodeContext ( input . context , "invoke.context" , true )
383384 const rawCaptures = input . captures === undefined ? { } : record ( input . captures )
384385 if ( rawCaptures === undefined ) throw new Error ( "invoke.captures must be an object" )
385386 const captureEntries = Object . entries ( rawCaptures )
@@ -397,17 +398,12 @@ const decodeInput = (line: string): HostInput => {
397398 type : "invoke" ,
398399 invocationId : boundedString ( input . invocationId , "invoke.invocationId" , MAX_ID_BYTES ) ,
399400 commandId : boundedString ( input . commandId , "invoke.commandId" , MAX_ID_BYTES ) ,
400- context : {
401- ...( application === undefined ? { } : { application } ) ,
402- ...( browserHost === undefined ? { } : { browserHost } ) ,
403- ...( browserUrl === undefined ? { } : { browserUrl } ) ,
404- ...( windowTitle === undefined ? { } : { windowTitle } ) ,
405- } ,
401+ context,
406402 captures,
407403 }
404+ }
408405 case "transform" : {
409- const context = record ( input . context )
410- if ( context === undefined ) throw new Error ( "transform.context must be an object" )
406+ const context = decodeContext ( input . context , "transform.context" , false )
411407 if ( ! Array . isArray ( input . transformationIds )
412408 || input . transformationIds . length === 0
413409 || input . transformationIds . length > MAX_TRANSFORMATIONS_PER_INVOCATION ) {
@@ -424,20 +420,7 @@ const decodeInput = (line: string): HostInput => {
424420 transformationIds : input . transformationIds . map ( ( id , index ) =>
425421 boundedString ( id , `transform.transformationIds[${ index } ]` , MAX_ID_BYTES ) ) ,
426422 text,
427- context : {
428- ...( context . application === undefined ? { } : {
429- application : boundedString ( context . application , "transform.context.application" , MAX_VALUE_BYTES ) ,
430- } ) ,
431- ...( context . browserHost === undefined ? { } : {
432- browserHost : boundedString ( context . browserHost , "transform.context.browserHost" , 253 ) ,
433- } ) ,
434- ...( context . browserUrl === undefined ? { } : {
435- browserUrl : boundedString ( context . browserUrl , "transform.context.browserUrl" , MAX_VALUE_BYTES ) ,
436- } ) ,
437- ...( context . windowTitle === undefined ? { } : {
438- windowTitle : boundedString ( context . windowTitle , "transform.context.windowTitle" , MAX_VALUE_BYTES ) ,
439- } ) ,
440- } ,
423+ context,
441424 }
442425 }
443426 case "toolResult" : {
0 commit comments