@@ -2,6 +2,22 @@ import { spawn } from "node:child_process"
22
33const fileWritingTools = [ "edit" , "write" , "apply_patch" , "patch" ]
44const commandTools = [ "bash" , "shell" ]
5+ const skillTool = "skill"
6+
7+ // skillDirFrom returns the folder of the skill that a skill tool call loaded:
8+ // the dir field of the result metadata, or else the base directory line of
9+ // the result text. Each OpenCode major gives the result in its own shape, so
10+ // each candidate is tried in turn.
11+ function skillDirFrom ( ...results : any [ ] ) : string {
12+ for ( const r of results ) {
13+ const dir = r ?. metadata ?. dir
14+ if ( typeof dir === "string" && dir ) return dir
15+ const text = typeof r === "string" ? r : typeof r ?. output === "string" ? r . output : ""
16+ const m = / ^ B a s e d i r e c t o r y f o r t h i s s k i l l : \s * ( .+ ) $ / m. exec ( text )
17+ if ( m ) return m [ 1 ] . trim ( )
18+ }
19+ return ""
20+ }
521
622function filePathsFromArgs ( args : any ) : string [ ] {
723 if ( args ?. filePath ) return [ args . filePath ]
@@ -117,12 +133,19 @@ async function sessionEvent(directory: string, type: string, sessionID: string,
117133 }
118134}
119135
120- async function toolEvent ( directory : string , hook : string , hookEventName : string , sessionID : string , tool : string , callID : string , args : any ) {
136+ async function toolEvent ( directory : string , hook : string , hookEventName : string , sessionID : string , tool : string , callID : string , args : any , skillDir = "" ) {
121137 const payload = { session_id : sessionID , hook_event_name : hookEventName , tool_name : tool }
138+ if ( tool === skillTool ) {
139+ // The skill is loaded after the call, and the result names its folder.
140+ // The hook copies the folder for the evidence.
141+ if ( skillDir ) await fire ( directory , hook , { ...payload , skill_dir : skillDir } )
142+ return
143+ }
122144 if ( commandTools . includes ( tool ) ) {
123145 // The call ID pairs this hook with the other hook of the same call, so
124- // overlapping commands keep their own snapshots.
125- await fire ( directory , hook , { ...payload , tool_use_id : callID } )
146+ // overlapping commands keep their own snapshots. The command line names
147+ // the checkouts that the command works in.
148+ await fire ( directory , hook , { ...payload , tool_use_id : callID , command : args ?. command ?? "" } )
126149 return
127150 }
128151 if ( ! fileWritingTools . includes ( tool ) ) return
@@ -172,8 +195,8 @@ async function server({ directory, client }: any) {
172195 "tool.execute.before" : async ( input : any , output : any ) => {
173196 await toolEvent ( directory , "pre-tool-use" , "tool.execute.before" , input . sessionID , input . tool , input . callID , output . args )
174197 } ,
175- "tool.execute.after" : async ( input : any ) => {
176- await toolEvent ( directory , "post-tool-use" , "tool.execute.after" , input . sessionID , input . tool , input . callID , input . args )
198+ "tool.execute.after" : async ( input : any , output : any ) => {
199+ await toolEvent ( directory , "post-tool-use" , "tool.execute.after" , input . sessionID , input . tool , input . callID , input . args , input . tool === skillTool ? skillDirFrom ( output ) : "" )
177200 } ,
178201 }
179202}
@@ -187,7 +210,7 @@ async function setup(ctx: any) {
187210 await toolEvent ( directory , "pre-tool-use" , "tool.execute.before" , event . sessionID , event . tool , event . id , event . input )
188211 } )
189212 await ctx . tool . hook ( "execute.after" , async ( event : any ) => {
190- await toolEvent ( directory , "post-tool-use" , "tool.execute.after" , event . sessionID , event . tool , event . id , event . input )
213+ await toolEvent ( directory , "post-tool-use" , "tool.execute.after" , event . sessionID , event . tool , event . id , event . input , event . tool === skillTool ? skillDirFrom ( event . output , event . result , event ) : "" )
191214 } )
192215
193216 // resume: false stores the message without asking the model for an answer.
0 commit comments