feat(core): support agents requiring CLI args

Amolith created

Some agents (like OpenCode) accept model configuration via CLI flags
instead of environment variables. Added _synu_agent_{agent}_args hook
that returns additional arguments to pass during execution.

Fixed flag extraction loop in main wrapper to handle arbitrary flag
names (including new -m/model for OpenCode). Adds OpenCode agent
definition that uses both cache system and CLI args feature.

Assisted-by: Claude Sonnet 4.5 via Crush

Change summary

functions/_synu_agents/opencode.fish | 88 ++++++++++++++++++++++++++++++
functions/synu.fish                  | 13 +++-
2 files changed, 98 insertions(+), 3 deletions(-)

Detailed changes

functions/_synu_agents/opencode.fish 🔗

@@ -0,0 +1,88 @@
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: Unlicense
+
+# OpenCode agent definition for synu
+# Provides model configuration for routing through Synthetic API
+# OpenCode only accepts model via -m flag, not environment variables
+
+# Source cache functions
+source (status dirname)/../_synu_cache.fish
+
+# Fallback default (used when no cache entry exists)
+set -g _synu_opencode_fallback_model "hf:MiniMaxAI/MiniMax-M2"
+
+function _synu_opencode_default --description "Get default model"
+    set -l cached (_synu_cache_get opencode model)
+    if test $status -eq 0
+        echo $cached
+    else
+        echo $_synu_opencode_fallback_model
+    end
+end
+
+function _synu_agent_opencode_flags --description "Return argparse-compatible flag specification"
+    echo "m/model="
+end
+
+function _synu_agent_opencode_configure --description "Configure OpenCode model selection"
+    # Parse flags passed from main synu
+    argparse 'm/model=' -- $argv
+    or return 1
+
+    # Start with default (from cache or fallback)
+    set -g _synu_opencode_selected_model (_synu_opencode_default)
+
+    # Apply override if provided
+    if set -q _flag_model
+        set -g _synu_opencode_selected_model $_flag_model
+    end
+end
+
+function _synu_agent_opencode_args --description "Return CLI arguments to pass to opencode"
+    # Return -m flag with selected model
+    echo -m
+    echo $_synu_opencode_selected_model
+end
+
+function _synu_agent_opencode_interactive --description "Interactive model selection using gum"
+    # Check for gum
+    if not command -q gum
+        echo "Error: gum is required for interactive mode. Install: https://github.com/charmbracelet/gum" >&2
+        return 1
+    end
+
+    # Fetch available models
+    set -l models_json (gum spin --spinner dot --title "Fetching models..." -- \
+        curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
+        "https://api.synthetic.new/openai/v1/models")
+    or return 1
+
+    set -l model_names (echo $models_json | jq -r '.data[].name')
+    or return 1
+
+    # Select model
+    set -l model_name (printf "%s\n" $model_names | \
+        gum filter --limit 1 --header "Select model for OpenCode" \
+        --placeholder "Filter models...")
+    or return 1
+
+    set -l model_id (echo $models_json | \
+        jq -r --arg name "$model_name" '.data[] | select(.name == $name) | .id')
+
+    if test -z "$model_id"
+        echo "Error: Could not find model ID" >&2
+        return 1
+    end
+
+    # Build flags
+    set -l flags --model=$model_id
+
+    # Offer to save as default
+    if gum confirm "Save as default for 'opencode'?"
+        _synu_cache_set opencode model $model_id
+    end
+
+    # Output flags for caller to use
+    echo $flags
+end

functions/synu.fish 🔗

@@ -83,6 +83,7 @@ function synu --description "Universal agent wrapper with Synthetic API quota tr
         set -l parsed_args
         if test -n "$flag_spec"
             # Parse with --ignore-unknown so agent-native flags pass through
+            # @fish-lsp-disable-next-line 4004
             argparse --ignore-unknown $flag_spec -- $agent_args
             or return 1
 
@@ -91,8 +92,7 @@ function synu --description "Universal agent wrapper with Synthetic API quota tr
 
             # Rebuild the flag arguments to pass to configure
             # Check each possible flag and add if set
-            for flag in L/large l/light o/opus s/sonnet H/haiku a/agent
-                set -l short_flag (string split '/' $flag)[1]
+            for flag in L/large l/light o/opus s/sonnet H/haiku a/agent m/model
                 set -l long_flag (string split '/' $flag)[2]
                 set -l var_name _flag_$long_flag
                 if set -q $var_name
@@ -106,6 +106,12 @@ function synu --description "Universal agent wrapper with Synthetic API quota tr
         or return 1
     end
 
+    # Check if agent provides extra CLI arguments (for agents that don't use env vars)
+    set -l extra_args
+    if functions -q _synu_agent_{$agent}_args
+        set extra_args (_synu_agent_{$agent}_args)
+    end
+
     # Fetch quota before agent execution
     set -l quota_before (_synu_get_quota)
     if test $status -ne 0
@@ -121,7 +127,8 @@ function synu --description "Universal agent wrapper with Synthetic API quota tr
 
     # Execute the agent with all arguments passed through unchanged
     # Use 'command' to bypass function recursion and call the actual binary
-    command $agent $agent_args
+    # extra_args contains agent-specific CLI flags (e.g., -m for opencode)
+    command $agent $extra_args $agent_args
     set -l exit_status $status
 
     # Fetch quota after agent execution