Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .infer/computer_use.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
enabled: false
floating_window:
enabled: true
respawn_on_close: true
position: top-right
always_on_top: true
screenshot:
enabled: true
max_width: 1920
max_height: 1080
target_width: 1024
target_height: 768
format: jpeg
quality: 85
streaming_enabled: true
capture_interval: 3
buffer_size: 5
temp_dir: ""
log_captures: false
show_overlay: true
rate_limit:
enabled: true
max_actions_per_minute: 60
window_seconds: 60
tools:
mouse_move:
enabled: true
mouse_click:
enabled: true
mouse_scroll:
enabled: true
keyboard_type:
enabled: true
max_text_length: 1000
typing_delay_ms: 100
get_focused_app:
enabled: true
activate_app:
enabled: true
41 changes: 1 addition & 40 deletions .infer/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ tools:
- .infer/keybindings.yaml
- .infer/prompts.yaml
- .infer/channels.yaml
- .infer/computer_use.yaml
- .git/
- '*.env'
bash:
Expand Down Expand Up @@ -273,43 +274,3 @@ web:
install_version: latest
install_dir: ~/.local/bin
servers: []
computer_use:
enabled: false
floating_window:
enabled: true
respawn_on_close: true
position: top-right
always_on_top: true
screenshot:
enabled: true
max_width: 1920
max_height: 1080
target_width: 1024
target_height: 768
format: jpeg
quality: 85
streaming_enabled: true
capture_interval: 3
buffer_size: 5
temp_dir: ""
log_captures: false
show_overlay: true
rate_limit:
enabled: true
max_actions_per_minute: 60
window_seconds: 60
tools:
mouse_move:
enabled: true
mouse_click:
enabled: true
mouse_scroll:
enabled: true
keyboard_type:
enabled: true
max_text_length: 1000
typing_delay_ms: 100
get_focused_app:
enabled: true
activate_app:
enabled: true
96 changes: 96 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,27 @@ func getEffectiveChannelsConfigPath() string {
return config.DefaultChannelsPath
}

// getEffectiveComputerUseConfigPath returns the path to the computer_use config file
// Searches in this order: 1) project .infer/computer_use.yaml, 2) user home ~/.infer/computer_use.yaml
func getEffectiveComputerUseConfigPath() string {
searchPaths := []string{
config.DefaultComputerUsePath,
}

if homeDir, err := os.UserHomeDir(); err == nil {
homePath := filepath.Join(homeDir, config.ConfigDirName, config.ComputerUseFileName)
searchPaths = append(searchPaths, homePath)
}

for _, path := range searchPaths {
if _, err := os.Stat(path); err == nil {
return path
}
}

return config.DefaultComputerUsePath
}

// getEffectivePromptsConfigPath returns the path to the prompts config file
// Searches in this order: 1) project .infer/prompts.yaml, 2) user home ~/.infer/prompts.yaml
func getEffectivePromptsConfigPath() string {
Expand Down Expand Up @@ -688,6 +709,15 @@ func loadConfigFromViper() (*config.Config, error) {
cfg.Channels = *channelsCfg
applyChannelsEnvOverrides(cfg)

cuPath := getEffectiveComputerUseConfigPath()
cuCfg, err := config.LoadComputerUse(cuPath)
if err != nil {
logger.Warn("Failed to load computer_use config, using defaults", "error", err, "path", cuPath)
cuCfg = config.DefaultComputerUseConfig()
}
cfg.ComputerUse = *cuCfg
applyComputerUseEnvOverrides(cfg)

return cfg, nil
}

Expand Down Expand Up @@ -867,6 +897,72 @@ func applyChannelsEnvOverrides(cfg *config.Config) {
setStringSlice("INFER_CHANNELS_WHATSAPP_ALLOWED_USERS", &cfg.Channels.WhatsApp.AllowedUsers)
}

// applyComputerUseEnvOverrides applies INFER_COMPUTER_USE_* env vars onto
// the in-memory computer_use config. Run AFTER LoadComputerUse so envs win
// over computer_use.yaml. The computer_use config now lives in its own
// file (yaml:"-" mapstructure:"-" on Config.ComputerUse), so viper does not
// bind these env vars itself - this function is the single source of
// env-var support. Mirrors applyChannelsEnvOverrides.
func applyComputerUseEnvOverrides(cfg *config.Config) {
setBool := func(env string, target *bool) {
val, ok := os.LookupEnv(env)
if !ok {
return
}
if b, err := strconv.ParseBool(strings.TrimSpace(val)); err == nil {
*target = b
}
}
setInt := func(env string, target *int) {
val, ok := os.LookupEnv(env)
if !ok {
return
}
if n, err := strconv.Atoi(strings.TrimSpace(val)); err == nil {
*target = n
}
}
setString := func(env string, target *string) {
if val, ok := os.LookupEnv(env); ok {
*target = val
}
}

setBool("INFER_COMPUTER_USE_ENABLED", &cfg.ComputerUse.Enabled)

setBool("INFER_COMPUTER_USE_FLOATING_WINDOW_ENABLED", &cfg.ComputerUse.FloatingWindow.Enabled)
setBool("INFER_COMPUTER_USE_FLOATING_WINDOW_RESPAWN_ON_CLOSE", &cfg.ComputerUse.FloatingWindow.RespawnOnClose)
setString("INFER_COMPUTER_USE_FLOATING_WINDOW_POSITION", &cfg.ComputerUse.FloatingWindow.Position)
setBool("INFER_COMPUTER_USE_FLOATING_WINDOW_ALWAYS_ON_TOP", &cfg.ComputerUse.FloatingWindow.AlwaysOnTop)

setBool("INFER_COMPUTER_USE_SCREENSHOT_ENABLED", &cfg.ComputerUse.Screenshot.Enabled)
setInt("INFER_COMPUTER_USE_SCREENSHOT_MAX_WIDTH", &cfg.ComputerUse.Screenshot.MaxWidth)
setInt("INFER_COMPUTER_USE_SCREENSHOT_MAX_HEIGHT", &cfg.ComputerUse.Screenshot.MaxHeight)
setInt("INFER_COMPUTER_USE_SCREENSHOT_TARGET_WIDTH", &cfg.ComputerUse.Screenshot.TargetWidth)
setInt("INFER_COMPUTER_USE_SCREENSHOT_TARGET_HEIGHT", &cfg.ComputerUse.Screenshot.TargetHeight)
setString("INFER_COMPUTER_USE_SCREENSHOT_FORMAT", &cfg.ComputerUse.Screenshot.Format)
setInt("INFER_COMPUTER_USE_SCREENSHOT_QUALITY", &cfg.ComputerUse.Screenshot.Quality)
setBool("INFER_COMPUTER_USE_SCREENSHOT_STREAMING_ENABLED", &cfg.ComputerUse.Screenshot.StreamingEnabled)
setInt("INFER_COMPUTER_USE_SCREENSHOT_CAPTURE_INTERVAL", &cfg.ComputerUse.Screenshot.CaptureInterval)
setInt("INFER_COMPUTER_USE_SCREENSHOT_BUFFER_SIZE", &cfg.ComputerUse.Screenshot.BufferSize)
setString("INFER_COMPUTER_USE_SCREENSHOT_TEMP_DIR", &cfg.ComputerUse.Screenshot.TempDir)
setBool("INFER_COMPUTER_USE_SCREENSHOT_LOG_CAPTURES", &cfg.ComputerUse.Screenshot.LogCaptures)
setBool("INFER_COMPUTER_USE_SCREENSHOT_SHOW_OVERLAY", &cfg.ComputerUse.Screenshot.ShowOverlay)

setBool("INFER_COMPUTER_USE_RATE_LIMIT_ENABLED", &cfg.ComputerUse.RateLimit.Enabled)
setInt("INFER_COMPUTER_USE_RATE_LIMIT_MAX_ACTIONS_PER_MINUTE", &cfg.ComputerUse.RateLimit.MaxActionsPerMinute)
setInt("INFER_COMPUTER_USE_RATE_LIMIT_WINDOW_SECONDS", &cfg.ComputerUse.RateLimit.WindowSeconds)

setBool("INFER_COMPUTER_USE_TOOLS_MOUSE_MOVE_ENABLED", &cfg.ComputerUse.Tools.MouseMove.Enabled)
setBool("INFER_COMPUTER_USE_TOOLS_MOUSE_CLICK_ENABLED", &cfg.ComputerUse.Tools.MouseClick.Enabled)
setBool("INFER_COMPUTER_USE_TOOLS_MOUSE_SCROLL_ENABLED", &cfg.ComputerUse.Tools.MouseScroll.Enabled)
setBool("INFER_COMPUTER_USE_TOOLS_KEYBOARD_TYPE_ENABLED", &cfg.ComputerUse.Tools.KeyboardType.Enabled)
setInt("INFER_COMPUTER_USE_TOOLS_KEYBOARD_TYPE_MAX_TEXT_LENGTH", &cfg.ComputerUse.Tools.KeyboardType.MaxTextLength)
setInt("INFER_COMPUTER_USE_TOOLS_KEYBOARD_TYPE_TYPING_DELAY_MS", &cfg.ComputerUse.Tools.KeyboardType.TypingDelayMs)
setBool("INFER_COMPUTER_USE_TOOLS_GET_FOCUSED_APP_ENABLED", &cfg.ComputerUse.Tools.GetFocusedApp.Enabled)
setBool("INFER_COMPUTER_USE_TOOLS_ACTIVATE_APP_ENABLED", &cfg.ComputerUse.Tools.ActivateApp.Enabled)
}

// GetUserspaceFlag checks for --userspace flag on the current command or parent commands
func GetUserspaceFlag(cmd *cobra.Command) bool {
if userspace, err := cmd.Flags().GetBool("userspace"); err == nil && userspace {
Expand Down
43 changes: 41 additions & 2 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen
userspace, _ := cmd.Flags().GetBool("userspace")
skipMigrations, _ := cmd.Flags().GetBool("skip-migrations")

var configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, agentsPath string
var configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, computerUsePath, agentsPath string

if userspace {
homeDir, err := os.UserHomeDir()
Expand All @@ -58,6 +58,7 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen
keybindingsPath = filepath.Join(homeDir, config.ConfigDirName, config.KeybindingsFileName)
promptsPath = filepath.Join(homeDir, config.ConfigDirName, config.PromptsFileName)
channelsPath = filepath.Join(homeDir, config.ConfigDirName, config.ChannelsFileName)
computerUsePath = filepath.Join(homeDir, config.ConfigDirName, config.ComputerUseFileName)
agentsPath = filepath.Join(homeDir, config.ConfigDirName, config.AgentsFileName)
} else {
configPath = config.DefaultConfigPath
Expand All @@ -72,11 +73,12 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen
keybindingsPath = config.DefaultKeybindingsPath
promptsPath = config.DefaultPromptsPath
channelsPath = config.DefaultChannelsPath
computerUsePath = config.DefaultComputerUsePath
agentsPath = config.DefaultAgentsPath
}

if !overwrite {
if err := validateFilesNotExist(configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, agentsPath); err != nil {
if err := validateFilesNotExist(configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, computerUsePath, agentsPath); err != nil {
return err
}
}
Expand Down Expand Up @@ -140,6 +142,11 @@ tmp/
return fmt.Errorf("failed to create channels config file: %w", err)
}

cuMigrated, err := createComputerUseConfigFile(computerUsePath)
if err != nil {
return fmt.Errorf("failed to create computer_use config file: %w", err)
}

if err := createAgentsConfigFile(agentsPath); err != nil {
return fmt.Errorf("failed to create agents config file: %w", err)
}
Expand All @@ -164,11 +171,16 @@ tmp/
fmt.Printf(" Created: %s\n", keybindingsPath)
fmt.Printf(" Created: %s\n", promptsPath)
fmt.Printf(" Created: %s\n", channelsPath)
fmt.Printf(" Created: %s\n", computerUsePath)
fmt.Printf(" Created: %s\n", agentsPath)
if migrated {
fmt.Printf("\n%s Migrated legacy `channels:` block from config.yaml into %s.\n", icons.CheckMarkStyle.Render(icons.CheckMark), channelsPath)
fmt.Printf(" You can now remove the `channels:` block from %s.\n", configPath)
}
if cuMigrated {
fmt.Printf("\n%s Migrated legacy `computer_use:` block from config.yaml into %s.\n", icons.CheckMarkStyle.Render(icons.CheckMark), computerUsePath)
fmt.Printf(" You can now remove the `computer_use:` block from %s.\n", configPath)
}
fmt.Println("")
if userspace {
fmt.Println("This userspace configuration will be used as a fallback for all projects.")
Expand Down Expand Up @@ -449,6 +461,33 @@ func createChannelsConfigFile(path string) (bool, error) {
return migrated, nil
}

// createComputerUseConfigFile writes a fresh computer_use.yaml. Returns
// true when the file was seeded from a legacy `computer_use:` block found
// in viper (i.e. migrated from config.yaml) rather than from in-code
// defaults. Migration only runs when no computer_use.yaml exists yet, so
// it is safe to re-run init.
func createComputerUseConfigFile(path string) (bool, error) {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return false, fmt.Errorf("failed to create config directory: %w", err)
}

cuCfg := config.DefaultComputerUseConfig()
migrated := false

if _, err := os.Stat(path); os.IsNotExist(err) && V != nil && V.IsSet("computer_use") {
legacy := config.DefaultComputerUseConfig()
if err := V.UnmarshalKey("computer_use", legacy); err == nil {
cuCfg = legacy
migrated = true
}
}

if err := config.SaveComputerUse(path, cuCfg); err != nil {
return false, err
}
return migrated, nil
}

// createAgentsConfigFile writes a fresh agents.yaml seeded from the in-code
// defaults so users can manage A2A agents via `infer agents` commands.
func createAgentsConfigFile(path string) error {
Expand Down
2 changes: 1 addition & 1 deletion cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestInitializeProject(t *testing.T) {
"userspace": false,
"skip-migrations": true,
},
wantFiles: []string{".infer/config.yaml", ".infer/.gitignore"},
wantFiles: []string{".infer/config.yaml", ".infer/.gitignore", ".infer/computer_use.yaml"},
wantNoFiles: []string{"AGENTS.md"},
wantErr: false,
},
Expand Down
72 changes: 72 additions & 0 deletions config/computer_use.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package config

import (
utils "github.com/inference-gateway/cli/config/utils"
)

const (
ComputerUseFileName = "computer_use.yaml"
DefaultComputerUsePath = ConfigDirName + "/" + ComputerUseFileName
)

// DefaultComputerUseConfig returns the in-code default computer_use
// configuration used when no computer_use.yaml file exists. `infer init`
// seeds the file from this and the runtime falls back to it when the file
// is absent.
func DefaultComputerUseConfig() *ComputerUseConfig {
return &ComputerUseConfig{
Enabled: false,
FloatingWindow: FloatingWindowConfig{
Enabled: true,
RespawnOnClose: true,
Position: "top-right",
AlwaysOnTop: true,
},
Screenshot: ScreenshotToolConfig{
Enabled: true,
MaxWidth: 1920,
MaxHeight: 1080,
TargetWidth: 1024,
TargetHeight: 768,
Format: "jpeg",
Quality: 85,
StreamingEnabled: true,
CaptureInterval: 3,
BufferSize: 5,
TempDir: "",
LogCaptures: false,
ShowOverlay: true,
},
RateLimit: RateLimitConfig{
Enabled: true,
MaxActionsPerMinute: 60,
WindowSeconds: 60,
},
Tools: ComputerUseToolsConfig{
MouseMove: MouseMoveToolConfig{Enabled: true},
MouseClick: MouseClickToolConfig{Enabled: true},
MouseScroll: MouseScrollToolConfig{Enabled: true},
KeyboardType: KeyboardTypeToolConfig{
Enabled: true,
MaxTextLength: 1000,
TypingDelayMs: 100,
},
GetFocusedApp: GetFocusedAppToolConfig{Enabled: true},
ActivateApp: ActivateAppToolConfig{Enabled: true},
},
}
}

// LoadComputerUse reads computer_use.yaml from disk. When the file is
// missing it returns the in-code defaults so callers can treat absence as
// "use defaults" without special-casing. The file body is run through
// os.ExpandEnv so `${VAR}`-style references resolve from the environment.
func LoadComputerUse(path string) (*ComputerUseConfig, error) {
return utils.LoadYAML(path, "computer_use", DefaultComputerUseConfig)
}

// SaveComputerUse writes the computer_use configuration to disk, creating
// any missing parent directories.
func SaveComputerUse(path string, cfg *ComputerUseConfig) error {
return utils.SaveYAML(path, "computer_use", cfg)
}
Loading