11package tools
22
33import (
4+ "context"
45 "os"
56 "path/filepath"
67 "strings"
8+ "time"
79
810 "github.com/HoangP8/tokless/internal/core"
911 "github.com/HoangP8/tokless/internal/util"
@@ -17,7 +19,9 @@ func cavemanExec(bin string, args []string, opts core.RunOpts, dryHint string, e
1719 if isTest () {
1820 return true , nil
1921 }
20- r := util .Run (bin , args , util.RunOptions {Capture : true , Env : env })
22+ ctx , cancel := context .WithTimeout (context .Background (), 15 * time .Minute )
23+ defer cancel ()
24+ r := util .Run (bin , args , util.RunOptions {Capture : true , Env : env , Ctx : ctx })
2125 if r .Code != 0 {
2226 util .L .Err ("caveman command failed: " + clip (r .Stderr ))
2327 return false , nil
@@ -33,6 +37,32 @@ func cavemanOpencodeInstallEnv() []string {
3337 return []string {"XDG_CONFIG_HOME=" + filepath .Dir (dir )}
3438}
3539
40+ // resolveSkillsBin returns ("skills", args[1:]) if skills binary is available,
41+ // otherwise ("npx", original npx args).
42+ func resolveSkillsBin (npxArgs []string ) (string , []string ) {
43+ if util .Which ("skills" ) != "" {
44+ return "skills" , npxArgs [2 :] // strip "-y skills" prefix
45+ }
46+ return "npx" , npxArgs
47+ }
48+
49+ // resolveCavemanBin returns ("caveman", cavemanArgs) if global caveman binary
50+ // is available, else ("npx", npxArgs).
51+ func resolveCavemanBin (agent string , upgrade bool ) (string , []string ) {
52+ if util .Which ("caveman" ) != "" {
53+ args := []string {"--only" , agent , "--no-mcp-shrink" }
54+ if upgrade {
55+ args = append (args , "--force" )
56+ }
57+ return "caveman" , args
58+ }
59+ args := []string {"-y" , "github:JuliusBrussee/caveman" , "--" , "--only" , agent , "--no-mcp-shrink" }
60+ if upgrade {
61+ args = append (args , "--force" )
62+ }
63+ return "npx" , args
64+ }
65+
3666func ensureOpencodeCommandsDir () {
3767 _ = os .MkdirAll (filepath .Join (util .OpenCodePathsResolved ().Dir , "commands" ), 0o755 )
3868}
@@ -305,7 +335,18 @@ var caveman = &core.ToolManifest{
305335 Channel : core .ChannelGitHub ,
306336 NotTrackable : true ,
307337 Install : func (opts core.RunOpts ) (bool , error ) {
308- opts .Reportf ("installed per agent" , 1 )
338+ if ! opts .DryRun && ! isTest () && ! opts .Upgrade && util .Which ("caveman" ) != "" {
339+ opts .Reportf ("already installed" , 1 )
340+ return true , nil
341+ }
342+ opts .Reportf ("installing from GitHub" , 0.3 )
343+ if ! opts .DryRun && ! isTest () && util .Which ("npm" ) != "" {
344+ ctx , cancel := context .WithTimeout (context .Background (), 15 * time .Minute )
345+ _ = util .Run ("npm" , []string {"install" , "-g" , "github:JuliusBrussee/caveman" },
346+ util.RunOptions {Capture : true , Ctx : ctx })
347+ cancel ()
348+ }
349+ opts .Reportf ("ready" , 1 )
309350 return true , nil
310351 },
311352 WireFor : map [string ]core.AgentFn {
@@ -328,19 +369,16 @@ var caveman = &core.ToolManifest{
328369 return ran , err
329370 },
330371 "opencode" : func (opts core.RunOpts ) (bool , error ) {
372+ if ! opts .Upgrade && opencodePluginInstalled () && opencodePluginFilesPresent () {
373+ WriteOwner ("opencode" , "caveman" )
374+ return true , nil
375+ }
331376 if ! opts .DryRun && ! isTest () {
332377 ensureOpencodeCommandsDir ()
333378 }
334- args := []string {"-y" , "github:JuliusBrussee/caveman" , "--" , "--only" , "opencode" , "--no-mcp-shrink" }
335- if opts .Upgrade {
336- args = append (args , "--force" )
337- }
338- ran , err := cavemanExec ("npx" , args , opts , "npx -y github:JuliusBrussee/caveman -- --only opencode --no-mcp-shrink" + func () string {
339- if opts .Upgrade {
340- return " --force"
341- }
342- return ""
343- }(), cavemanOpencodeInstallEnv ()... )
379+ bin , args := resolveCavemanBin ("opencode" , opts .Upgrade )
380+ ran , err := cavemanExec (bin , args , opts , bin + " " + strings .Join (args , " " ),
381+ cavemanOpencodeInstallEnv ()... )
344382 WriteOwner ("opencode" , "caveman" )
345383 if opts .DryRun || isTest () {
346384 return ran , err
@@ -353,8 +391,12 @@ var caveman = &core.ToolManifest{
353391 return opencodePluginInstalled (), err
354392 },
355393 "codex" : func (opts core.RunOpts ) (bool , error ) {
356- args := cavemanSkillsAddArgs ("codex" )
357- ran , err := cavemanExec ("npx" , args , opts , "npx " + strings .Join (args , "" ))
394+ if ! opts .Upgrade && codexCavemanInstalled () {
395+ WriteOwner ("codex" , "caveman" )
396+ return true , nil
397+ }
398+ bin , args := resolveSkillsBin (cavemanSkillsAddArgs ("codex" ))
399+ ran , err := cavemanExec (bin , args , opts , bin + " " + strings .Join (args , " " ))
358400 WriteOwner ("codex" , "caveman" )
359401 if opts .DryRun || isTest () {
360402 return ran , err
@@ -364,8 +406,12 @@ var caveman = &core.ToolManifest{
364406 return codexCavemanInstalled (), err
365407 },
366408 "antigravity" : func (opts core.RunOpts ) (bool , error ) {
367- args := cavemanSkillsAddArgs ("antigravity" )
368- ran , err := cavemanExec ("npx" , args , opts , "npx " + strings .Join (args , " " ))
409+ if ! opts .Upgrade && antigravityCavemanInstalled () {
410+ WriteOwner ("antigravity" , "caveman" )
411+ return true , nil
412+ }
413+ bin , args := resolveSkillsBin (cavemanSkillsAddArgs ("antigravity" ))
414+ ran , err := cavemanExec (bin , args , opts , bin + " " + strings .Join (args , " " ))
369415 WriteOwner ("antigravity" , "caveman" )
370416 if opts .DryRun || isTest () {
371417 return ran , err
0 commit comments