From 6cd0dee9374a6d48f9aed5929f69e9fc5b4770a7 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Mon, 22 Dec 2025 06:16:34 -0800 Subject: [PATCH 1/8] Enhance PATH setup scripts for complete cycod ecosystem - Update cycodpath.cmd and setup-debug-path.sh to include all 9 executables: - Main apps: cycod, cycodmd, cycodt, cycodgr - MCP servers: geolocation, mxlookup, osm, weather, whois - Move cycodpath.cmd to scripts/ folder for better organization - Update scripts/README.md to document both Windows and Unix activation scripts - Provides complete PATH coverage for development across worktrees --- scripts/README.md | 25 ++++++- scripts/cycodpath.cmd | 137 ++++++++++++++++++++++++++++++++++++ scripts/setup-debug-path.sh | 10 ++- 3 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 scripts/cycodpath.cmd diff --git a/scripts/README.md b/scripts/README.md index e7bed9ad..b6d3c52d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -22,14 +22,33 @@ Sets up the development environment to prioritize debug builds over globally ins ./scripts/setup-debug-path.sh --help ``` -### What it does +## cycodpath.cmd -- Adds debug binary paths to the front of PATH: +Windows equivalent of setup-debug-path.sh. Sets up PATH for current session to prioritize debug builds. + +### Usage + +```cmd +REM Setup PATH for current session +scripts\cycodpath.cmd +``` + +### What they do + +- Add debug binary paths to the front of PATH: - `src/cycod/bin/Debug/net9.0` - `src/cycodmd/bin/Debug/net9.0` - `src/cycodt/bin/Debug/net9.0` -- When you run `cycod`, `cycodmd`, or `cycodt`, it will use debug versions if built + - `src/cycodgr/bin/Debug/net9.0` + - `src/mcp/geolocation/bin/Debug/net9.0` + - `src/mcp/mxlookup/bin/Debug/net9.0` + - `src/mcp/osm/bin/Debug/net9.0` + - `src/mcp/weather/bin/Debug/net9.0` + - `src/mcp/whois/bin/Debug/net9.0` +- When you run any cycod tool, it will use debug versions if built - Falls back to global dotnet tool versions if debug versions don't exist +- **setup-debug-path.sh** can persist changes to ~/.bashrc for permanent setup +- **cycodpath.cmd** sets up PATH for current Windows session only - Automatically runs in codespaces via `postCreateCommand` in devcontainer.json ### Integration with .bashrc diff --git a/scripts/cycodpath.cmd b/scripts/cycodpath.cmd new file mode 100644 index 00000000..57064f34 --- /dev/null +++ b/scripts/cycodpath.cmd @@ -0,0 +1,137 @@ +@echo off + +REM ===== CYCODPATH.CMD ===== +REM Finds all cycod executables (main apps and MCP servers) and adds their folders to PATH +REM Prioritizes Debug over Release, newest timestamp as fallback + +echo Finding all cycod executables (main apps and MCP servers)... +echo. + +REM Initialize variables +set "folders_to_add=" +set "found_cycod=" +set "found_cycodt=" +set "found_cycodmd=" +set "found_cycodgr=" +set "found_cycod-mcp-geolocation=" +set "found_cycod-mcp-mxlookup=" +set "found_cycod-mcp-osm=" +set "found_cycod-mcp-weather=" +set "found_cycod-mcp-whois=" +set "folder_count=0" + +REM ===== MAIN SEARCH LOOP ===== +for %%e in (cycod cycodt cycodmd cycodgr cycod-mcp-geolocation cycod-mcp-mxlookup cycod-mcp-osm cycod-mcp-weather cycod-mcp-whois) do ( + call :FindExecutable %%e +) + +REM ===== UPDATE PATH ===== +call :UpdatePath + +REM ===== SUMMARY ===== +call :ShowSummary + +goto :EOF + +REM ===== SUBROUTINES ===== + +:FindExecutable +set "exe_name=%1" +set "found_path=" + +REM Try direct paths first - Debug priority +if exist "bin\Debug\net9.0\%exe_name%.exe" ( + set "found_path=bin\Debug\net9.0\%exe_name%.exe" + goto :FoundDirect +) + +if exist "bin\Release\net9.0\%exe_name%.exe" ( + set "found_path=bin\Release\net9.0\%exe_name%.exe" + goto :FoundDirect +) + +REM Try other common debug/release patterns +for %%p in ("bin\Debug\%exe_name%.exe" "bin\Release\%exe_name%.exe" "bin\x64\Debug\%exe_name%.exe" "bin\x64\Release\%exe_name%.exe") do ( + if exist %%p ( + set "found_path=%%~p" + goto :FoundDirect + ) +) + +REM Fallback: Use forfiles to find newest by timestamp +set "temp_file=%TEMP%\cycodpath_temp_%RANDOM%.txt" +forfiles /m "%exe_name%.exe" /s /c "cmd /c echo @path" 2>nul > "%temp_file%" + +if exist "%temp_file%" ( + for /f "usebackq tokens=*" %%f in ("%temp_file%") do ( + set "found_path=%%~f" + goto :FoundFallback + ) +) + +del /q "%temp_file%" 2>nul +echo %exe_name%.exe not found +set "found_%exe_name%=" +goto :EOF + +:FoundDirect +echo Found %exe_name%.exe: %CD%\%found_path% +for %%d in ("%found_path%") do set "dir_path=%%~dpd" +set "dir_path=%dir_path:~0,-1%" +call :AddFolder "%CD%\%dir_path%" +set "found_%exe_name%=YES" +goto :EOF + +:FoundFallback +echo Found %exe_name%.exe: %found_path% +for %%d in ("%found_path%") do set "dir_path=%%~dpd" +set "dir_path=%dir_path:~0,-1%" +call :AddFolder "%dir_path%" +set "found_%exe_name%=YES" +del /q "%temp_file%" 2>nul +goto :EOF + +:AddFolder +set "new_folder=%~1" +REM Check if folder already in our list +echo %folders_to_add% | findstr /C:"%new_folder%" >nul +if errorlevel 1 ( + if defined folders_to_add ( + set "folders_to_add=%folders_to_add%;%new_folder%" + ) else ( + set "folders_to_add=%new_folder%" + ) + echo -> Added %new_folder% to PATH + set /a folder_count+=1 +) +goto :EOF + +:UpdatePath +if defined folders_to_add ( + set "PATH=%folders_to_add%;%PATH%" + echo PATH updated. +) else ( + echo No folders to add to PATH. +) +goto :EOF + +:ShowSummary +echo. +set "summary=Summary: " +if defined found_cycod (set "summary=%summary%cycod YES") else (set "summary=%summary%cycod NO") +if defined found_cycodt (set "summary=%summary%, cycodt YES") else (set "summary=%summary%, cycodt NO") +if defined found_cycodmd (set "summary=%summary%, cycodmd YES") else (set "summary=%summary%, cycodmd NO") +if defined found_cycodgr (set "summary=%summary%, cycodgr YES") else (set "summary=%summary%, cycodgr NO") +if defined found_cycod-mcp-geolocation (set "summary=%summary%, mcp-geo YES") else (set "summary=%summary%, mcp-geo NO") +if defined found_cycod-mcp-mxlookup (set "summary=%summary%, mcp-mx YES") else (set "summary=%summary%, mcp-mx NO") +if defined found_cycod-mcp-osm (set "summary=%summary%, mcp-osm YES") else (set "summary=%summary%, mcp-osm NO") +if defined found_cycod-mcp-weather (set "summary=%summary%, mcp-weather YES") else (set "summary=%summary%, mcp-weather NO") +if defined found_cycod-mcp-whois (set "summary=%summary%, mcp-whois YES") else (set "summary=%summary%, mcp-whois NO") + +echo %summary% +if %folder_count% gtr 0 ( + echo PATH updated with %folder_count% new entries. +) else ( + echo No executables found - PATH unchanged. +) +goto :EOF \ No newline at end of file diff --git a/scripts/setup-debug-path.sh b/scripts/setup-debug-path.sh index aae28065..1774cd31 100755 --- a/scripts/setup-debug-path.sh +++ b/scripts/setup-debug-path.sh @@ -1,7 +1,7 @@ #!/bin/bash # Cycod Debug Environment Setup Script -# This script sets up the PATH to prioritize debug builds over global dotnet tools +# This script sets up the PATH to prioritize debug builds of all cycod tools over global dotnet tools set -e @@ -13,6 +13,12 @@ DEBUG_PATHS=( "$REPO_ROOT/src/cycod/bin/Debug/net9.0" "$REPO_ROOT/src/cycodmd/bin/Debug/net9.0" "$REPO_ROOT/src/cycodt/bin/Debug/net9.0" + "$REPO_ROOT/src/cycodgr/bin/Debug/net9.0" + "$REPO_ROOT/src/mcp/geolocation/bin/Debug/net9.0" + "$REPO_ROOT/src/mcp/mxlookup/bin/Debug/net9.0" + "$REPO_ROOT/src/mcp/osm/bin/Debug/net9.0" + "$REPO_ROOT/src/mcp/weather/bin/Debug/net9.0" + "$REPO_ROOT/src/mcp/whois/bin/Debug/net9.0" ) # Function to check if PATH already contains our debug paths @@ -71,7 +77,7 @@ test_setup() { echo "๐Ÿงช Testing debug environment setup:" echo "==================================" - for tool in cycod cycodmd cycodt; do + for tool in cycod cycodmd cycodt cycodgr cycod-mcp-geolocation cycod-mcp-mxlookup cycod-mcp-osm cycod-mcp-weather cycod-mcp-whois; do local tool_path=$(which "$tool" 2>/dev/null || echo "not found") echo " $tool: $tool_path" From b719a76488738e098fa1093f063fd40caa0a9643 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Mon, 22 Dec 2025 07:05:58 -0800 Subject: [PATCH 2/8] feat: Add wrapper scripts for environment activation in multiple shells --- here.cmd | 11 ++++ here.ps1 | 19 +++++++ here.sh | 15 ++++++ scripts/here-impl.cmd | 70 +++++++++++++++++++++++++ scripts/here-impl.ps1 | 102 ++++++++++++++++++++++++++++++++++++ scripts/here-impl.sh | 118 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 335 insertions(+) create mode 100644 here.cmd create mode 100644 here.ps1 create mode 100644 here.sh create mode 100644 scripts/here-impl.cmd create mode 100644 scripts/here-impl.ps1 create mode 100644 scripts/here-impl.sh diff --git a/here.cmd b/here.cmd new file mode 100644 index 00000000..214422ee --- /dev/null +++ b/here.cmd @@ -0,0 +1,11 @@ +@echo off +REM ===== HERE.CMD WRAPPER ===== +REM Thin wrapper that calls the real implementation in scripts/ + +if not exist "scripts\here-impl.cmd" ( + echo Error: scripts\here-impl.cmd not found + echo Make sure you're running this from the repository root. + exit /b 1 +) + +call scripts\here-impl.cmd %* \ No newline at end of file diff --git a/here.ps1 b/here.ps1 new file mode 100644 index 00000000..2b913f76 --- /dev/null +++ b/here.ps1 @@ -0,0 +1,19 @@ +# ===== HERE.PS1 WRAPPER ===== +# Thin wrapper that calls the real implementation in scripts/ + +param( + [Parameter(ValueFromRemainingArguments)] + [string[]]$Arguments +) + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ImplScript = Join-Path $ScriptDir "scripts\here-impl.ps1" + +if (-not (Test-Path $ImplScript)) { + Write-Error "Error: scripts\here-impl.ps1 not found" + Write-Error "Make sure you're running this from the repository root." + exit 1 +} + +# Pass through all arguments to the implementation +& $ImplScript @Arguments \ No newline at end of file diff --git a/here.sh b/here.sh new file mode 100644 index 00000000..202c009a --- /dev/null +++ b/here.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# ===== HERE.SH WRAPPER ===== +# Thin wrapper that calls the real implementation in scripts/ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IMPL_SCRIPT="$SCRIPT_DIR/scripts/here-impl.sh" + +if [[ ! -f "$IMPL_SCRIPT" ]]; then + echo "Error: scripts/here-impl.sh not found" + echo "Make sure you're running this from the repository root." + exit 1 +fi + +# Pass through all arguments to the implementation +exec "$IMPL_SCRIPT" "$@" \ No newline at end of file diff --git a/scripts/here-impl.cmd b/scripts/here-impl.cmd new file mode 100644 index 00000000..9c13cd74 --- /dev/null +++ b/scripts/here-impl.cmd @@ -0,0 +1,70 @@ +@echo off + +REM ===== HERE-IMPL.CMD ===== +REM Environment activation script for cycod development + +set "REPO_ROOT=%~dp0\.." +set "MODE=light" +set "STAY_FLAG=" + +REM Parse arguments +if "%~1"=="--shell" set "MODE=heavy" +if "%~1"=="-s" set "MODE=heavy" +if "%~1"=="shell" set "MODE=heavy" +if "%~2"=="--stay" set "STAY_FLAG=yes" +if "%~1"=="--help" goto :show_usage +if "%~1"=="-h" goto :show_usage + +echo. +echo Cycod Environment Activation +echo ============================ + +if "%MODE%"=="light" goto :light_mode +if "%MODE%"=="heavy" goto :heavy_mode + +:light_mode +echo Light mode: Setting up environment in current shell... +echo. + +call "%REPO_ROOT%\scripts\cycodpath.cmd" + +set "CYCOD_DEV_MODE=1" +set "CYCOD_REPO_ROOT=%REPO_ROOT%" + +echo. +echo Environment activated in current shell! +echo Tip: Use 'here.cmd --shell' to launch new environment shell +goto :end + +:heavy_mode +echo Heavy mode: Launching new environment shell... +echo. + +set "CYCOD_DEV_MODE=1" +set "CYCOD_REPO_ROOT=%REPO_ROOT%" + +if not defined STAY_FLAG ( + echo Changing to repository root... + cd /d "%REPO_ROOT%" +) + +echo Starting new shell with cycod environment... +echo Type 'exit' to return to previous environment. +echo. + +cmd /k "call "%REPO_ROOT%\scripts\cycodpath.cmd" >nul & prompt (here:cycod) $P$G & echo Environment shell ready!" + +goto :end + +:show_usage +echo. +echo Usage: here.cmd [options] +echo. +echo Options: +echo (no options) Light mode: Set up environment in current shell +echo --shell, -s Heavy mode: Launch new shell with full environment +echo --stay (with --shell) Stay in current directory +echo --help, -h Show this help +goto :end + +:end \ No newline at end of file diff --git a/scripts/here-impl.ps1 b/scripts/here-impl.ps1 new file mode 100644 index 00000000..b49410fa --- /dev/null +++ b/scripts/here-impl.ps1 @@ -0,0 +1,102 @@ +# ===== HERE-IMPL.PS1 ===== +# Environment activation script for cycod development + +param( + [switch]$Shell, + [switch]$S, + [switch]$Stay, + [switch]$Help, + [switch]$H, + [Parameter(ValueFromRemainingArguments)] + [string[]]$RemainingArgs +) + +# Get the repository root directory +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir + +# Parse arguments +$Mode = "light" +$StayFlag = $Stay +$ShowHelp = $Help -or $H + +if ($Shell -or $S -or ($RemainingArgs -contains "shell")) { + $Mode = "heavy" +} + +function Show-Usage { + Write-Host "" + Write-Host "Usage: here.ps1 [options]" + Write-Host "" + Write-Host "Options:" + Write-Host " (no options) Light mode: Set up environment in current shell" + Write-Host " -Shell, -S Heavy mode: Launch new shell with full environment" + Write-Host " -Stay (with -Shell) Stay in current directory" + Write-Host " -Help, -H Show this help" + Write-Host "" + Write-Host "Examples:" + Write-Host " .\here.ps1 Light: Quick environment setup" + Write-Host " .\here.ps1 -Shell Heavy: New shell at repo root" + Write-Host " .\here.ps1 -Shell -Stay Heavy: New shell at current location" +} + +if ($ShowHelp) { + Show-Usage + exit +} + +Write-Host "" +Write-Host "Cycod Environment Activation" -ForegroundColor Cyan +Write-Host "============================" -ForegroundColor Cyan + +if ($Mode -eq "light") { + Write-Host "Light mode: Setting up environment in current shell..." -ForegroundColor Yellow + Write-Host "" + + # Call the existing PATH setup script + $CycodPathScript = Join-Path $RepoRoot "scripts\cycodpath.cmd" + if (Test-Path $CycodPathScript) { + cmd /c """$CycodPathScript""" + } + + # Set additional environment variables + $env:CYCOD_DEV_MODE = "1" + $env:CYCOD_REPO_ROOT = $RepoRoot + + Write-Host "" + Write-Host "Environment activated!" -ForegroundColor Green + Write-Host "Tip: Use './here.ps1 -Shell' to launch new environment shell" -ForegroundColor Yellow + +} elseif ($Mode -eq "heavy") { + Write-Host "Heavy mode: Launching new environment shell..." -ForegroundColor Yellow + Write-Host "" + + # Set environment variables + $env:CYCOD_DEV_MODE = "1" + $env:CYCOD_REPO_ROOT = $RepoRoot + + # Determine target directory + if (-not $StayFlag) { + $TargetDir = $RepoRoot + Write-Host "Changing to repository root..." -ForegroundColor Yellow + } else { + $TargetDir = Get-Location + Write-Host "Staying in current directory..." -ForegroundColor Yellow + } + + Write-Host "Starting new PowerShell with cycod environment..." -ForegroundColor Yellow + Write-Host "Type 'exit' to return to previous environment." -ForegroundColor Yellow + Write-Host "" + + # Launch new PowerShell with custom prompt and setup + $setupCommands = @" +`$env:CYCOD_DEV_MODE='1' +`$env:CYCOD_REPO_ROOT='$RepoRoot' +function prompt { "(here:cycod) PS " + (Get-Location) + "> " } +Set-Location '$TargetDir' +cmd /c '$CycodPathScript' | Out-Null +Write-Host 'Environment shell ready!' -ForegroundColor Green +"@ + + powershell -NoExit -Command $setupCommands +} \ No newline at end of file diff --git a/scripts/here-impl.sh b/scripts/here-impl.sh new file mode 100644 index 00000000..16662118 --- /dev/null +++ b/scripts/here-impl.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# ===== HERE-IMPL.SH ===== +# Environment activation script for cycod development +# Light mode: Sets up PATH and environment in current shell +# Heavy mode: Launches new shell with full environment setup + +set -e + +# Get the repository root directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Default values +MODE="light" +STAY_FLAG="" +SHOW_HELP="" + +# ===== PARSE ARGUMENTS ===== +while [[ $# -gt 0 ]]; do + case $1 in + --shell|-s|shell) + MODE="heavy" + shift + ;; + --stay) + STAY_FLAG="yes" + shift + ;; + --help|-h) + SHOW_HELP="yes" + shift + ;; + *) + echo "Unknown argument: $1" + show_usage + exit 1 + ;; + esac +done + +show_usage() { + cat << EOF + +Usage: here.sh [options] + +Options: + (no options) Light mode: Set up environment in current shell + --shell, -s Heavy mode: Launch new shell with full environment + --stay (with --shell) Stay in current directory + --help, -h Show this help + +Examples: + ./here.sh Light: Quick environment setup + ./here.sh --shell Heavy: New shell at repo root + ./here.sh --shell --stay Heavy: New shell at current location + +Note: For light mode to affect your current shell, source this script: + source ./here.sh Light mode in current shell +EOF +} + +if [[ -n "$SHOW_HELP" ]]; then + show_usage + exit 0 +fi + +echo "" +echo "๐Ÿ”ง Cycod Environment Activation" +echo "==============================" + +if [[ "$MODE" == "light" ]]; then + echo "Light mode: Setting up environment..." + echo "" + + # Call the existing PATH setup script + source "$REPO_ROOT/scripts/setup-debug-path.sh" --session-only --no-test + + # Set additional environment variables + export CYCOD_DEV_MODE=1 + export CYCOD_REPO_ROOT="$REPO_ROOT" + + echo "" + echo "โœ… Environment activated!" + echo "๐Ÿ’ก Tip: Use './here.sh --shell' to launch new environment shell at repo root" + echo "๐Ÿ’ก Note: To affect current shell, use: source ./here.sh" + +elif [[ "$MODE" == "heavy" ]]; then + echo "Heavy mode: Launching new environment shell..." + echo "" + + # Prepare environment for new shell + export CYCOD_DEV_MODE=1 + export CYCOD_REPO_ROOT="$REPO_ROOT" + + # Set up PATH + source "$REPO_ROOT/scripts/setup-debug-path.sh" --session-only --no-test >/dev/null 2>&1 + + # Prepare custom prompt + CUSTOM_PS1="(here:cycod) \u@\h:\w\$ " + + # Determine target directory + if [[ -z "$STAY_FLAG" ]]; then + TARGET_DIR="$REPO_ROOT" + echo "Changing to repository root..." + else + TARGET_DIR="$(pwd)" + echo "Staying in current directory..." + fi + + echo "Starting new shell with cycod environment..." + echo "Type 'exit' to return to previous environment." + echo "" + + # Launch new bash with custom environment + cd "$TARGET_DIR" + PS1="$CUSTOM_PS1" bash --rcfile <(cat ~/.bashrc 2>/dev/null || echo ""; echo "echo 'โœ… Cycod environment shell ready!'; echo") +fi \ No newline at end of file From ebd4513c51555114aa36588f576de7000b2cf955 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Mon, 22 Dec 2025 07:08:50 -0800 Subject: [PATCH 3/8] updated --- here.cmd | 12 +----------- here.ps1 | 20 +------------------- here.sh | 15 +-------------- 3 files changed, 3 insertions(+), 44 deletions(-) diff --git a/here.cmd b/here.cmd index 214422ee..f6752536 100644 --- a/here.cmd +++ b/here.cmd @@ -1,11 +1 @@ -@echo off -REM ===== HERE.CMD WRAPPER ===== -REM Thin wrapper that calls the real implementation in scripts/ - -if not exist "scripts\here-impl.cmd" ( - echo Error: scripts\here-impl.cmd not found - echo Make sure you're running this from the repository root. - exit /b 1 -) - -call scripts\here-impl.cmd %* \ No newline at end of file +@call scripts\here-impl.cmd %* \ No newline at end of file diff --git a/here.ps1 b/here.ps1 index 2b913f76..336b7c7c 100644 --- a/here.ps1 +++ b/here.ps1 @@ -1,19 +1 @@ -# ===== HERE.PS1 WRAPPER ===== -# Thin wrapper that calls the real implementation in scripts/ - -param( - [Parameter(ValueFromRemainingArguments)] - [string[]]$Arguments -) - -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$ImplScript = Join-Path $ScriptDir "scripts\here-impl.ps1" - -if (-not (Test-Path $ImplScript)) { - Write-Error "Error: scripts\here-impl.ps1 not found" - Write-Error "Make sure you're running this from the repository root." - exit 1 -} - -# Pass through all arguments to the implementation -& $ImplScript @Arguments \ No newline at end of file +& "$PSScriptRoot\scripts\here-impl.ps1" @args \ No newline at end of file diff --git a/here.sh b/here.sh index 202c009a..80505743 100644 --- a/here.sh +++ b/here.sh @@ -1,15 +1,2 @@ #!/bin/bash -# ===== HERE.SH WRAPPER ===== -# Thin wrapper that calls the real implementation in scripts/ - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -IMPL_SCRIPT="$SCRIPT_DIR/scripts/here-impl.sh" - -if [[ ! -f "$IMPL_SCRIPT" ]]; then - echo "Error: scripts/here-impl.sh not found" - echo "Make sure you're running this from the repository root." - exit 1 -fi - -# Pass through all arguments to the implementation -exec "$IMPL_SCRIPT" "$@" \ No newline at end of file +exec "$(dirname "$0")/scripts/here-impl.sh" "$@" \ No newline at end of file From 27b1bbdd4edd6794cbf57fa34654a1ed66789088 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Tue, 23 Dec 2025 07:28:27 -0800 Subject: [PATCH 4/8] first draft of imagine command --- .../CommandLine/CycoDevCommandLineOptions.cs | 111 ++++++ .../CommandLineCommands/ImagineCommand.cs | 347 ++++++++++++++++++ src/cycod/assets/help/imagine examples.txt | 137 +++++++ src/cycod/assets/help/imagine.txt | 94 +++++ src/cycod/assets/help/usage.txt | 3 + 5 files changed, 692 insertions(+) create mode 100644 src/cycod/CommandLineCommands/ImagineCommand.cs create mode 100644 src/cycod/assets/help/imagine examples.txt create mode 100644 src/cycod/assets/help/imagine.txt diff --git a/src/cycod/CommandLine/CycoDevCommandLineOptions.cs b/src/cycod/CommandLine/CycoDevCommandLineOptions.cs index 4cb2d487..e722459b 100644 --- a/src/cycod/CommandLine/CycoDevCommandLineOptions.cs +++ b/src/cycod/CommandLine/CycoDevCommandLineOptions.cs @@ -35,6 +35,7 @@ override protected string PeekCommandName(string[] args, int i) return name1 switch { "chat" => "chat", + "imagine" => "imagine", _ => base.PeekCommandName(args, i) }; } @@ -58,6 +59,7 @@ override protected bool CheckPartialCommandNeedsHelp(string commandName) return commandName switch { "chat" => new ChatCommand(), + "imagine" => new ImagineCommand(), "github login" => new GitHubLoginCommand(), "github models" => new GitHubModelsCommand(), "config list" => new ConfigListCommand(), @@ -85,6 +87,7 @@ override protected bool CheckPartialCommandNeedsHelp(string commandName) override protected bool TryParseOtherCommandOptions(Command? command, string[] args, ref int i, string arg) { return TryParseChatCommandOptions(command as ChatCommand, args, ref i, arg) || + TryParseImagineCommandOptions(command as ImagineCommand, args, ref i, arg) || TryParseGitHubLoginCommandOptions(command as GitHubLoginCommand, args, ref i, arg) || TryParseConfigCommandOptions(command as ConfigBaseCommand, args, ref i, arg) || TryParseAliasCommandOptions(command as AliasBaseCommand, args, ref i, arg) || @@ -205,6 +208,12 @@ override protected bool TryParseOtherCommandArg(Command? command, string arg) mcpRemoveCommand.Name = arg; parsedOption = true; } + else if (command is ImagineCommand imagineCommand) + { + // Add positional arguments as prompts + imagineCommand.Prompts.Add(arg); + parsedOption = true; + } return parsedOption; } @@ -724,6 +733,108 @@ private bool TryParseGitHubLoginCommandOptions(GitHubLoginCommand? command, stri return parsed; } + private bool TryParseImagineCommandOptions(ImagineCommand? command, string[] args, ref int i, string arg) + { + bool parsed = true; + + if (command == null) + { + parsed = false; + } + else if (arg == "--input" || arg == "-i") + { + var maxArgs = GetInputOptionArgs(i + 1, args, max: 1); + var prompt = maxArgs.FirstOrDefault(); + if (!string.IsNullOrEmpty(prompt)) + { + command.Prompts.Add(prompt); + } + i += maxArgs.Count(); + } + else if (arg == "--inputs") + { + var inputs = GetInputOptionArgs(i + 1, args); + command.Prompts.AddRange(inputs); + i += inputs.Count(); + } + else if (arg == "--count" || arg == "-c") + { + var maxArgs = GetInputOptionArgs(i + 1, args, max: 1); + var countStr = maxArgs.FirstOrDefault(); + if (!string.IsNullOrEmpty(countStr) && int.TryParse(countStr, out var count)) + { + command.Count = Math.Max(1, Math.Min(10, count)); // Limit to 1-10 images + } + i += maxArgs.Count(); + } + else if (arg == "--size" || arg == "-s") + { + var maxArgs = GetInputOptionArgs(i + 1, args, max: 1); + var size = maxArgs.FirstOrDefault(); + if (!string.IsNullOrEmpty(size)) + { + command.Size = size; + } + i += maxArgs.Count(); + } + else if (arg == "--style") + { + var maxArgs = GetInputOptionArgs(i + 1, args, max: 1); + var style = maxArgs.FirstOrDefault(); + if (!string.IsNullOrEmpty(style)) + { + command.Style = style; + } + i += maxArgs.Count(); + } + else if (arg == "--quality" || arg == "-q") + { + var maxArgs = GetInputOptionArgs(i + 1, args, max: 1); + var quality = maxArgs.FirstOrDefault(); + if (!string.IsNullOrEmpty(quality)) + { + command.Quality = quality; + } + i += maxArgs.Count(); + } + else if (arg == "--output" || arg == "-o") + { + var maxArgs = GetInputOptionArgs(i + 1, args, max: 1); + var output = maxArgs.FirstOrDefault(); + if (!string.IsNullOrEmpty(output)) + { + command.OutputDirectory = output; + } + i += maxArgs.Count(); + } + else if (arg == "--format" || arg == "-f") + { + var maxArgs = GetInputOptionArgs(i + 1, args, max: 1); + var format = maxArgs.FirstOrDefault(); + if (!string.IsNullOrEmpty(format)) + { + command.Format = format; + } + i += maxArgs.Count(); + } + else if (arg == "--provider" || arg == "-p") + { + var maxArgs = GetInputOptionArgs(i + 1, args, max: 1); + var provider = maxArgs.FirstOrDefault(); + if (!string.IsNullOrEmpty(provider)) + { + command.Provider = provider; + } + i += maxArgs.Count(); + } + else + { + parsed = false; + } + + return parsed; + } + private const string DefaultSimpleChatHistoryFileName = "chat-history.jsonl"; private const string DefaultOutputChatHistoryFileNameTemplate = "chat-history-{time}.jsonl"; diff --git a/src/cycod/CommandLineCommands/ImagineCommand.cs b/src/cycod/CommandLineCommands/ImagineCommand.cs new file mode 100644 index 00000000..2cc8b949 --- /dev/null +++ b/src/cycod/CommandLineCommands/ImagineCommand.cs @@ -0,0 +1,347 @@ +#pragma warning disable MEAI001 // Microsoft.Extensions.AI types are experimental + +using Microsoft.Extensions.AI; +using Azure; + +public class ImagineCommand : CommandWithVariables +{ + public List Prompts { get; set; } = []; + public int Count { get; set; } = 1; + public string Size { get; set; } = "1024x1024"; + public string Style { get; set; } = "vivid"; + public string Quality { get; set; } = "standard"; + public string OutputDirectory { get; set; } = "."; + public string Format { get; set; } = "png"; + public string? Provider { get; set; } = null; + + public override string GetCommandName() + { + return "imagine"; + } + + public override bool IsEmpty() + { + return Prompts.Count == 0; + } + + public override CommandWithVariables Clone() + { + var clone = new ImagineCommand + { + Prompts = new List(this.Prompts), + Count = this.Count, + Size = this.Size, + Style = this.Style, + Quality = this.Quality, + OutputDirectory = this.OutputDirectory, + Format = this.Format, + Provider = this.Provider, + Variables = new Dictionary(this.Variables), + ForEachVariables = new List(this.ForEachVariables) + }; + return clone; + } + + public override async Task ExecuteAsync(bool interactive) + { + try + { + if (Prompts.Count == 0) + { + ConsoleHelpers.WriteErrorLine("No prompts provided. Use --input or provide prompts as arguments."); + return 1; + } + + ConsoleHelpers.WriteLine($"๐ŸŽจ Imagining {Prompts.Count} image{(Prompts.Count == 1 ? "" : "s")}...\n", ConsoleColor.Cyan); + + // Create a working image generator (using the pattern from ImageAIExample) + var imageGenerator = CreateWorkingImageGenerator(); + + // Ensure output directory exists + if (!Directory.Exists(OutputDirectory)) + { + Directory.CreateDirectory(OutputDirectory); + ConsoleHelpers.WriteDebugLine($"Created output directory: {OutputDirectory}"); + } + + int totalGenerated = 0; + foreach (var prompt in Prompts) + { + ConsoleHelpers.WriteLine($"๐Ÿ’ญ Generating: \"{prompt}\""); + + var options = new ImageGenerationOptions + { + Count = Count, + ModelId = "dall-e-3" + }; + + var response = await imageGenerator.GenerateAsync(new ImageGenerationRequest(prompt), options); + + totalGenerated += await SaveGeneratedImages(response, prompt, totalGenerated); + } + + ConsoleHelpers.WriteLine($"\nโœ… Successfully generated {totalGenerated} image{(totalGenerated == 1 ? "" : "s")}!", ConsoleColor.Green); + return 0; + } + catch (Exception ex) + { + ConsoleHelpers.WriteErrorLine($"Error generating images: {ex.Message}"); + Logger.Error($"Image generation failed: {ex}"); + return 1; + } + } + + private IImageGenerator CreateWorkingImageGenerator() + { + // Try Azure OpenAI first (following ChatClientFactory pattern) + var azureApiKey = EnvironmentHelpers.FindEnvVar("AZURE_OPENAI_API_KEY"); + var azureEndpoint = EnvironmentHelpers.FindEnvVar("AZURE_OPENAI_ENDPOINT"); + + if (!string.IsNullOrEmpty(azureApiKey) && !string.IsNullOrEmpty(azureEndpoint)) + { + return CreateAzureOpenAIImageGenerator(azureEndpoint, azureApiKey); + } + + // Fall back to standard OpenAI + var openAIApiKey = EnvironmentHelpers.FindEnvVar("OPENAI_API_KEY"); + if (!string.IsNullOrEmpty(openAIApiKey)) + { + return CreateStandardOpenAIImageGenerator(openAIApiKey); + } + + throw new EnvVarSettingException("Either AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT or OPENAI_API_KEY environment variables are required for image generation"); + } + + private IImageGenerator CreateAzureOpenAIImageGenerator(string endpoint, string apiKey) + { + try + { + ConsoleHelpers.WriteDebugLine("Creating Azure OpenAI image generator"); + + // Use the DALL-E deployment we just created + var deploymentName = "dall-e-3"; + + // Create Azure OpenAI client the same way ChatClientFactory does it + var client = new Azure.AI.OpenAI.AzureOpenAIClient(new Uri(endpoint), new Azure.AzureKeyCredential(apiKey)); + var imageClient = client.GetImageClient(deploymentName); + + // Wrap it in our adapter + return new OpenAIImageGeneratorWrapper(imageClient); + } + catch (Exception ex) + { + ConsoleHelpers.WriteErrorLine($"Failed to create Azure OpenAI image generator: {ex.Message}"); + throw; + } + } + + private IImageGenerator CreateStandardOpenAIImageGenerator(string apiKey) + { + try + { + ConsoleHelpers.WriteDebugLine("Creating standard OpenAI image generator"); + + var model = "dall-e-3"; + var endpoint = EnvironmentHelpers.FindEnvVar("OPENAI_ENDPOINT"); // Optional custom endpoint + + // Create OpenAI client + var options = new OpenAI.OpenAIClientOptions(); + if (!string.IsNullOrEmpty(endpoint)) + { + options.Endpoint = new Uri(endpoint); + } + + var openAIClient = new OpenAI.OpenAIClient(new System.ClientModel.ApiKeyCredential(apiKey), options); + var imageClient = openAIClient.GetImageClient(model); + + return new OpenAIImageGeneratorWrapper(imageClient); + } + catch (Exception ex) + { + ConsoleHelpers.WriteErrorLine($"Failed to create standard OpenAI image generator: {ex.Message}"); + throw; + } + } + + private IImageGenerator CreateMockImageGenerator() + { + // Use the pattern from ImageAIExample that actually works + return new TestImageGenerator + { + GenerateImagesAsyncCallback = (request, options, ct) => + { + // Create a more realistic mock PNG image + var mockImageBytes = CreateMockImageData(request.Prompt ?? "unknown"); + var content = new DataContent(mockImageBytes, "image/png") { Name = "generated_image.png" }; + var response = new ImageGenerationResponse([content]); + return Task.FromResult(response); + } + }; + } + + private async Task SaveGeneratedImages(ImageGenerationResponse response, string prompt, int startIndex) + { + int saved = 0; + + for (int i = 0; i < response.Contents.Count; i++) + { + var content = response.Contents[i]; + + if (content is DataContent dataContent) + { + // Save binary image data + var fileName = GenerateFileName(prompt, startIndex + i + 1); + var filePath = Path.Combine(OutputDirectory, fileName); + + await File.WriteAllBytesAsync(filePath, dataContent.Data.ToArray()); + ConsoleHelpers.WriteLine($"๐Ÿ’พ Saved: {fileName}"); + saved++; + } + else if (content is UriContent uriContent) + { + // Download and save from URL + using var httpClient = new HttpClient(); + var imageData = await httpClient.GetByteArrayAsync(uriContent.Uri); + + var fileName = GenerateFileName(prompt, startIndex + i + 1); + var filePath = Path.Combine(OutputDirectory, fileName); + + await File.WriteAllBytesAsync(filePath, imageData); + ConsoleHelpers.WriteLine($"๐Ÿ’พ Saved: {fileName}"); + saved++; + } + else + { + ConsoleHelpers.WriteWarning($"Unexpected content type: {content.GetType().Name}"); + } + } + + return saved; + } + + private string GenerateFileName(string prompt, int index) + { + // Create a safe filename from the prompt + var safePrompt = string.Concat(prompt + .Take(20) + .Where(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c))) + .Replace(' ', '_'); + + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + return $"{safePrompt}_{timestamp}_{index:D2}.{Format}"; + } + + private byte[] CreateMockImageData(string prompt) + { + // Create a simple but valid PNG file + // This is a 1x1 transparent PNG with the prompt as metadata + var pngHeader = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; // PNG signature + var ihdrChunk = new byte[] { + 0x00, 0x00, 0x00, 0x0D, // IHDR chunk length + 0x49, 0x48, 0x44, 0x52, // IHDR + 0x00, 0x00, 0x00, 0x01, // Width: 1 + 0x00, 0x00, 0x00, 0x01, // Height: 1 + 0x08, 0x06, 0x00, 0x00, 0x00, // Bit depth: 8, Color type: 6 (RGBA), Compression, Filter, Interlace + 0x1F, 0x15, 0xC4, 0x89 // CRC + }; + var idatChunk = new byte[] { + 0x00, 0x00, 0x00, 0x0B, // IDAT chunk length + 0x49, 0x44, 0x41, 0x54, // IDAT + 0x78, 0x9C, 0x62, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4 // Compressed data + CRC + }; + var iendChunk = new byte[] { + 0x00, 0x00, 0x00, 0x00, // IEND chunk length + 0x49, 0x45, 0x4E, 0x44, // IEND + 0xAE, 0x42, 0x60, 0x82 // CRC + }; + + var result = new List(); + result.AddRange(pngHeader); + result.AddRange(ihdrChunk); + result.AddRange(idatChunk); + result.AddRange(iendChunk); + + return result.ToArray(); + } +} + +// Test implementation (from ImageAIExample working code) +public sealed class TestImageGenerator : IImageGenerator +{ + public Func>? GenerateImagesAsyncCallback { get; set; } + + public Task GenerateAsync(ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + return GenerateImagesAsyncCallback?.Invoke(request, options, cancellationToken) ?? + Task.FromResult(new ImageGenerationResponse()); + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + return serviceType.IsInstanceOfType(this) ? this : null; + } + + public void Dispose() { } +} + +// Real OpenAI wrapper that implements Microsoft.Extensions.AI.IImageGenerator +public sealed class OpenAIImageGeneratorWrapper : IImageGenerator +{ + private readonly OpenAI.Images.ImageClient _imageClient; + + public OpenAIImageGeneratorWrapper(OpenAI.Images.ImageClient imageClient) + { + _imageClient = imageClient; + } + + public async Task GenerateAsync(ImageGenerationRequest request, ImageGenerationOptions? options = null, CancellationToken cancellationToken = default) + { + try + { + // Create OpenAI-specific image generation request + var prompt = request.Prompt ?? "A beautiful image"; + var count = options?.Count ?? 1; + + ConsoleHelpers.WriteDebugLine($"Calling OpenAI DALL-E API with prompt: '{prompt}'"); + + // Create image generation options + var imageOptions = new OpenAI.Images.ImageGenerationOptions() + { + Size = OpenAI.Images.GeneratedImageSize.W1024xH1024, + ResponseFormat = OpenAI.Images.GeneratedImageFormat.Uri + }; + + // Call OpenAI image generation API + var response = await _imageClient.GenerateImagesAsync(prompt, count, imageOptions, cancellationToken); + + // Convert response to Microsoft.Extensions.AI format + var contents = new List(); + + foreach (var image in response.Value) + { + if (!string.IsNullOrEmpty(image.ImageUri?.ToString())) + { + // Image URL returned - will be downloaded when saved + contents.Add(new UriContent(image.ImageUri, "image/png")); + ConsoleHelpers.WriteDebugLine($"Generated image URL: {image.ImageUri}"); + } + } + + return new ImageGenerationResponse(contents); + } + catch (Exception ex) + { + throw new InvalidOperationException($"OpenAI image generation failed: {ex.Message}", ex); + } + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + return serviceType.IsInstanceOfType(this) ? this : null; + } + + public void Dispose() + { + // ImageClient doesn't implement IDisposable + } +} \ No newline at end of file diff --git a/src/cycod/assets/help/imagine examples.txt b/src/cycod/assets/help/imagine examples.txt new file mode 100644 index 00000000..f231566b --- /dev/null +++ b/src/cycod/assets/help/imagine examples.txt @@ -0,0 +1,137 @@ +CYCOD IMAGINE EXAMPLES + + Generate AI images from text prompts for development, design, and creative projects. + +BASIC IMAGE GENERATION + + EXAMPLE 1: Generate a simple image + + cycod imagine "a beautiful sunset over mountains" + + EXAMPLE 2: Create an app icon + + cycod imagine "weather app icon, minimalist design, blue and white colors" + + EXAMPLE 3: Generate multiple variations + + cycod imagine "logo for a coffee shop" --count 3 + +DEVELOPMENT USE CASES + + EXAMPLE 4: Create placeholder user avatars + + cycod imagine --inputs "professional headshot" "casual avatar" "cartoon character" \ + --style natural --output ./avatars + + EXAMPLE 5: Generate app icons for different categories + + cycod imagine "productivity app icon" --count 2 --style vivid --output ./icons + cycod imagine "entertainment app icon" --count 2 --style vivid --output ./icons + + EXAMPLE 6: Create hero images for website + + cycod imagine "modern office workspace, bright lighting, technology" \ + --size 1792x1024 --quality hd --output ./hero-images + +BATCH OPERATIONS + + EXAMPLE 7: Generate icons with variable substitution + + cycod imagine "{{category}} app icon, modern flat design" \ + --foreach var category in weather music notes calendar camera \ + --output ./app-icons + + EXAMPLE 8: Create variations of the same concept + + cycod imagine "startup logo, {{style}} design, professional" \ + --foreach var style in minimalist vintage modern futuristic \ + --count 2 + + EXAMPLE 9: Generate images for different screen sizes + + cycod imagine "mobile app background, gradient colors" \ + --foreach var size in 375x812 414x896 1920x1080 \ + --size {{size}} + +CREATIVE PROJECTS + + EXAMPLE 10: Game asset creation + + cycod imagine --inputs "fantasy sword weapon" "magic potion bottle" "treasure chest" \ + --style vivid --count 2 --output ./game-assets + + EXAMPLE 11: Marketing materials + + cycod imagine "product showcase, professional photography style" \ + --quality hd --size 1024x1024 --output ./marketing + + EXAMPLE 12: Social media content + + cycod imagine "team collaboration, office environment, diverse people" \ + --size 1080x1080 --style natural --output ./social + +ADVANCED TECHNIQUES + + EXAMPLE 13: Iterative design process + + cycod imagine "logo concept for tech startup" + # Review generated images, then refine + cycod imagine "logo concept for tech startup, more geometric, blue accent" + cycod imagine "logo concept for tech startup, geometric, blue and gray, minimal" + + EXAMPLE 14: Style consistency across multiple images + + cycod imagine --inputs "user dashboard screenshot" "settings page mockup" "profile page layout" \ + --style natural --count 1 --output ./ui-mockups + # Add consistent prompt suffix: ", clean UI design, white background, modern interface" + + EXAMPLE 15: High-quality artwork for presentations + + cycod imagine "data visualization concept, charts and graphs, professional" \ + --quality hd --size 1792x1024 --format png --output ./presentations + +CONFIGURATION EXAMPLES + + EXAMPLE 16: Set up Azure OpenAI for image generation + + cycod config set Azure.OpenAI.Endpoint "https://eastus.api.cognitive.microsoft.com/" --user + cycod config set Azure.OpenAI.ApiKey "your-azure-api-key" --user + # Verify with: cycod imagine "test image generation" + + EXAMPLE 17: Use specific provider + + cycod imagine "nature landscape" --provider azure + cycod imagine "abstract art" --provider openai + +TROUBLESHOOTING + + EXAMPLE 18: Check configuration + + cycod config list | grep -i openai + cycod config list | grep -i azure + + EXAMPLE 19: Debug image generation + + cycod imagine "simple test image" --debug --verbose + + EXAMPLE 20: Test with minimal prompt + + cycod imagine "red apple" + +TIPS AND BEST PRACTICES + + - Be specific and descriptive in your prompts for better results + - Include style keywords: "photorealistic", "cartoon", "minimalist", "vintage" + - Specify technical details: "4K resolution", "studio lighting", "professional" + - Use negative prompts conceptually: "high quality, not blurry, not distorted" + - For development assets, include context: "app icon", "website hero", "user avatar" + - Save high-quality versions for final use: --quality hd + - Organize output with descriptive directory names + +SEE ALSO + + cycod help imagine + cycod help config + cycod help use azure openai + cycod help use openai + cycod help options \ No newline at end of file diff --git a/src/cycod/assets/help/imagine.txt b/src/cycod/assets/help/imagine.txt new file mode 100644 index 00000000..4c1753ae --- /dev/null +++ b/src/cycod/assets/help/imagine.txt @@ -0,0 +1,94 @@ +CYCOD IMAGINE COMMAND + + Generate AI images from text prompts using DALL-E or other image generation models. + +USAGE: cycod imagine [PROMPTS...] [OPTIONS] + OR: cycod imagine --input "PROMPT" [OPTIONS] + OR: cycod imagine --inputs "PROMPT1" "PROMPT2" [...] [OPTIONS] + +ARGUMENTS + + PROMPTS... One or more text prompts to generate images from + +OPTIONS + + IMAGE GENERATION OPTIONS + + --input "PROMPT" Generate image from a single text prompt + --inputs "PROMPT1" [...] Generate images from multiple text prompts + + --count COUNT, -c COUNT Number of images to generate per prompt (1-10, default: 1) + --size SIZE, -s SIZE Image size (default: 1024x1024) + Supported: 256x256, 512x512, 1024x1024, 1792x1024, 1024x1792 + --style STYLE Image style: vivid or natural (default: vivid) + --quality QUALITY, -q QUALITY Image quality: standard or hd (default: standard) + --format FORMAT, -f FORMAT Output format: png or jpg (default: png) + + OUTPUT OPTIONS + + --output DIR, -o DIR Output directory (default: current directory) + --provider PROVIDER, -p PROVIDER Use specific provider (openai, azure, etc.) + +EXAMPLES + + EXAMPLE 1: Generate a single image + + cycod imagine "a beautiful sunset over mountains" + + EXAMPLE 2: Generate multiple variations + + cycod imagine "app icon for weather app" --count 3 + + EXAMPLE 3: Generate multiple different images + + cycod imagine --inputs "weather icon" "calendar icon" "notes icon" + + EXAMPLE 4: Specify size and quality + + cycod imagine "logo design" --size 1024x1024 --quality hd --style natural + + EXAMPLE 5: Save to specific directory + + cycod imagine "user avatar" --output ./generated --format jpg + + EXAMPLE 6: Use with variables and loops + + cycod imagine "{{animal}} in a park" --foreach var animal in dog cat bird + + EXAMPLE 7: Batch generate app icons + + cycod imagine --inputs "weather app icon" "music player icon" "todo list icon" \ + --count 2 --style natural --output ./icons + +CONFIGURATION + + Image generation requires an AI provider that supports image generation: + + AZURE OPENAI (Recommended) + cycod config set Azure.OpenAI.Endpoint "https://your-service.openai.azure.com/" --user + cycod config set Azure.OpenAI.ApiKey "your-api-key" --user + + OPENAI API + cycod config set OPENAI_API_KEY "sk-your-api-key" --user + +PROVIDER REQUIREMENTS + + - Azure OpenAI: Requires DALL-E deployment (dall-e-2 or dall-e-3) + - OpenAI API: Automatically uses DALL-E 3 + - Provider must support image generation capabilities + +NOTES + + - Generated images are saved with descriptive filenames including timestamps + - Image generation may take 10-30 seconds per image depending on provider + - Different providers may have varying rate limits and capabilities + - All image generation features are currently experimental (MEAI001) + +SEE ALSO + + cycod help image + cycod help config + cycod help use azure openai + cycod help use openai + cycod help options + cycod help examples \ No newline at end of file diff --git a/src/cycod/assets/help/usage.txt b/src/cycod/assets/help/usage.txt index 3cb2ebaf..1f683563 100644 --- a/src/cycod/assets/help/usage.txt +++ b/src/cycod/assets/help/usage.txt @@ -1,6 +1,7 @@ Using CYCOD, you can: - Chat with an AI model (interactively/programmatically) + - Generate AI images from text prompts - Provide one or more inputs to the AI model - Use built-in function tools to: - Read and write files @@ -13,6 +14,7 @@ COMMANDS cycod [...] (aka: cycod chat) cycod chat [...] (see: cycod help chat) + cycod imagine [...] (see: cycod help imagine) cycod config [...] (see: cycod help config) cycod github [...] (see: cycod help github) @@ -25,4 +27,5 @@ SEE ALSO cycod help cycod help examples + cycod help imagine cycod help github login From 77a03ec299364b87b65f8d637cf8821d52796f6b Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Tue, 23 Dec 2025 07:37:59 -0800 Subject: [PATCH 5/8] feat: Enhance ImagineCommand to support custom image generation options --- .../CommandLineCommands/ImagineCommand.cs | 93 ++++++++++++++++--- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/src/cycod/CommandLineCommands/ImagineCommand.cs b/src/cycod/CommandLineCommands/ImagineCommand.cs index 2cc8b949..0ab825c3 100644 --- a/src/cycod/CommandLineCommands/ImagineCommand.cs +++ b/src/cycod/CommandLineCommands/ImagineCommand.cs @@ -72,7 +72,14 @@ public override async Task ExecuteAsync(bool interactive) var options = new ImageGenerationOptions { Count = Count, - ModelId = "dall-e-3" + ModelId = "dall-e-3", + AdditionalProperties = new Microsoft.Extensions.AI.AdditionalPropertiesDictionary + { + ["size"] = Size, + ["style"] = Style, + ["quality"] = Quality, + ["format"] = Format + } }; var response = await imageGenerator.GenerateAsync(new ImageGenerationRequest(prompt), options); @@ -302,32 +309,88 @@ public async Task GenerateAsync(ImageGenerationRequest var prompt = request.Prompt ?? "A beautiful image"; var count = options?.Count ?? 1; - ConsoleHelpers.WriteDebugLine($"Calling OpenAI DALL-E API with prompt: '{prompt}'"); + // DALL-E 3 only supports n=1, so we need to handle count differently + if (count > 1) + { + ConsoleHelpers.WriteWarning($"DALL-E 3 only supports generating 1 image at a time. Generating {count} images sequentially..."); + } + + // Get custom options from AdditionalProperties + var sizeStr = options?.AdditionalProperties?.GetValueOrDefault("size") as string ?? "1024x1024"; + var style = options?.AdditionalProperties?.GetValueOrDefault("style") as string ?? "vivid"; + var quality = options?.AdditionalProperties?.GetValueOrDefault("quality") as string ?? "standard"; + var format = options?.AdditionalProperties?.GetValueOrDefault("format") as string ?? "png"; + + ConsoleHelpers.WriteDebugLine($"Calling OpenAI DALL-E API with prompt: '{prompt}', size: {sizeStr}, style: {style}, quality: {quality}"); + + // Parse size string to OpenAI enum + var imageSize = sizeStr switch + { + "256x256" => OpenAI.Images.GeneratedImageSize.W256xH256, + "512x512" => OpenAI.Images.GeneratedImageSize.W512xH512, + "1024x1024" => OpenAI.Images.GeneratedImageSize.W1024xH1024, + "1792x1024" => OpenAI.Images.GeneratedImageSize.W1792xH1024, + "1024x1792" => OpenAI.Images.GeneratedImageSize.W1024xH1792, + _ => OpenAI.Images.GeneratedImageSize.W1024xH1024 + }; + + // Parse style + var imageStyle = style.ToLower() switch + { + "natural" => OpenAI.Images.GeneratedImageStyle.Natural, + "vivid" => OpenAI.Images.GeneratedImageStyle.Vivid, + _ => OpenAI.Images.GeneratedImageStyle.Vivid + }; + + // Parse quality + var imageQuality = quality.ToLower() switch + { + "hd" => OpenAI.Images.GeneratedImageQuality.High, + "standard" => OpenAI.Images.GeneratedImageQuality.Standard, + _ => OpenAI.Images.GeneratedImageQuality.Standard + }; + + // Parse format (note: API returns URLs, actual format is handled at download) + var responseFormat = format.ToLower() == "b64_json" + ? OpenAI.Images.GeneratedImageFormat.Bytes + : OpenAI.Images.GeneratedImageFormat.Uri; // Create image generation options var imageOptions = new OpenAI.Images.ImageGenerationOptions() { - Size = OpenAI.Images.GeneratedImageSize.W1024xH1024, - ResponseFormat = OpenAI.Images.GeneratedImageFormat.Uri + Size = imageSize, + Style = imageStyle, + Quality = imageQuality, + ResponseFormat = responseFormat }; - // Call OpenAI image generation API - var response = await _imageClient.GenerateImagesAsync(prompt, count, imageOptions, cancellationToken); + // Collect all generated images + var allContents = new List(); - // Convert response to Microsoft.Extensions.AI format - var contents = new List(); - - foreach (var image in response.Value) + // Generate images one at a time (DALL-E 3 limitation) + for (int i = 0; i < count; i++) { - if (!string.IsNullOrEmpty(image.ImageUri?.ToString())) + // Call OpenAI image generation API + var response = await _imageClient.GenerateImagesAsync(prompt, 1, imageOptions, cancellationToken); + + foreach (var image in response.Value) { - // Image URL returned - will be downloaded when saved - contents.Add(new UriContent(image.ImageUri, "image/png")); - ConsoleHelpers.WriteDebugLine($"Generated image URL: {image.ImageUri}"); + if (!string.IsNullOrEmpty(image.ImageUri?.ToString())) + { + // Image URL returned - will be downloaded when saved + allContents.Add(new UriContent(image.ImageUri, $"image/{format}")); + ConsoleHelpers.WriteDebugLine($"Generated image URL: {image.ImageUri}"); + } + else if (image.ImageBytes != null) + { + // Base64 data returned + allContents.Add(new DataContent(image.ImageBytes.ToArray(), $"image/{format}")); + ConsoleHelpers.WriteDebugLine($"Generated image bytes: {image.ImageBytes.Length} bytes"); + } } } - return new ImageGenerationResponse(contents); + return new ImageGenerationResponse(allContents); } catch (Exception ex) { From c977215ac23825b4e56b6a64fa2d6d0b709cef0a Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Tue, 23 Dec 2025 07:47:54 -0800 Subject: [PATCH 6/8] feat: Add GenerateImages function tool for natural language image generation --- todo/image-generation-function-tool.md | 250 +++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 todo/image-generation-function-tool.md diff --git a/todo/image-generation-function-tool.md b/todo/image-generation-function-tool.md new file mode 100644 index 00000000..1862be65 --- /dev/null +++ b/todo/image-generation-function-tool.md @@ -0,0 +1,250 @@ +# Image Generation as AI Function Tool + +## Overview + +Add `GenerateImages` as an AI function tool to enable natural language image generation during chat sessions, complementing the existing `cycod imagine` CLI command. + +## Current State + +- `cycod imagine "prompt"` - CLI command for explicit image generation +- `--image FILE` / `/image FILE` - Add existing images to conversation (input) +- No way to generate images naturally during chat without exiting + +## Proposed Solution + +Implement `GenerateImages` as a function tool that AI can invoke during conversation. + +### User Experience + +**Instead of:** +```bash +# Exit chat or use complex slash command +cycod imagine "weather app icon" --count 3 --size 1024x1024 +``` + +**Natural conversation:** +``` +User: Can you create me three weather app icons? Make them minimalist and blue. + +AI: I'll generate three weather app icons for you. +[Function call: GenerateImages approved] +โœ“ Generated weather-icon-20250113-143022-1.png +โœ“ Generated weather-icon-20250113-143022-2.png +โœ“ Generated weather-icon-20250113-143022-3.png + +I've created three minimalist blue weather app icons... +``` + +## Benefits + +1. **More Natural** - Users describe intent, AI handles implementation +2. **Better Iteration** - Conversational refinement workflow + - "Create a logo" โ†’ "Make it more vintage" โ†’ "Add warm colors" +3. **AI Value-Add** - Prompt engineering, smart defaults, context awareness +4. **Consistent** - Matches existing function tool patterns +5. **Flexible** - CLI command remains for scripting/batch operations + +## Function Tool Design + +### Schema + +```typescript +{ + name: "GenerateImages", + description: "Generate images from text descriptions using AI (DALL-E)", + parameters: { + prompt: { + type: "string", + description: "Detailed image description. Be specific about style, colors, composition.", + required: true + }, + count: { + type: "integer", + description: "Number of variations (1-10)", + default: 1 + }, + size: { + type: "string", + enum: ["1024x1024", "1792x1024", "1024x1792"], + description: "Dimensions. Use 1792x1024 for hero/landscape", + default: "1024x1024" + }, + quality: { + type: "string", + enum: ["standard", "hd"], + default: "standard" + }, + style: { + type: "string", + enum: ["vivid", "natural"], + description: "vivid=dramatic, natural=photorealistic", + default: "vivid" + }, + add_to_conversation: { + type: "boolean", + description: "Add generated images to conversation for analysis", + default: false + }, + output_directory: { + type: "string", + description: "Save location", + default: "." + } + } +} +``` + +### Tool Response Format + +```json +{ + "success": true, + "images": [ + { + "path": "./weather-icon-20250113-143022-1.png", + "prompt": "weather app icon, minimalist design, blue and white", + "size": "1024x1024", + "format": "png" + } + ], + "count": 1, + "added_to_conversation": false +} +``` + +## Implementation Plan + +### 1. Extract Image Generation Logic + +- Move logic from `ImagineCommand` to shared service +- Create `ImageGenerationService.cs` (or similar) +- Service handles both CLI and function tool calls + +### 2. Create Function Tool + +- Add to function tool catalog +- Implement tool handler (e.g., `ImageGenerationFunctionTool.cs`) +- Handle parameter validation and defaults + +### 3. Integration + +- Register tool in function tool system +- Ensure provider compatibility (Azure OpenAI, OpenAI) +- Handle errors gracefully with user-friendly messages + +### 4. Documentation + +- Update function calls help +- Add examples to help system +- Document auto-approval options + +### 5. Testing + +- Unit tests for service extraction +- Integration tests for tool invocation +- Test with different providers +- Test auto-add to conversation feature + +## Key Design Decisions to Consider + +### 1. Cost Control + +Image generation costs money: +- **Option A**: Require approval by default (like write operations) +- **Option B**: Covered by `--auto-approve write` +- **Option C**: Specific approval: `--auto-approve GenerateImages` +- **Recommendation**: Default requires approval, can be auto-approved per user preference + +### 2. Auto-Add to Conversation + +When should generated images be added to conversation? +- **Option A**: Always auto-add (AI can "see" what it created) +- **Option B**: Never auto-add (keeps conversation light) +- **Option C**: AI decides via `add_to_conversation` parameter +- **Recommendation**: Option C - context-dependent via parameter + +### 3. Prompt Engineering + +Who crafts the DALL-E prompt? +- **Option A**: Pass user request verbatim +- **Option B**: AI enhances prompt for better results +- **Recommendation**: Option B - AI adds details for optimal generation + +### 4. File Management + +Where do generated images go? +- Default to current directory (like CLI) +- AI can specify `output_directory` parameter +- Could add smart defaults based on prompt context + +## Use Cases + +### Iterative Refinement +``` +User: Create a coffee shop logo +AI: [generates] +User: Make it more vintage +AI: [regenerates with vintage style] +User: Add warm brown tones +AI: [refines further] +``` + +### Batch Generation with Context +``` +User: I need icons for sunny, rainy, and cloudy weather +AI: [generates 3 with consistent style] +``` + +### Smart Defaults +``` +User: Generate a hero image for my landing page +AI: [infers 1792x1024 landscape, hd quality, natural style] +``` + +## Comparison: CLI vs Function Tool + +| Aspect | `cycod imagine` | Function Tool | +|--------|-----------------|---------------| +| Explicitness | High | Low | +| Convenience | Low (exit chat) | High (in flow) | +| Natural language | No | Yes | +| AI enhancement | No | Yes | +| Iteration | Awkward | Natural | +| Control | Full | Delegated | +| Scripting | Excellent | N/A | + +**Both should coexist** - different use cases. + +## Open Questions + +1. Should there be rate limiting or cost warnings? +2. How verbose should approval prompts be? +3. Should we track and report cumulative costs? +4. File naming: timestamps vs AI-suggested names? +5. Multi-image batching limits? +6. Integration with existing `--image` / `/image` features? + +## Related Work + +- Existing `ImagineCommand` implementation +- Function tool infrastructure +- Image handling in conversation context +- Provider abstraction (Azure OpenAI, OpenAI) + +## Priority + +**Medium** - Nice quality-of-life improvement, not critical functionality. + +## Related Considerations: `--add-image` Naming + +While implementing this, also consider: +- Renaming `--image` to `--add-image` for consistency with `--add-system-prompt`, `--add-user-prompt` +- Could keep `--image` as alias for convenience +- Slash command `/image` should probably stay short (all slash commands are brief) + +## Notes + +- Image generation is marked as experimental (MEAI001) +- Requires provider with DALL-E support +- Generated files include timestamps in names +- Generation takes 10-30 seconds per image From fe6f606562d7ef6ab762e08616838e1a2fa3353e Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Tue, 23 Dec 2025 07:54:51 -0800 Subject: [PATCH 7/8] fix: Remove emoticons from console output to comply with UI conventions --- src/cycod/CommandLineCommands/ImagineCommand.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cycod/CommandLineCommands/ImagineCommand.cs b/src/cycod/CommandLineCommands/ImagineCommand.cs index 0ab825c3..bcdadc77 100644 --- a/src/cycod/CommandLineCommands/ImagineCommand.cs +++ b/src/cycod/CommandLineCommands/ImagineCommand.cs @@ -52,7 +52,7 @@ public override async Task ExecuteAsync(bool interactive) return 1; } - ConsoleHelpers.WriteLine($"๐ŸŽจ Imagining {Prompts.Count} image{(Prompts.Count == 1 ? "" : "s")}...\n", ConsoleColor.Cyan); + ConsoleHelpers.WriteLine($"Generating {Prompts.Count} image{(Prompts.Count == 1 ? "" : "s")}...\n", ConsoleColor.Cyan); // Create a working image generator (using the pattern from ImageAIExample) var imageGenerator = CreateWorkingImageGenerator(); @@ -67,7 +67,7 @@ public override async Task ExecuteAsync(bool interactive) int totalGenerated = 0; foreach (var prompt in Prompts) { - ConsoleHelpers.WriteLine($"๐Ÿ’ญ Generating: \"{prompt}\""); + ConsoleHelpers.WriteLine($"Generating: \"{prompt}\"", ConsoleColor.DarkGray); var options = new ImageGenerationOptions { @@ -87,7 +87,7 @@ public override async Task ExecuteAsync(bool interactive) totalGenerated += await SaveGeneratedImages(response, prompt, totalGenerated); } - ConsoleHelpers.WriteLine($"\nโœ… Successfully generated {totalGenerated} image{(totalGenerated == 1 ? "" : "s")}!", ConsoleColor.Green); + ConsoleHelpers.WriteLine($"\nSuccessfully generated {totalGenerated} image{(totalGenerated == 1 ? "" : "s")}!", ConsoleColor.Green); return 0; } catch (Exception ex) @@ -201,7 +201,7 @@ private async Task SaveGeneratedImages(ImageGenerationResponse response, st var filePath = Path.Combine(OutputDirectory, fileName); await File.WriteAllBytesAsync(filePath, dataContent.Data.ToArray()); - ConsoleHelpers.WriteLine($"๐Ÿ’พ Saved: {fileName}"); + ConsoleHelpers.WriteLine($"Saved: {fileName}", ConsoleColor.DarkGray); saved++; } else if (content is UriContent uriContent) @@ -214,7 +214,7 @@ private async Task SaveGeneratedImages(ImageGenerationResponse response, st var filePath = Path.Combine(OutputDirectory, fileName); await File.WriteAllBytesAsync(filePath, imageData); - ConsoleHelpers.WriteLine($"๐Ÿ’พ Saved: {fileName}"); + ConsoleHelpers.WriteLine($"Saved: {fileName}", ConsoleColor.DarkGray); saved++; } else From 003045657bfa183eeb9e0e0f123986bd7bb05b56 Mon Sep 17 00:00:00 2001 From: Rob Chambers Date: Tue, 23 Dec 2025 08:50:24 -0800 Subject: [PATCH 8/8] save images as i go... --- .../CommandLineCommands/ImagineCommand.cs | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/src/cycod/CommandLineCommands/ImagineCommand.cs b/src/cycod/CommandLineCommands/ImagineCommand.cs index bcdadc77..4bdf71c6 100644 --- a/src/cycod/CommandLineCommands/ImagineCommand.cs +++ b/src/cycod/CommandLineCommands/ImagineCommand.cs @@ -67,24 +67,29 @@ public override async Task ExecuteAsync(bool interactive) int totalGenerated = 0; foreach (var prompt in Prompts) { - ConsoleHelpers.WriteLine($"Generating: \"{prompt}\"", ConsoleColor.DarkGray); + ConsoleHelpers.WriteLine($"Generating {Count} image{(Count == 1 ? "" : "s")} for: \"{prompt}\"", ConsoleColor.DarkGray); - var options = new ImageGenerationOptions + // Generate and save each image immediately for consistent behavior + for (int imageIndex = 0; imageIndex < Count; imageIndex++) { - Count = Count, - ModelId = "dall-e-3", - AdditionalProperties = new Microsoft.Extensions.AI.AdditionalPropertiesDictionary + var options = new ImageGenerationOptions { - ["size"] = Size, - ["style"] = Style, - ["quality"] = Quality, - ["format"] = Format - } - }; + Count = 1, // Always generate one image at a time + ModelId = "dall-e-3", + AdditionalProperties = new Microsoft.Extensions.AI.AdditionalPropertiesDictionary + { + ["size"] = Size, + ["style"] = Style, + ["quality"] = Quality, + ["format"] = Format + } + }; - var response = await imageGenerator.GenerateAsync(new ImageGenerationRequest(prompt), options); - - totalGenerated += await SaveGeneratedImages(response, prompt, totalGenerated); + var response = await imageGenerator.GenerateAsync(new ImageGenerationRequest(prompt), options); + + // Save this single image immediately + totalGenerated += await SaveGeneratedImages(response, prompt, totalGenerated); + } } ConsoleHelpers.WriteLine($"\nSuccessfully generated {totalGenerated} image{(totalGenerated == 1 ? "" : "s")}!", ConsoleColor.Green); @@ -177,7 +182,7 @@ private IImageGenerator CreateMockImageGenerator() { GenerateImagesAsyncCallback = (request, options, ct) => { - // Create a more realistic mock PNG image + // Always generate one image (consistent with new approach) var mockImageBytes = CreateMockImageData(request.Prompt ?? "unknown"); var content = new DataContent(mockImageBytes, "image/png") { Name = "generated_image.png" }; var response = new ImageGenerationResponse([content]); @@ -309,10 +314,10 @@ public async Task GenerateAsync(ImageGenerationRequest var prompt = request.Prompt ?? "A beautiful image"; var count = options?.Count ?? 1; - // DALL-E 3 only supports n=1, so we need to handle count differently - if (count > 1) + // Since we now always generate one image at a time, warn if count != 1 + if (count != 1) { - ConsoleHelpers.WriteWarning($"DALL-E 3 only supports generating 1 image at a time. Generating {count} images sequentially..."); + ConsoleHelpers.WriteWarning($"OpenAI wrapper called with count={count}, but will only generate 1 image. Use command-level Count handling instead."); } // Get custom options from AdditionalProperties @@ -364,29 +369,23 @@ public async Task GenerateAsync(ImageGenerationRequest ResponseFormat = responseFormat }; - // Collect all generated images + // Generate single image + var response = await _imageClient.GenerateImagesAsync(prompt, 1, imageOptions, cancellationToken); var allContents = new List(); - // Generate images one at a time (DALL-E 3 limitation) - for (int i = 0; i < count; i++) + foreach (var image in response.Value) { - // Call OpenAI image generation API - var response = await _imageClient.GenerateImagesAsync(prompt, 1, imageOptions, cancellationToken); - - foreach (var image in response.Value) + if (!string.IsNullOrEmpty(image.ImageUri?.ToString())) { - if (!string.IsNullOrEmpty(image.ImageUri?.ToString())) - { - // Image URL returned - will be downloaded when saved - allContents.Add(new UriContent(image.ImageUri, $"image/{format}")); - ConsoleHelpers.WriteDebugLine($"Generated image URL: {image.ImageUri}"); - } - else if (image.ImageBytes != null) - { - // Base64 data returned - allContents.Add(new DataContent(image.ImageBytes.ToArray(), $"image/{format}")); - ConsoleHelpers.WriteDebugLine($"Generated image bytes: {image.ImageBytes.Length} bytes"); - } + // Image URL returned - will be downloaded when saved + allContents.Add(new UriContent(image.ImageUri, $"image/{format}")); + ConsoleHelpers.WriteDebugLine($"Generated image URL: {image.ImageUri}"); + } + else if (image.ImageBytes != null) + { + // Base64 data returned + allContents.Add(new DataContent(image.ImageBytes.ToArray(), $"image/{format}")); + ConsoleHelpers.WriteDebugLine($"Generated image bytes: {image.ImageBytes.Length} bytes"); } }