<!--
SPDX-FileCopyrightText: Amolith <amolith@secluded.site>

SPDX-License-Identifier: Unlicense
-->

# AGENTS.md - Developer Guide for synu

This document helps AI agents work effectively with the synu codebase.

## Project Overview

**synu** is a universal wrapper for AI agents that tracks quota usage for [Synthetic](https://synthetic.new) API calls. It's written in Fish shell and provides:

- Transparent quota tracking before/after agent execution
- Agent-specific configuration system (model routing, API endpoints)
- Persistent model preferences via cache file
- Interactive model selection with `gum`
- Passthrough mode for agents without special configuration

### Architecture

```
synu (main wrapper)
  ├─ _synu_get_quota (API quota fetching)
  ├─ _synu_cache (model preference persistence)
  └─ _synu_agents/ (agent-specific configuration)
      ├─ claude.fish  (Claude Code: env vars, multi-model)
      ├─ opencode.fish (OpenCode: CLI args)
      ├─ aider.fish    (Aider: env vars + CLI args)
      ├─ llxprt.fish   (llxprt: CLI args only)
      └─ qwen.fish     (Qwen Code: env vars)
```

**Control Flow:**
1. Parse arguments (check for interactive mode, agent name)
2. Load agent definition from `functions/_synu_agents/<agent>.fish` if it exists
3. Parse agent-specific flags using `argparse` with `--ignore-unknown` for passthrough
4. Configure agent environment (if agent has `_configure` function)
5. Fetch initial quota from Synthetic API
6. Execute agent with remaining arguments (plus any `_args` output)
7. Clean up environment variables (if agent has `_env_vars` function)
8. Fetch final quota and calculate/display session usage

**Data Flow:**
- Quota API → `_synu_get_quota` → space-separated "requests limit" string
- Agent flags → `argparse` → `_flag_*` variables → agent `_configure` function
- Cache file → `_synu_cache_get` → default model values
- Interactive mode → `gum` + Synthetic API → model IDs → recursive `synu` call with flags

## File Structure

```
├── functions/
│   ├── synu.fish              # Main wrapper function
│   ├── _synu_get_quota.fish   # Private: Fetch quota from API
│   ├── _synu_cache.fish       # Private: Model preference cache
│   └── _synu_agents/
│       ├── claude.fish        # Claude Code agent configuration
│       ├── opencode.fish      # OpenCode agent configuration
│       ├── aider.fish         # Aider agent configuration
│       ├── llxprt.fish        # llxprt agent configuration
│       └── qwen.fish          # Qwen Code agent configuration
├── completions/
│   └── synu.fish              # Shell completions
├── crush.json                 # LSP configuration (fish-lsp)
├── README.md                  # User documentation
└── .gitignore                 # Ignore agent working directories
```

## Essential Commands

This is a Fish shell library with no build system. Key commands:

### Testing Manually

```fish
# Source the function (from repo root)
source functions/synu.fish
source functions/_synu_get_quota.fish
source functions/_synu_cache.fish

# Check quota only
synu

# Test with an agent
synu claude "What does this function do?"

# Test interactive mode (requires gum)
synu i claude "prompt"
synu i opencode "prompt"
synu i aider "prompt"

# Test with model override flags
synu claude --opus hf:some/model "prompt"
synu claude --large hf:some/model "prompt"
synu opencode --model hf:some/model "prompt"
synu aider --model hf:some/model --editor-model hf:other/model "prompt"
```

### Installation Testing

```fish
# Install via fundle (in a clean fish instance)
fundle plugin 'synu' --url 'https://git.secluded.site/amolith/llm-projects'
fundle install
fundle init
```

### Checking for Syntax Errors

```fish
# Validate Fish syntax
fish -n functions/synu.fish
fish -n functions/_synu_get_quota.fish
fish -n functions/_synu_cache.fish
fish -n functions/_synu_agents/*.fish
fish -n completions/synu.fish
```

### LSP Integration

The project includes `crush.json` configuring `fish-lsp` for Fish language support. The LSP should automatically provide diagnostics when editing `.fish` files.

## Code Conventions

### Fish Shell Style

- **Indentation**: 4 spaces (no tabs)
- **Variable naming**: `snake_case` with descriptive names
  - Private/local vars: `set -l var_name`
  - Global vars: `set -g var_name` (agent defaults, selected models)
  - Exported vars: `set -gx VAR_NAME` (environment)
- **Function naming**:
  - Public: `synu`
  - Private/helper: `_synu_*`
  - Agent-specific: `_synu_agent_<name>_<action>`
  - Agent internal helpers: `_synu_<agent>_<helper>` (e.g., `_synu_claude_default`)
- **Comments**: Use `#` for explanatory comments focusing on *why* not *what*
- **Error handling**: Check `$status` after critical operations, return non-zero on errors
- **Error output**: Use `>&2` for all error messages
- **SPDX headers**: All files must include SPDX copyright and license headers

### Agent Configuration Pattern

Each agent definition in `functions/_synu_agents/<agent>.fish` can provide five functions:

1. **`_synu_agent_<name>_flags`** - Returns argparse flag specification (one per line)
   ```fish
   function _synu_agent_myagent_flags
       echo "m/model="
       echo "e/extra="
   end
   ```

2. **`_synu_agent_<name>_configure`** - Sets environment variables and/or global state
   ```fish
   function _synu_agent_myagent_configure
       argparse 'm/model=' -- $argv
       or return 1
       
       # Configure based on flags (use cache or fallback for defaults)
       set -g _synu_myagent_selected_model (_synu_myagent_default)
       if set -q _flag_model
           set -g _synu_myagent_selected_model $_flag_model
       end
       
       # Export env vars if agent uses them
       set -gx AGENT_MODEL $_synu_myagent_selected_model
   end
   ```

3. **`_synu_agent_<name>_args`** - Returns CLI arguments to inject (for agents that don't use env vars)
   ```fish
   function _synu_agent_myagent_args
       echo --model
       echo $_synu_myagent_selected_model
   end
   ```

4. **`_synu_agent_<name>_env_vars`** - Returns list of environment variables to clean up after execution
   ```fish
   function _synu_agent_myagent_env_vars
       echo AGENT_MODEL
       echo AGENT_CONFIG
   end
   ```

5. **`_synu_agent_<name>_interactive`** - Implements interactive model selection
   ```fish
   function _synu_agent_myagent_interactive
       # Use gum to get user input
       set -l selection (gum choose "option1" "option2")
       or return 1
       
       # Optionally save to cache
       if gum confirm "Save as default?"
           _synu_cache_set myagent model $selection
       end
       
       # Return flags to pass to main synu call
       echo --model=$selection
   end
   ```

**Agent Configuration Patterns:**

| Agent | Configuration Method | Notes |
|-------|---------------------|-------|
| claude | Environment variables | Multi-model (opus, sonnet, haiku, subagent) |
| qwen | Environment variables | Single model |
| aider | Env vars + CLI args | Env for API, CLI for models |
| opencode | CLI args only | Uses `synthetic/` prefix in model |
| llxprt | CLI args only | Needs --provider, --baseurl flags |

**Important patterns:**
- Agents without definitions work as passthrough (no special config)
- Use `argparse --ignore-unknown` in main wrapper so agent-native flags pass through
- Interactive functions return flags that trigger recursive `synu` call
- Configure functions receive flags already parsed from `_flag_*` variables
- Environment variables listed in `_env_vars` are automatically unset after agent exits
- Use `_synu_cache_get`/`_synu_cache_set` for persistent model preferences

### Cache System

The cache stores model preferences in `$XDG_CONFIG_HOME/synu/models.conf` (or `~/.config/synu/models.conf`).

**Format:**
```
# synu model preferences
# Format: agent.slot = model_id

claude.opus = hf:moonshotai/Kimi-K2-Thinking
claude.sonnet = hf:MiniMaxAI/MiniMax-M2
opencode.model = hf:MiniMaxAI/MiniMax-M2
```

**Functions:**
- `_synu_cache_get agent slot` - Returns cached value or fails
- `_synu_cache_set agent slot value` - Creates/updates cache entry

**Pattern for using cache with fallbacks:**
```fish
function _synu_myagent_default --description "Get default model"
    set -l cached (_synu_cache_get myagent model)
    if test $status -eq 0
        echo $cached
    else
        echo $_synu_myagent_fallback_model  # Hardcoded fallback
    end
end
```

### API Integration

**Synthetic API endpoints:**
- Quota: `GET https://api.synthetic.new/v2/quotas`
- Models: `GET https://api.synthetic.new/openai/v1/models`
- Claude routing: `https://api.synthetic.new/anthropic`
- OpenAI-compatible: `https://api.synthetic.new/openai/v1`

**Authentication**: Bearer token via `Authorization: Bearer $SYNTHETIC_API_KEY`

**Response parsing**: Use `jq` with fallbacks (`// 0`) for missing fields

**Error handling**: Check `curl` exit status and response emptiness before parsing

## Important Gotchas

### Argument Passing and argparse

1. **Passthrough requires `--ignore-unknown`**: When parsing agent flags, use `argparse --ignore-unknown` so agent-native flags aren't consumed by synu's parser.

2. **Flag variables after argparse**: After `argparse`, the `$argv` variable contains only non-flag arguments. Rebuild flag arguments from `_flag_*` variables to pass to configure function.

3. **Preserve argument order**: When executing the agent with `command $agent $extra_args $agent_args`, all original arguments (except synu-specific flags) must pass through unchanged.

4. **The `_args` function output is inserted before user args**: This allows agent-specific flags to be set while still allowing user to pass additional flags.

### Quota Tracking Edge Cases

1. **API failures are non-fatal**: If quota fetch fails before/after execution, warn but continue. The agent should still run.

2. **Default to zeros on failure**: If pre-execution quota fetch fails, use `"0 0"` to avoid math errors in post-execution display.

3. **Exit status preservation**: Always return the agent's exit status (`$exit_status`), not the quota fetch status.

### Environment Variable Scoping

- Use `set -gx` for environment variables that need to be visible to child processes (the actual agent binary)
- Agent defaults and selected models use `set -g` (global but not exported)
- Local calculations use `set -l`
- Environment variables are cleaned up after execution via `_env_vars`

### Interactive Mode Flow

Interactive mode (`synu i <agent> [args...]`) works by:
1. Calling agent's `_interactive` function to get flags
2. Optionally saving selections to cache (user prompted)
3. Recursively calling `synu <agent> <flags> [args...]` (non-interactive)
4. The recursive call then follows normal path with pre-parsed flags

This means the `_configure` function must handle both direct flag input and flags from interactive mode identically.

### Model ID Prefixes

Different agents expect different model ID formats:
- **opencode**: Prefix with `synthetic/` (e.g., `synthetic/hf:moonshotai/Kimi-K2`)
- **aider**: Prefix with `openai/` (e.g., `openai/hf:moonshotai/Kimi-K2`)
- **claude, qwen, llxprt**: Use raw model ID (e.g., `hf:moonshotai/Kimi-K2`)

### Fish-specific Considerations

- **Status checks**: Always use `test $status -ne 0` immediately after command, as any subsequent command overwrites `$status`
- **Array slicing**: Use `$argv[2..-1]` syntax (1-indexed, inclusive)
- **Command substitution**: Use `(command)` not `$(command)`
- **String operations**: Use `string` builtin (`string split`, `string match`)
- **No `local` keyword**: Use `set -l` for local scope
- **Variable indirection**: Use `$$var_name` to get value of variable named by `$var_name`

## Testing Approach

This project has no automated test suite. Testing is manual:

1. **Syntax validation**: Run `fish -n` on all `.fish` files
2. **Quota tracking**: Run `synu` alone, check quota display with color
3. **Passthrough**: Test with unknown agent (`synu echo "hello"`)
4. **Agent config**: Test each agent with/without flags
5. **Interactive mode**: Test `synu i <agent>` with gum installed
6. **Cache persistence**: Test that interactive selections save correctly
7. **Error cases**: Test without `SYNTHETIC_API_KEY`, with invalid API key

**When making changes:**
- Validate syntax with `fish -n`
- Source the function and test manually with different argument combinations
- Check that exit status is preserved
- Verify quota display shows correct before/after values
- Test cache file creation and updates

## Common Tasks

### Adding a New Agent Configuration

1. Create `functions/_synu_agents/<agent>.fish`
2. Add SPDX header
3. Source cache functions: `source (status dirname)/../_synu_cache.fish`
4. Define fallback default: `set -g _synu_<agent>_fallback_model "hf:..."`
5. Define `_synu_<agent>_default` helper (uses cache with fallback)
6. Define `_synu_agent_<agent>_flags` (return argparse flag specs)
7. Define `_synu_agent_<agent>_configure` (set state/env vars)
8. Define `_synu_agent_<agent>_args` (if agent uses CLI args for model)
9. Define `_synu_agent_<agent>_env_vars` (if agent uses env vars)
10. Optionally define `_synu_agent_<agent>_interactive` (for `synu i <agent>`)
11. Update `completions/synu.fish` to add the new agent to suggestions
12. Test manually

### Modifying Quota Display

Quota display logic is in two places:
- **No args case**: `synu.fish` lines ~7-29
- **Post-execution**: `synu.fish` lines ~162-180

Both use the same color thresholds:
- Green: < 33% used
- Yellow: 33-66% used
- Red: > 66% used

### Debugging API Issues

```fish
# Test quota fetch directly
source functions/_synu_get_quota.fish
set -gx SYNTHETIC_API_KEY your_key_here
_synu_get_quota
# Should output: "requests_used limit"

# Test with curl directly
curl -s -f -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
    "https://api.synthetic.new/v2/quotas" | jq .

# Test model listing
curl -s -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
    "https://api.synthetic.new/openai/v1/models" | jq '.data[].name'
```

### Debugging Cache Issues

```fish
# Check cache file location
echo $XDG_CONFIG_HOME/synu/models.conf
# or
echo ~/.config/synu/models.conf

# View cache contents
cat ~/.config/synu/models.conf

# Test cache functions
source functions/_synu_cache.fish
_synu_cache_set test model hf:test/model
_synu_cache_get test model
```

### Understanding Model Configuration Flow

**Example: Claude with flag override**
1. User runs: `synu claude --large hf:some/model "prompt"`
2. `synu.fish` loads `functions/_synu_agents/claude.fish`
3. Calls `_synu_agent_claude_flags` to get flag spec
4. Parses with `argparse --ignore-unknown` → sets `_flag_large`
5. Rebuilds flags for configure: `--large=hf:some/model`
6. Calls `_synu_agent_claude_configure --large=hf:some/model`
7. Configure sets opus/sonnet/subagent models from flag
8. Exports `ANTHROPIC_*` environment variables
9. Executes `command claude "prompt"` with those environment variables
10. Claude Code sees Synthetic models via env vars and uses them
11. After exit, env vars are cleaned up via `_env_vars`

**Example: OpenCode with CLI args**
1. User runs: `synu opencode "prompt"`
2. `synu.fish` loads `functions/_synu_agents/opencode.fish`
3. `_configure` sets `_synu_opencode_selected_model` from cache/fallback
4. `_args` returns: `--model synthetic/hf:MiniMaxAI/MiniMax-M2`
5. Executes: `command opencode -m "synthetic/hf:..." "prompt"`

## Commit Conventions

Use **Conventional Commits** with type and scope:

**Types:** `feat`, `fix`, `refactor`, `docs`, `chore`, `test`

**Scopes:**
- `synu` - main wrapper function
- `quota` - quota fetching/display
- `cache` - model preference caching
- `agents/<name>` - specific agent configuration (e.g., `agents/claude`)
- `completions` - shell completions
- `docs` - documentation

**Examples:**
```
feat(synu): add support for model override flags
fix(quota): handle API errors gracefully
feat(agents/claude): add interactive model selection
feat(cache): implement persistent model preferences
docs(readme): document interactive mode
```

**Trailers:**
- Use `References: HASH` for related commits/bugs
- Always include `Assisted-by: [Model Name] via [Tool Name]` when AI-assisted

## Project-Specific Context

### Why Fish Shell?

This is Amolith's personal project and Fish is their shell of choice. Fish provides:
- Clean syntax without bashisms
- Better error handling than POSIX shells
- Native arrays and proper scoping
- Built-in string manipulation

### Why Synthetic API?

Synthetic provides a unified API for multiple LLM providers with quota tracking. This wrapper makes quota visibility automatic and consistent across different agent tools.

### Design Philosophy

- **Minimal interference**: Pass through as much as possible to the underlying agent
- **Graceful degradation**: If quota tracking fails, still run the agent
- **Extensible**: Easy to add new agent configurations without modifying core wrapper
- **Interactive when helpful**: Support both CLI flags and TUI selection
- **Persistent preferences**: Save model selections for future sessions

### Configured Agents

Agents with specific configurations in `functions/_synu_agents/`:

| Agent | Models | Configuration Method | Interactive |
|-------|--------|---------------------|-------------|
| claude | opus, sonnet, haiku, subagent | Environment variables | Yes |
| opencode | single | CLI args | Yes |
| aider | main, editor | Env + CLI args | Yes |
| llxprt | single | CLI args | Yes |
| qwen | single | Environment variables | Yes |

Passthrough agents (no config, just quota tracking): crush, amp, octo, codex

Any command can be wrapped; the above are just agents with explicit configuration.

## Future Considerations

Potential enhancements (not currently implemented):
- Support for other quota tracking APIs besides Synthetic
- Agent-specific usage tracking/history
- Budget warnings/limits
- More agent configurations

When implementing features, maintain the core principles:
- Don't break passthrough mode
- Keep it simple and transparent
- Graceful degradation
- Follow Fish idioms
