22///
33/// ```toml
44/// [skills]
5- /// backend = "native" # "native" | "agent-skills" | "akm"
6- /// runner = "skills" # path to agent-skills-cli binary (agent-skills backend only)
7- /// timeout_secs = 120 # subprocess timeout in seconds (agent-skills backend only)
5+ /// backend = "native" # "native" | "agent-skills" | "akm"
6+ /// runner = "skills" # binary name or path (agent-skills backend only)
7+ /// runner = ["bunx", "agent-skills-cli"] # or an array: program + args prefix
8+ /// timeout_secs = 120 # subprocess timeout in seconds (agent-skills backend only)
89/// ```
910///
1011/// When `ai/config.toml` is absent, all defaults apply (native backend).
@@ -35,9 +36,19 @@ impl BackendKind {
3536#[ derive( Debug , Clone ) ]
3637pub struct AiConfig {
3738 pub backend : BackendKind ,
38- /// Path or name of the agent-skills-cli runner binary (default: `"skills"`).
39+ /// Runner command for the agent-skills-cli subprocess (default: `["skills"]`).
40+ ///
41+ /// The first element is the program, the rest are arguments prepended before the
42+ /// subcommand. This lets users invoke via a package runner without a global install:
43+ ///
44+ /// ```toml
45+ /// runner = ["bunx", "agent-skills-cli"]
46+ /// runner = ["npx", "agent-skills-cli"]
47+ /// runner = "/usr/local/bin/skills" # string shorthand for single-element array
48+ /// ```
49+ ///
3950 /// Only used by `AgentSkills` backend (and future `Akm`).
40- pub runner : String ,
51+ pub runner : Vec < String > ,
4152 /// Subprocess timeout in seconds (default: 120).
4253 /// Only used by `AgentSkills` backend (and future `Akm`).
4354 pub timeout_secs : u64 ,
@@ -47,7 +58,7 @@ impl Default for AiConfig {
4758 fn default ( ) -> Self {
4859 AiConfig {
4960 backend : BackendKind :: Native ,
50- runner : "skills" . to_string ( ) ,
61+ runner : vec ! [ "skills" . to_string( ) ] ,
5162 timeout_secs : 120 ,
5263 }
5364 }
@@ -77,10 +88,27 @@ struct RawAiConfig {
7788 skills : RawSkillsSection ,
7889}
7990
91+ /// Accepts either `runner = "skills"` or `runner = ["bunx", "agent-skills-cli"]`.
92+ #[ derive( Deserialize ) ]
93+ #[ serde( untagged) ]
94+ enum RunnerValue {
95+ Single ( String ) ,
96+ Multi ( Vec < String > ) ,
97+ }
98+
99+ impl From < RunnerValue > for Vec < String > {
100+ fn from ( v : RunnerValue ) -> Self {
101+ match v {
102+ RunnerValue :: Single ( s) => vec ! [ s] ,
103+ RunnerValue :: Multi ( v) => v,
104+ }
105+ }
106+ }
107+
80108#[ derive( Deserialize , Default ) ]
81109struct RawSkillsSection {
82110 backend : Option < String > ,
83- runner : Option < String > ,
111+ runner : Option < RunnerValue > ,
84112 timeout_secs : Option < u64 > ,
85113}
86114
@@ -97,9 +125,16 @@ impl RawAiConfig {
97125 ) ,
98126 } ;
99127
128+ let runner = self . skills . runner
129+ . map ( Vec :: from)
130+ . unwrap_or_else ( || vec ! [ "skills" . to_string( ) ] ) ;
131+ if runner. is_empty ( ) {
132+ anyhow:: bail!( "{}: 'runner' must not be an empty array" , path_display) ;
133+ }
134+
100135 Ok ( AiConfig {
101136 backend,
102- runner : self . skills . runner . unwrap_or_else ( || "skills" . to_string ( ) ) ,
137+ runner,
103138 timeout_secs : self . skills . timeout_secs . unwrap_or ( 120 ) ,
104139 } )
105140 }
@@ -138,22 +173,42 @@ mod tests {
138173 write_config ( & dir, "[skills]\n backend = \" agent-skills\" \n " ) ;
139174 let cfg = AiConfig :: load ( dir. path ( ) ) . unwrap ( ) ;
140175 assert_eq ! ( cfg. backend, BackendKind :: AgentSkills ) ;
141- assert_eq ! ( cfg. runner, "skills" ) ;
176+ assert_eq ! ( cfg. runner, vec! [ "skills" ] ) ;
142177 assert_eq ! ( cfg. timeout_secs, 120 ) ;
143178 }
144179
145180 #[ test]
146- fn ai_config_reads_custom_runner_and_timeout ( ) {
181+ fn ai_config_reads_string_runner ( ) {
147182 let dir = TempDir :: new ( ) . unwrap ( ) ;
148183 write_config (
149184 & dir,
150185 "[skills]\n backend = \" agent-skills\" \n runner = \" /usr/local/bin/skills\" \n timeout_secs = 60\n " ,
151186 ) ;
152187 let cfg = AiConfig :: load ( dir. path ( ) ) . unwrap ( ) ;
153- assert_eq ! ( cfg. runner, "/usr/local/bin/skills" ) ;
188+ assert_eq ! ( cfg. runner, vec! [ "/usr/local/bin/skills" ] ) ;
154189 assert_eq ! ( cfg. timeout_secs, 60 ) ;
155190 }
156191
192+ #[ test]
193+ fn ai_config_reads_array_runner ( ) {
194+ let dir = TempDir :: new ( ) . unwrap ( ) ;
195+ write_config (
196+ & dir,
197+ "[skills]\n backend = \" agent-skills\" \n runner = [\" bunx\" , \" agent-skills-cli\" ]\n " ,
198+ ) ;
199+ let cfg = AiConfig :: load ( dir. path ( ) ) . unwrap ( ) ;
200+ assert_eq ! ( cfg. runner, vec![ "bunx" , "agent-skills-cli" ] ) ;
201+ }
202+
203+ #[ test]
204+ fn ai_config_errors_on_empty_runner_array ( ) {
205+ let dir = TempDir :: new ( ) . unwrap ( ) ;
206+ write_config ( & dir, "[skills]\n backend = \" agent-skills\" \n runner = []\n " ) ;
207+ let err = AiConfig :: load ( dir. path ( ) ) . unwrap_err ( ) ;
208+ let msg = format ! ( "{err:#}" ) ;
209+ assert ! ( msg. contains( "empty" ) , "error should mention empty array: {msg}" ) ;
210+ }
211+
157212 #[ test]
158213 fn ai_config_errors_on_unknown_backend ( ) {
159214 let dir = TempDir :: new ( ) . unwrap ( ) ;
0 commit comments