11use std:: io:: Write ;
22
3+ use clap:: {
4+ Parser ,
5+ Subcommand ,
6+ } ;
37use crossterm:: style:: Color ;
48use crossterm:: {
59 queue,
@@ -87,6 +91,57 @@ Profiles allow you to organize and manage different sets of context files for di
8791 }
8892}
8993
94+ #[ derive( Parser , Debug , Clone ) ]
95+ #[ command( name = "hooks" , disable_help_flag = true , disable_help_subcommand = true ) ]
96+ struct HooksCommand {
97+ #[ command( subcommand) ]
98+ command : HooksSubcommand ,
99+ }
100+
101+ #[ derive( Subcommand , Debug , Clone , Eq , PartialEq ) ]
102+ pub enum HooksSubcommand {
103+ Add {
104+ name : String ,
105+
106+ #[ arg( long, value_parser = [ "per_prompt" , "conversation_start" ] ) ]
107+ r#type : String ,
108+
109+ #[ arg( long, value_parser = clap:: value_parser!( String ) ) ]
110+ command : String ,
111+
112+ #[ arg( long) ]
113+ global : bool ,
114+ } ,
115+ #[ command( name = "rm" ) ]
116+ Remove {
117+ name : String ,
118+
119+ #[ arg( long) ]
120+ global : bool ,
121+ } ,
122+ Enable {
123+ name : String ,
124+
125+ #[ arg( long) ]
126+ global : bool ,
127+ } ,
128+ Disable {
129+ name : String ,
130+
131+ #[ arg( long) ]
132+ global : bool ,
133+ } ,
134+ EnableAll {
135+ #[ arg( long) ]
136+ global : bool ,
137+ } ,
138+ DisableAll {
139+ #[ arg( long) ]
140+ global : bool ,
141+ } ,
142+ Help ,
143+ }
144+
90145#[ derive( Debug , Clone , PartialEq , Eq ) ]
91146pub enum ContextSubcommand {
92147 Show {
@@ -104,6 +159,9 @@ pub enum ContextSubcommand {
104159 Clear {
105160 global : bool ,
106161 } ,
162+ Hooks {
163+ subcommand : Option < HooksSubcommand > ,
164+ } ,
107165 Help ,
108166}
109167
@@ -124,15 +182,43 @@ impl ContextSubcommand {
124182 <black!>--global: Remove specified rules globally</black!>
125183
126184 <em>clear [--global]</em> <black!>Remove all rules from current profile</black!>
127- <black!>--global: Remove global rules</black!>" } ;
185+ <black!>--global: Remove global rules</black!>
186+
187+ <em>hooks</em> <black!>View and manage context hooks</black!>" } ;
128188 const CLEAR_USAGE : & str = "/context clear [--global]" ;
189+ const HOOKS_AVAILABLE_COMMANDS : & str = color_print:: cstr! { "<cyan!>Available subcommands</cyan!>
190+ <em>hooks help</em> <black!>Show an explanation for context hooks commands</black!>
191+
192+ <em>hooks add [--global] <<name>></em> <black!>Add a new command context hook</black!>
193+ <black!>--global: Add to global hooks</black!>
194+ <em>--type <<type>></em> <black!>Type of hook, valid options: `per_prompt` or `conversation_start`</black!>
195+ <em>--command <<command>></em> <black!>Shell command to execute</black!>
196+
197+ <em>hooks rm [--global] <<name>></em> <black!>Remove an existing context hook</black!>
198+ <black!>--global: Remove from global hooks</black!>
199+
200+ <em>hooks enable [--global] <<name>></em> <black!>Enable an existing context hook</black!>
201+ <black!>--global: Enable in global hooks</black!>
202+
203+ <em>hooks disable [--global] <<name>></em> <black!>Disable an existing context hook</black!>
204+ <black!>--global: Disable in global hooks</black!>
205+
206+ <em>hooks enable-all [--global]</em> <black!>Enable all existing context hooks</black!>
207+ <black!>--global: Enable all in global hooks</black!>
208+
209+ <em>hooks disable-all [--global]</em> <black!>Disable all existing context hooks</black!>
210+ <black!>--global: Disable all in global hooks</black!>" } ;
129211 const REMOVE_USAGE : & str = "/context rm [--global] <path1> [path2...]" ;
130212 const SHOW_USAGE : & str = "/context show [--expand]" ;
131213
132214 fn usage_msg ( header : impl AsRef < str > ) -> String {
133215 format ! ( "{}\n \n {}" , header. as_ref( ) , Self :: AVAILABLE_COMMANDS )
134216 }
135217
218+ fn hooks_usage_msg ( header : impl AsRef < str > ) -> String {
219+ format ! ( "{}\n \n {}" , header. as_ref( ) , Self :: HOOKS_AVAILABLE_COMMANDS )
220+ }
221+
136222 pub fn help_text ( ) -> String {
137223 color_print:: cformat!(
138224 r#"
@@ -143,6 +229,9 @@ The files matched by these rules provide Amazon Q with additional information
143229about your project or environment. Adding relevant files helps Q generate
144230more accurate and helpful responses.
145231
232+ In addition to files, you can specify hooks that will run commands and return
233+ the output as context to Amazon Q.
234+
146235{}
147236
148237<cyan!>Notes</cyan!>
@@ -154,6 +243,32 @@ more accurate and helpful responses.
154243 Self :: AVAILABLE_COMMANDS
155244 )
156245 }
246+
247+ pub fn hooks_help_text ( ) -> String {
248+ color_print:: cformat!(
249+ r#"
250+ <magenta,em>(Beta) Context Hooks</magenta,em>
251+
252+ Use context hooks to specify shell commands to run. The output from these
253+ commands will be appended to the prompt to Amazon Q. Hooks can be defined
254+ in global or local profiles.
255+
256+ <cyan!>Usage: /context hooks [SUBCOMMAND]</cyan!>
257+
258+ <cyan!>Description</cyan!>
259+ Show existing global or profile-specific hooks.
260+ Alternatively, specify a subcommand to modify the hooks.
261+
262+ {}
263+
264+ <cyan!>Notes</cyan!>
265+ • Hooks are executed in parallel
266+ • 'conversation_start' hooks run on the first user prompt and are attached once to the conversation history sent to Amazon Q
267+ • 'per_prompt' hooks run on each user prompt and are attached to the prompt
268+ "# ,
269+ Self :: HOOKS_AVAILABLE_COMMANDS
270+ )
271+ }
157272}
158273
159274#[ derive( Debug , Clone , PartialEq , Eq ) ]
@@ -495,6 +610,18 @@ impl Command {
495610 "help" => Self :: Context {
496611 subcommand : ContextSubcommand :: Help ,
497612 } ,
613+ "hooks" => {
614+ if parts. get ( 2 ) . is_none ( ) {
615+ return Ok ( Self :: Context {
616+ subcommand : ContextSubcommand :: Hooks { subcommand : None } ,
617+ } ) ;
618+ } ;
619+
620+ match Self :: parse_hooks ( & parts) {
621+ Ok ( command) => command,
622+ Err ( err) => return Err ( ContextSubcommand :: hooks_usage_msg ( err) ) ,
623+ }
624+ } ,
498625 other => {
499626 return Err ( ContextSubcommand :: usage_msg ( format ! ( "Unknown subcommand '{}'." , other) ) ) ;
500627 } ,
@@ -579,6 +706,27 @@ impl Command {
579706 prompt : input. to_string ( ) ,
580707 } )
581708 }
709+
710+ // NOTE: Here we use clap to parse the hooks subcommand instead of parsing manually
711+ // like the rest of the file.
712+ // Since the hooks subcommand has a lot of options, this makes more sense.
713+ // Ideally, we parse everything with clap instead of trying to do it manually.
714+ fn parse_hooks ( parts : & [ & str ] ) -> Result < Self , String > {
715+ // Skip the first two parts ("/context" and "hooks")
716+ let args = match shlex:: split ( & parts[ 1 ..] . join ( " " ) ) {
717+ Some ( args) => args,
718+ None => return Err ( "Failed to parse arguments" . to_string ( ) ) ,
719+ } ;
720+
721+ // Parse with Clap
722+ HooksCommand :: try_parse_from ( args)
723+ . map ( |hooks_command| Self :: Context {
724+ subcommand : ContextSubcommand :: Hooks {
725+ subcommand : Some ( hooks_command. command ) ,
726+ } ,
727+ } )
728+ . map_err ( |e| e. to_string ( ) )
729+ }
582730}
583731
584732#[ cfg( test) ]
@@ -700,6 +848,66 @@ mod tests {
700848 ( "/issue \" there was an error in the chat\" " , Command :: Issue {
701849 prompt : Some ( "\" there was an error in the chat\" " . to_string ( ) ) ,
702850 } ) ,
851+ (
852+ "/context hooks" ,
853+ context ! ( ContextSubcommand :: Hooks { subcommand: None } ) ,
854+ ) ,
855+ (
856+ "/context hooks add test --type per_prompt --command 'echo 1' --global" ,
857+ context ! ( ContextSubcommand :: Hooks {
858+ subcommand: Some ( HooksSubcommand :: Add {
859+ name: "test" . to_string( ) ,
860+ global: true ,
861+ r#type: "per_prompt" . to_string( ) ,
862+ command: "echo 1" . to_string( )
863+ } )
864+ } ) ,
865+ ) ,
866+ (
867+ "/context hooks rm test --global" ,
868+ context ! ( ContextSubcommand :: Hooks {
869+ subcommand: Some ( HooksSubcommand :: Remove {
870+ name: "test" . to_string( ) ,
871+ global: true
872+ } )
873+ } ) ,
874+ ) ,
875+ (
876+ "/context hooks enable test --global" ,
877+ context ! ( ContextSubcommand :: Hooks {
878+ subcommand: Some ( HooksSubcommand :: Enable {
879+ name: "test" . to_string( ) ,
880+ global: true
881+ } )
882+ } ) ,
883+ ) ,
884+ (
885+ "/context hooks disable test" ,
886+ context ! ( ContextSubcommand :: Hooks {
887+ subcommand: Some ( HooksSubcommand :: Disable {
888+ name: "test" . to_string( ) ,
889+ global: false
890+ } )
891+ } ) ,
892+ ) ,
893+ (
894+ "/context hooks enable-all --global" ,
895+ context ! ( ContextSubcommand :: Hooks {
896+ subcommand: Some ( HooksSubcommand :: EnableAll { global: true } )
897+ } ) ,
898+ ) ,
899+ (
900+ "/context hooks disable-all" ,
901+ context ! ( ContextSubcommand :: Hooks {
902+ subcommand: Some ( HooksSubcommand :: DisableAll { global: false } )
903+ } ) ,
904+ ) ,
905+ (
906+ "/context hooks help" ,
907+ context ! ( ContextSubcommand :: Hooks {
908+ subcommand: Some ( HooksSubcommand :: Help )
909+ } ) ,
910+ ) ,
703911 ] ;
704912
705913 for ( input, parsed) in tests {
0 commit comments