From 5e49f26c8166722cdb79147019cacd7ab01e1015 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 26 Nov 2025 08:02:09 -0700 Subject: [PATCH] feat(synu): clean up agent env vars after exit Agents can now declare which environment variables they set via a _synu_agent__env_vars function. The main wrapper automatically unsets these variables after the agent exits, preventing pollution of the parent shell environment. Changes: - Added _synu_agent_claude_env_vars to declare ANTHROPIC_* and CLAUDE_CODE_* vars - Added cleanup logic in synu.fish to call _env_vars and erase variables - Fixed string match bug in claude.fish interactive mode (missing -- separator) - Updated AGENTS.md to document the new _env_vars function pattern Assisted-by: Claude Sonnet 4.5 via Crush --- AGENTS.md | 366 +++++++++++++++++++++++++++++ functions/_synu_agents/claude.fish | 12 +- functions/synu.fish | 7 + 3 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..f6dc8d80c169df127e375eb9ea7e37a9cc1f0775 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,366 @@ + + +# 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 (e.g., model routing for Claude Code) +- Interactive model selection with `gum` +- Passthrough mode for agents without special configuration + +### Architecture + +``` +synu (main wrapper) + ├─ _synu_get_quota (API quota fetching) + └─ _synu_agents/ (agent-specific configuration) + └─ claude.fish (Claude Code model configuration) +``` + +**Control Flow:** +1. Parse arguments (check for interactive mode, agent name) +2. Load agent definition from `functions/_synu_agents/.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 +7. 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 +- 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_agents/ +│ └── claude.fish # Claude 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 + +# Check quota only +synu + +# Test with an agent +synu claude "What does this function do?" + +# Test interactive mode (requires gum) +synu i claude "prompt" + +# Test with model override flags +synu claude --opus hf:some/model "prompt" +synu claude --large hf:some/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_agents/claude.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) + - Exported vars: `set -gx VAR_NAME` (environment) +- **Function naming**: + - Public: `synu` + - Private/helper: `_synu_*` + - Agent-specific: `_synu_agent__` +- **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/.fish` can provide four functions: + +1. **`_synu_agent__flags`** - Returns argparse flag specification (one per line) + ```fish + function _synu_agent_myagent_flags + echo "L/large=" + echo "o/option=" + end + ``` + +2. **`_synu_agent__configure`** - Sets environment variables before execution + ```fish + function _synu_agent_myagent_configure + argparse 'L/large=' 'o/option=' -- $argv + or return 1 + + # Configure based on flags + if set -q _flag_large + set -gx AGENT_MODEL $_flag_large + end + end + ``` + +3. **`_synu_agent__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 + ``` + +4. **`_synu_agent__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 + + # Return flags to pass to main synu call + echo --option=$selection + end + ``` + +**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 + +### 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` + +**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 $agent_args`, all original arguments (except synu-specific flags) must pass through unchanged. + +### 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 use `set -g` (global but not exported) +- Local calculations use `set -l` + +### Interactive Mode Flow + +Interactive mode (`synu i [args...]`) works by: +1. Calling agent's `_interactive` function to get flags +2. Recursively calling `synu [args...]` (non-interactive) +3. 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. + +### 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 + +## 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 Claude with/without flags +5. **Interactive mode**: Test `synu i claude` with gum installed +6. **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 + +## Common Tasks + +### Adding a New Agent Configuration + +1. Create `functions/_synu_agents/.fish` +2. Add SPDX header +3. Define `_synu_agent__flags` (return argparse flag specs) +4. Define `_synu_agent__configure` (set environment variables) +5. Optionally define `_synu_agent__interactive` (for `synu i `) +6. Update `completions/synu.fish` to add the new agent to suggestions +7. Test manually + +### Modifying Quota Display + +Quota display logic is in two places: +- **No args case**: lines 6-28 of `synu.fish` +- **Post-execution**: lines 128-166 of `synu.fish` + +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 . +``` + +### Understanding Model Configuration Flow (Claude) + +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: `L/large=`, `o/opus=`, etc. +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 `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, etc. +8. Executes `command claude "prompt"` with those environment variables +9. Claude Code sees Synthetic models via env vars and uses them + +## 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 +- `agents/` - 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 +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 + +### Known Agents + +Suggested in completions (may or may not have specific configurations): +- `claude` - Claude Code (has configuration) +- `crush` - Crush agent (passthrough) +- `amp` - Amp agent (passthrough) +- `octo` - Octo agent (passthrough) +- `codex` - Codex agent (passthrough) + +Any command can be wrapped, these are just suggestions for completion. + +## Future Considerations + +Potential enhancements (not currently implemented): +- Cache quota between rapid calls to reduce API hits +- Support for other quota tracking APIs besides Synthetic +- Agent-specific usage tracking/history +- Budget warnings/limits +- More agent configurations (crush, amp, etc.) + +When implementing features, maintain the core principles: +- Don't break passthrough mode +- Keep it simple and transparent +- Graceful degradation +- Follow Fish idioms diff --git a/functions/_synu_agents/claude.fish b/functions/_synu_agents/claude.fish index 370449b4f139f692d5e8925b846b1524b1ee1e38..1b4fa0e294d3ec86368ea06c963d9038c963d732 100644 --- a/functions/_synu_agents/claude.fish +++ b/functions/_synu_agents/claude.fish @@ -34,6 +34,16 @@ function _synu_agent_claude_flags --description "Return argparse-compatible flag echo "a/agent=" end +function _synu_agent_claude_env_vars --description "Return list of environment variables set by configure" + echo ANTHROPIC_BASE_URL + echo ANTHROPIC_AUTH_TOKEN + echo ANTHROPIC_DEFAULT_OPUS_MODEL + echo ANTHROPIC_DEFAULT_SONNET_MODEL + echo ANTHROPIC_DEFAULT_HAIKU_MODEL + echo CLAUDE_CODE_SUBAGENT_MODEL + echo CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC +end + function _synu_agent_claude_configure --description "Configure Claude Code environment variables" # Parse flags passed from main synu argparse 'L/large=' 'l/light=' 'o/opus=' 's/sonnet=' 'H/haiku=' 'a/agent=' -- $argv @@ -192,7 +202,7 @@ function _synu_agent_claude_interactive --description "Interactive model selecti if gum confirm "Save as default for 'claude'?" for flag in $flags # Parse --key=value format - set -l parts (string match -r '^--([^=]+)=(.+)$' $flag) + set -l parts (string match -r -- '^--([^=]+)=(.+)$' $flag) if test -n "$parts[2]" set -l key $parts[2] set -l value $parts[3] diff --git a/functions/synu.fish b/functions/synu.fish index dd54c7b1aa0ab28cbbe2351e8a1a79edb79eec57..89034354d5482a89d7b08888ed70c25b873eaec3 100644 --- a/functions/synu.fish +++ b/functions/synu.fish @@ -131,6 +131,13 @@ function synu --description "Universal agent wrapper with Synthetic API quota tr command $agent $extra_args $agent_args set -l exit_status $status + # Clean up environment variables set by agent + if functions -q _synu_agent_{$agent}_env_vars + for var in (_synu_agent_{$agent}_env_vars) + set -e $var + end + end + # Fetch quota after agent execution set -l quota_after (_synu_get_quota) if test $status -ne 0